@operato/twin-kernel 0.2.3 → 0.4.0
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/README.md +46 -5
- package/dist/capability.js +2 -2
- package/dist/capacity.d.ts +99 -0
- package/dist/capacity.js +172 -0
- package/dist/contract.d.ts +836 -45
- package/dist/contract.js +519 -7
- package/dist/divergence.d.ts +1 -1
- package/dist/divergence.js +8 -5
- package/dist/domain-catalog.d.ts +4 -4
- package/dist/domain-catalog.js +4 -4
- package/dist/domain-definition.d.ts +50 -5
- package/dist/domain-definition.js +6 -6
- package/dist/epcis.d.ts +12 -0
- package/dist/epcis.js +12 -0
- package/dist/event-journal.d.ts +31 -1
- package/dist/event-journal.js +27 -1
- package/dist/flow-engine.d.ts +323 -36
- package/dist/flow-engine.js +868 -164
- package/dist/forecast.d.ts +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/job-response.d.ts +59 -0
- package/dist/job-response.js +56 -0
- package/dist/kernel.d.ts +5 -0
- package/dist/kernel.js +31 -11
- package/dist/mes-kernel.d.ts +28 -0
- package/dist/mes-kernel.js +83 -29
- package/dist/mes-profile.d.ts +2 -2
- package/dist/mes-profile.js +11 -11
- package/dist/observed-reducer.d.ts +68 -7
- package/dist/observed-reducer.js +248 -41
- package/dist/task-fold.d.ts +2 -2
- package/dist/task-fold.js +4 -4
- package/dist/vocabulary.d.ts +19 -0
- package/dist/vocabulary.js +66 -0
- package/dist/wms-profile.d.ts +2 -2
- package/dist/wms-profile.js +6 -6
- package/dist/yms-kernel.js +36 -15
- package/dist/yms-profile.d.ts +2 -2
- package/dist/yms-profile.js +6 -6
- package/dist-cjs/index.cjs +1618 -270
- package/package.json +1 -1
package/dist/forecast.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export interface ForecastTwin {
|
|
|
8
8
|
/**
|
|
9
9
|
* 시뮬 시각(ms) — **구동 루프가 시각만 읽을 때 쓰는 값.**
|
|
10
10
|
*
|
|
11
|
-
* 없으면 `getSnapshot().simClockMs` 로 떨어지지만, 그 경로는 전
|
|
11
|
+
* 없으면 `getSnapshot().simClockMs` 로 떨어지지만, 그 경로는 전 자리·물품·자원·작업·오더를 새로
|
|
12
12
|
* 재료화하고 주목 신호까지 계산한 뒤 숫자 하나만 꺼내 버린다 — tick 마다 그러면 상태 크기 × 지평선
|
|
13
13
|
* 길이만큼 낭비가 쌓인다(물품 1만 건·30분 지평선에서 측정: 루프 조건에만 149ms vs 0ms).
|
|
14
14
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -15,12 +15,15 @@ export * from './iso-duration.ts';
|
|
|
15
15
|
export * from './observed-reducer.ts';
|
|
16
16
|
export * from './state-projector.ts';
|
|
17
17
|
export * from './task-fold.ts';
|
|
18
|
+
export * from './job-response.ts';
|
|
18
19
|
export * from './face2-adapter.ts';
|
|
19
20
|
export * from './runtime.ts';
|
|
20
21
|
export * from './yms-profile.ts';
|
|
21
22
|
export * from './mes-profile.ts';
|
|
22
23
|
export * from './flow-engine.ts';
|
|
24
|
+
export * from './capacity.ts';
|
|
23
25
|
export { WmsKernel } from './kernel.ts';
|
|
24
26
|
export { YmsKernel } from './yms-kernel.ts';
|
|
25
27
|
export { MesKernel, MES_PART_GTINS, MES_PRODUCT_GTINS, MES_PRODUCTS } from './mes-kernel.ts';
|
|
26
28
|
export type { MesDefinitionSpec } from './mes-kernel.ts';
|
|
29
|
+
export * from './vocabulary.ts';
|
package/dist/index.js
CHANGED
|
@@ -15,11 +15,14 @@ export * from "./iso-duration.js";
|
|
|
15
15
|
export * from "./observed-reducer.js";
|
|
16
16
|
export * from "./state-projector.js";
|
|
17
17
|
export * from "./task-fold.js";
|
|
18
|
+
export * from "./job-response.js";
|
|
18
19
|
export * from "./face2-adapter.js";
|
|
19
20
|
export * from "./runtime.js";
|
|
20
21
|
export * from "./yms-profile.js";
|
|
21
22
|
export * from "./mes-profile.js";
|
|
22
23
|
export * from "./flow-engine.js";
|
|
24
|
+
export * from "./capacity.js";
|
|
23
25
|
export { WmsKernel } from "./kernel.js";
|
|
24
26
|
export { YmsKernel } from "./yms-kernel.js";
|
|
25
27
|
export { MesKernel, MES_PART_GTINS, MES_PRODUCT_GTINS, MES_PRODUCTS } from "./mes-kernel.js";
|
|
28
|
+
export * from "./vocabulary.js";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ISOTime } from './contract.ts';
|
|
2
|
+
import { type TaskDeltaRow } from './task-fold.ts';
|
|
3
|
+
/** 표준 `JobState` — 우리 작업 상태를 표준 낱말로 옮긴다. */
|
|
4
|
+
export type JobState = 'ready' | 'running' | 'completed';
|
|
5
|
+
/** 자원 실적 한 줄 — 표준 `OpPersonnelActualType`/`OpEquipmentActualType` 의 우리 부분집합. */
|
|
6
|
+
export interface ResourceActual {
|
|
7
|
+
/** 표준 `PersonID`/`EquipmentID`. */
|
|
8
|
+
id: string;
|
|
9
|
+
/** 표준 `Quantity`. 우리는 개체를 지목하므로 언제나 1이다(등급 단위 집계는 소비처의 일). */
|
|
10
|
+
quantity: number;
|
|
11
|
+
}
|
|
12
|
+
/** 자재 실적 한 줄 — 표준 `OpMaterialActualType` 의 우리 부분집합. */
|
|
13
|
+
export interface MaterialActual {
|
|
14
|
+
/** 표준 `MaterialDefinitionID`. */
|
|
15
|
+
definitionId: string;
|
|
16
|
+
/** 표준 `MaterialUse`. */
|
|
17
|
+
use: 'consumed' | 'produced';
|
|
18
|
+
quantity: number;
|
|
19
|
+
uom?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface JobResponse {
|
|
22
|
+
/** 표준 `ID` — 작업 식별자. */
|
|
23
|
+
id: string;
|
|
24
|
+
/** 표준 `JobOrderID` — 소속 오더. **없으면 없는 것이다**(다른 식별자로 대체하지 않는다). */
|
|
25
|
+
jobOrderId?: string;
|
|
26
|
+
/** 표준 `WorkType` — 작업 종류(우리 `kind`). */
|
|
27
|
+
workType?: string;
|
|
28
|
+
/** 표준 `JobState`. */
|
|
29
|
+
jobState: JobState;
|
|
30
|
+
/** 표준 `StartTime` — **실제** 착수. 착수 기록을 못 찾으면 없다(짐작하지 않는다). */
|
|
31
|
+
startTime?: ISOTime;
|
|
32
|
+
/** 표준 `EndTime` — **실제** 완료. */
|
|
33
|
+
endTime?: ISOTime;
|
|
34
|
+
/** 표준 `PersonnelActual`. */
|
|
35
|
+
personnelActual?: ResourceActual[];
|
|
36
|
+
/** 표준 `EquipmentActual`. */
|
|
37
|
+
equipmentActual?: ResourceActual[];
|
|
38
|
+
/**
|
|
39
|
+
* 표준 `PhysicalAssetActual` — **실제로 쓰인 반복사용 자산**(팔레트·용기 등).
|
|
40
|
+
*
|
|
41
|
+
* 작업이 이미 들고 있던 사실인데 실적에서 빠져 있었다. 빠지면 "그 출고에 팔레트 몇 장이 나갔나" 를
|
|
42
|
+
* 실적으로 답할 수 없고, 자산 회수 계획이 근거를 잃는다.
|
|
43
|
+
*/
|
|
44
|
+
physicalAssetActual?: ResourceActual[];
|
|
45
|
+
/** 표준 `MaterialActual`. */
|
|
46
|
+
materialActual?: MaterialActual[];
|
|
47
|
+
}
|
|
48
|
+
/** 저널 한 줄에서 자원·자재 실적을 읽기 위한 최소 모양(작업 폴드가 보는 것과 같은 줄). */
|
|
49
|
+
type Row = TaskDeltaRow;
|
|
50
|
+
/**
|
|
51
|
+
* 저널을 접어 **작업별 실적 회신**을 만든다 — 표준 이름으로.
|
|
52
|
+
*
|
|
53
|
+
* 짝맞춤(어느 전이가 한 작업인가)은 **커널 규칙**(`foldTaskRecords`)을 그대로 쓴다. 여기서 다시 짝을
|
|
54
|
+
* 맞추면 같은 저널이 지표에서는 3건, 실적에서는 4건이 되는 일이 생긴다 — 실제로 겪은 부류다.
|
|
55
|
+
*
|
|
56
|
+
* 자원·자재 실적은 전이가 실어 온 값에서 모은다. **마지막에 본 값**이 사실이다(재전송이면 나중 것).
|
|
57
|
+
*/
|
|
58
|
+
export declare function foldJobResponses(rows: readonly Row[]): JobResponse[];
|
|
59
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { foldTaskRecords } from "./task-fold.js";
|
|
2
|
+
const iso = (ms) => (typeof ms === 'number' ? new Date(ms).toISOString() : undefined);
|
|
3
|
+
const dataOf = (r) => {
|
|
4
|
+
const p = r.payload;
|
|
5
|
+
return r.data ?? p?.data ?? p ?? {};
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* 저널을 접어 **작업별 실적 회신**을 만든다 — 표준 이름으로.
|
|
9
|
+
*
|
|
10
|
+
* 짝맞춤(어느 전이가 한 작업인가)은 **커널 규칙**(`foldTaskRecords`)을 그대로 쓴다. 여기서 다시 짝을
|
|
11
|
+
* 맞추면 같은 저널이 지표에서는 3건, 실적에서는 4건이 되는 일이 생긴다 — 실제로 겪은 부류다.
|
|
12
|
+
*
|
|
13
|
+
* 자원·자재 실적은 전이가 실어 온 값에서 모은다. **마지막에 본 값**이 사실이다(재전송이면 나중 것).
|
|
14
|
+
*/
|
|
15
|
+
export function foldJobResponses(rows) {
|
|
16
|
+
const { records } = foldTaskRecords(rows);
|
|
17
|
+
/* 자원·자재는 작업 폴드가 모으는 축(facets) 밖이라 여기서 한 번 더 훑는다 — 작업 폴드의 뜻을
|
|
18
|
+
넓히지 않는다(그것은 "언제" 를 아는 모듈이고, 이것은 "무엇으로" 를 아는 모듈이다). */
|
|
19
|
+
const extra = new Map();
|
|
20
|
+
for (const r of rows) {
|
|
21
|
+
const d = dataOf(r);
|
|
22
|
+
const id = typeof d.taskId === 'string' ? d.taskId : undefined;
|
|
23
|
+
if (!id)
|
|
24
|
+
continue;
|
|
25
|
+
const cur = extra.get(id) ?? {};
|
|
26
|
+
if (Array.isArray(d.personnel))
|
|
27
|
+
cur.personnel = d.personnel;
|
|
28
|
+
/* 대표 자원 하나만 온 전이도 있다(`resourceRef`) — 함께 잡힌 목록이 있으면 그것이 더 완전하다. */
|
|
29
|
+
if (Array.isArray(d.resources))
|
|
30
|
+
cur.resources = d.resources;
|
|
31
|
+
else if (typeof d.resourceRef === 'string' && !cur.resources)
|
|
32
|
+
cur.resources = [d.resourceRef];
|
|
33
|
+
if (Array.isArray(d.assets))
|
|
34
|
+
cur.assets = d.assets;
|
|
35
|
+
if (Array.isArray(d.materialActual))
|
|
36
|
+
cur.material = d.materialActual;
|
|
37
|
+
extra.set(id, cur);
|
|
38
|
+
}
|
|
39
|
+
return records.map(rec => {
|
|
40
|
+
const x = extra.get(rec.taskId) ?? {};
|
|
41
|
+
const jobState = rec.completedMs !== undefined ? 'completed' : rec.startedMs !== undefined ? 'running' : 'ready';
|
|
42
|
+
const one = (ids) => ids?.length ? ids.map(id => ({ id, quantity: 1 })) : undefined;
|
|
43
|
+
return {
|
|
44
|
+
id: rec.taskId,
|
|
45
|
+
...(rec.facets.order ? { jobOrderId: rec.facets.order } : {}),
|
|
46
|
+
...(rec.facets.kind ? { workType: rec.facets.kind } : {}),
|
|
47
|
+
jobState,
|
|
48
|
+
...(iso(rec.startedMs) ? { startTime: iso(rec.startedMs) } : {}),
|
|
49
|
+
...(iso(rec.completedMs) ? { endTime: iso(rec.completedMs) } : {}),
|
|
50
|
+
...(one(x.personnel) ? { personnelActual: one(x.personnel) } : {}),
|
|
51
|
+
...(one(x.resources) ? { equipmentActual: one(x.resources) } : {}),
|
|
52
|
+
...(one(x.assets) ? { physicalAssetActual: one(x.assets) } : {}),
|
|
53
|
+
...(x.material?.length ? { materialActual: x.material.map(m => ({ ...m })) } : {})
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
}
|
package/dist/kernel.d.ts
CHANGED
|
@@ -14,6 +14,11 @@ export declare class WmsKernel extends FlowEngine {
|
|
|
14
14
|
protected onOrder(spec: GeneratorSpec): void;
|
|
15
15
|
/** Command 채널 — 오더 즉시 투입(트랜잭션 프론트엔드/prescriptive). */
|
|
16
16
|
protected handleCommand(cmd: Command): CommandAck;
|
|
17
|
+
/**
|
|
18
|
+
* 오더 생성 — 약속(`promised`)이 있으면 **납기와 우선순위를 함께 싣는다**(표준 `EndTime`·`Priority`).
|
|
19
|
+
* 없으면 싣지 않는다: 없는 약속을 만들면 그 뒤 모든 지연 판정이 거짓 위에 선다.
|
|
20
|
+
*/
|
|
21
|
+
/** 오더 생성 — 약속(납기·우선순위)은 base `promiseOf` 가 계산한다(도메인마다 다르게 재지 않는다). */
|
|
17
22
|
private createSalesOrder;
|
|
18
23
|
/**
|
|
19
24
|
* created 오더를 정책으로 할당 → SO TransactionEvent(라인 통합) + pick task(라인별).
|
package/dist/kernel.js
CHANGED
|
@@ -21,11 +21,14 @@ export class WmsKernel extends FlowEngine {
|
|
|
21
21
|
}
|
|
22
22
|
/** 입고 도착 — §4 라이프사이클: ASN(PO) → 팔레트 조립 → 수령 → putaway task. */
|
|
23
23
|
onArrival(spec) {
|
|
24
|
-
const dock = this.
|
|
24
|
+
const dock = this.locationByType('dock');
|
|
25
25
|
if (!dock)
|
|
26
26
|
return;
|
|
27
27
|
const epc = ssccUri(COMPANY_PREFIX, ++this.epcSeq); // 팔레트 SSCC
|
|
28
28
|
const gtin = this.pickGtin(spec.content.skuMix); // SGTIN idpat = epcClass
|
|
29
|
+
/* 품목 구성이 비어 있으면 **도착을 만들지 않는다** — 무엇이 왔는지 말할 수 없는 입고는 사실이 아니다. */
|
|
30
|
+
if (!gtin)
|
|
31
|
+
return;
|
|
29
32
|
const qty = this.randInt(spec.content.qtyPerLine.min, spec.content.qtyPerLine.max); // 케이스 수(비직렬)
|
|
30
33
|
const po = gdtiUri(COMPANY_PREFIX, '401', ++this.poSeq);
|
|
31
34
|
const eventTime = this.now();
|
|
@@ -62,7 +65,10 @@ export class WmsKernel extends FlowEngine {
|
|
|
62
65
|
onOrder(spec) {
|
|
63
66
|
const lpl = spec.content.linesPerOrder;
|
|
64
67
|
if (!lpl) {
|
|
65
|
-
|
|
68
|
+
const only = this.pickGtin(spec.content.skuMix);
|
|
69
|
+
if (!only)
|
|
70
|
+
return; // 품목 구성이 없으면 오더도 없다(무엇을 달라는 오더인지 말할 수 없다)
|
|
71
|
+
this.createSalesOrder([{ gtin: only, qty: this.randInt(spec.content.qtyPerLine.min, spec.content.qtyPerLine.max) }], spec);
|
|
66
72
|
return;
|
|
67
73
|
}
|
|
68
74
|
const n = Math.min(this.randInt(lpl.min, lpl.max), spec.content.skuMix.length);
|
|
@@ -70,10 +76,14 @@ export class WmsKernel extends FlowEngine {
|
|
|
70
76
|
let pool = spec.content.skuMix;
|
|
71
77
|
for (let i = 0; i < n && pool.length > 0; i++) {
|
|
72
78
|
const gtin = this.pickGtin(pool);
|
|
79
|
+
if (!gtin)
|
|
80
|
+
break;
|
|
73
81
|
lines.push({ gtin, qty: this.randInt(spec.content.qtyPerLine.min, spec.content.qtyPerLine.max) });
|
|
74
82
|
pool = pool.filter(m => m.gtin !== gtin); // 라인 SKU 중복 방지(distinct)
|
|
75
83
|
}
|
|
76
|
-
|
|
84
|
+
if (!lines.length)
|
|
85
|
+
return; // 라인이 하나도 안 나오면 오더를 만들지 않는다
|
|
86
|
+
this.createSalesOrder(lines, spec);
|
|
77
87
|
}
|
|
78
88
|
/** Command 채널 — 오더 즉시 투입(트랜잭션 프론트엔드/prescriptive). */
|
|
79
89
|
handleCommand(cmd) {
|
|
@@ -87,13 +97,19 @@ export class WmsKernel extends FlowEngine {
|
|
|
87
97
|
}
|
|
88
98
|
return super.handleCommand(cmd);
|
|
89
99
|
}
|
|
90
|
-
|
|
100
|
+
/**
|
|
101
|
+
* 오더 생성 — 약속(`promised`)이 있으면 **납기와 우선순위를 함께 싣는다**(표준 `EndTime`·`Priority`).
|
|
102
|
+
* 없으면 싣지 않는다: 없는 약속을 만들면 그 뒤 모든 지연 판정이 거짓 위에 선다.
|
|
103
|
+
*/
|
|
104
|
+
/** 오더 생성 — 약속(납기·우선순위)은 base `promiseOf` 가 계산한다(도메인마다 다르게 재지 않는다). */
|
|
105
|
+
createSalesOrder(lines, spec) {
|
|
91
106
|
const id = `order-${++this.orderSeq}`;
|
|
92
107
|
const so = gdtiUri(COMPANY_PREFIX, '402', ++this.soSeq);
|
|
93
108
|
const requested = lines.reduce((s, l) => s + l.qty, 0);
|
|
94
109
|
const order = {
|
|
95
110
|
id, kind: 'outbound', status: 'created', requested, fulfilled: 0, bizTransaction: so,
|
|
96
|
-
allocated: [], picked: [], shipmentEpc: null, lines: lines.map(l => ({ gtin: l.gtin, requested: l.qty }))
|
|
111
|
+
allocated: [], picked: [], shipmentEpc: null, lines: lines.map(l => ({ gtin: l.gtin, requested: l.qty })),
|
|
112
|
+
...(spec ? this.promiseOf(spec) : {})
|
|
97
113
|
};
|
|
98
114
|
this.orders.set(id, order);
|
|
99
115
|
this.emitOrder(order);
|
|
@@ -104,7 +120,7 @@ export class WmsKernel extends FlowEngine {
|
|
|
104
120
|
* 멀티SKU 는 여러 라인의 팔레트를 한 오더로 모아 단일 출하(finalizeOrder 통합 화물).
|
|
105
121
|
*/
|
|
106
122
|
allocate(o) {
|
|
107
|
-
const staging = this.
|
|
123
|
+
const staging = this.locationByType('staging');
|
|
108
124
|
if (!staging || !o.lines)
|
|
109
125
|
return;
|
|
110
126
|
const chosenAll = [];
|
|
@@ -114,7 +130,7 @@ export class WmsKernel extends FlowEngine {
|
|
|
114
130
|
if (need <= 0)
|
|
115
131
|
continue;
|
|
116
132
|
const available = [...this.items.values()]
|
|
117
|
-
.filter(i => i.gtin === line.gtin && i.disposition === DISP.sellable && this.
|
|
133
|
+
.filter(i => i.gtin === line.gtin && i.disposition === DISP.sellable && this.locations.get(i.location)?.type === 'storage')
|
|
118
134
|
.map(i => ({ epc: i.epc, location: i.location, qty: i.qty ?? 1, expiry: i.expiry }));
|
|
119
135
|
const chosen = this.policy.selectStock({ gtin: line.gtin, qty: need, available });
|
|
120
136
|
for (const epc of chosen) {
|
|
@@ -140,9 +156,13 @@ export class WmsKernel extends FlowEngine {
|
|
|
140
156
|
}
|
|
141
157
|
/** 태스크 완료 — 이동 반영 후 putaway=storing, pick=picking(+전량 시 pack→stage→ship). */
|
|
142
158
|
onTaskComplete(t) {
|
|
143
|
-
const item = this.
|
|
144
|
-
|
|
145
|
-
|
|
159
|
+
const item = this.itemByRef(t.itemEpc);
|
|
160
|
+
/* 여기 도달했다면 코어가 이미 물품을 확인했다(주체가 사라진 작업은 완료 전에 접힌다).
|
|
161
|
+
그래도 단정(`!`)은 쓰지 않는다 — 계약이 바뀌면 조용히 틀리는 대신 분명히 멈춘다. */
|
|
162
|
+
if (!item)
|
|
163
|
+
throw new Error(`task ${t.id}: item "${t.itemEpc}" vanished between the core check and the domain hook`);
|
|
164
|
+
const from = this.locations.get(t.fromNode);
|
|
165
|
+
const to = this.locations.get(t.toNode);
|
|
146
166
|
from.occupancy--;
|
|
147
167
|
to.occupancy++;
|
|
148
168
|
item.location = to.id;
|
|
@@ -163,7 +183,7 @@ export class WmsKernel extends FlowEngine {
|
|
|
163
183
|
}
|
|
164
184
|
/** 전량 피킹 → packing(조립)·staging·shipping 마감. 화물 사이트 이탈, 백오더 잔량 재할당. */
|
|
165
185
|
finalizeOrder(order, staging) {
|
|
166
|
-
const shipDock = this.
|
|
186
|
+
const shipDock = this.locationByType('dock-ship') ?? staging;
|
|
167
187
|
const eventTime = this.now();
|
|
168
188
|
const shipment = ssccUri(COMPANY_PREFIX, ++this.epcSeq);
|
|
169
189
|
order.shipmentEpc = shipment;
|
package/dist/mes-kernel.d.ts
CHANGED
|
@@ -36,6 +36,19 @@ export declare class MesKernel extends FlowEngine {
|
|
|
36
36
|
/** 정의-구동 모드(선택). 미지정 시 레거시 하드코딩 경로 — byte-identical. */
|
|
37
37
|
private mesSpec?;
|
|
38
38
|
constructor(tenantId: string, policy?: AllocationPolicy, mesSpec?: MesDefinitionSpec);
|
|
39
|
+
/**
|
|
40
|
+
* **산출을 두 곳에서 만들지 않는다** — 기동 때 막는다.
|
|
41
|
+
*
|
|
42
|
+
* MES 는 레시피로 산출을 만든다: WIP 사슬(스텝마다 앞의 WIP 을 먹고 다음 WIP 을 낸다) · **직렬번호**
|
|
43
|
+
* (회사 프리픽스 + 일련번호) · **수율**(양품/불량이 처분을 가른다) · 생산오더 연결. 코어의 일반
|
|
44
|
+
* 자재 명세(`use: 'produced'`)는 **비직렬 클래스+수량**이라 그 넷을 표현하지 못한다.
|
|
45
|
+
*
|
|
46
|
+
* 그래서 MES 를 일반 선언으로 옮기지 않는다 — 옮기면 정체성·수율·오더 연결을 **잃는다**(§4-20).
|
|
47
|
+
* 대신 둘이 겹치면 **같은 산출을 두 번 만들게** 되므로, 그 조합을 기동 시점에 거부한다.
|
|
48
|
+
* 런타임에 조용히 두 배가 되는 것보다 기동이 실패하는 편이 낫다(재고가 거짓이 되면 그 위의
|
|
49
|
+
* 모든 계산이 거짓이 된다).
|
|
50
|
+
*/
|
|
51
|
+
private assertNoDoubleProduction;
|
|
39
52
|
private productOf;
|
|
40
53
|
/**
|
|
41
54
|
* MES 도메인 커맨드(Tier 2) — mes.changeover: 설비를 제품 gtin 으로 강제 전환.
|
|
@@ -53,11 +66,26 @@ export declare class MesKernel extends FlowEngine {
|
|
|
53
66
|
/** 라우트 스테이션 태스크 발행(공통) — 제자리 가공(process), 이종 자원, 제품 전환 셋업. */
|
|
54
67
|
private emitStation;
|
|
55
68
|
/** op 완료 = 변환. 라우트 인덱스로 분기: 첫=BOM 소비→WIP, 중간=WIP→WIP, 마지막(조립)=WIP→완성차(수율→OEE 품질). */
|
|
69
|
+
/**
|
|
70
|
+
* MES 는 완료 시점에 **오더와 제품 정의**를 딛고 선다 — 둘 중 하나만 없어도 끝맺을 수 없다.
|
|
71
|
+
*
|
|
72
|
+
* 씨앗이 이미 이행된 오더를 심지 않으므로(남은 수량 0) 그 오더에 딸린 작업만 남을 수 있고,
|
|
73
|
+
* 관측이 오더 연결을 담지 못한 작업도 있다. 그때 예전에는 `order.gtin` 에서 던졌고 **그 예외
|
|
74
|
+
* 하나가 정확도 추세 전체를 죽였다.** 이제 코어가 이 답을 보고 그 작업만 접는다.
|
|
75
|
+
*/
|
|
76
|
+
protected canComplete(t: FlowTask): boolean;
|
|
56
77
|
protected onTaskComplete(t: FlowTask): void;
|
|
57
78
|
private recipeDef;
|
|
58
79
|
/** 자재 키 → 구체 gtin 클래스(idpat). 구체 식별은 바인딩+prefix 로 인스턴스가 주입. */
|
|
59
80
|
private classOf;
|
|
60
81
|
private serialOf;
|
|
82
|
+
/**
|
|
83
|
+
* 라우트를 용량 계산에 알려 준다 — 수율을 거슬러 올릴 때 순서가 곧 계산이다.
|
|
84
|
+
*
|
|
85
|
+
* 순서를 모르면 선언 순서대로 세는데, 그러면 하류에서 잃는 몫이 엉뚱한 공정에 얹힌다(조용히
|
|
86
|
+
* 틀린다). 생산 정의를 가진 커널만 이 답을 안다.
|
|
87
|
+
*/
|
|
88
|
+
protected routeKeys(): string[] | undefined;
|
|
61
89
|
/** recipe.route → 오퍼레이션 시퀀스 해소. */
|
|
62
90
|
private routeOps;
|
|
63
91
|
/** 정의 모드 수령 — skuMix gtin 이 레시피 입력 자재면 raw-store 에 생성. */
|
package/dist/mes-kernel.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* ① BOM = 제품별 다품목 레시피(P1=2A+1B / P2=1A+2B — 상이 → 체인지오버 유발).
|
|
7
7
|
* ② 수율 = 일부 불량(non_sellable) → OEE Quality.
|
|
8
8
|
* ③ 셋업/체인지오버 = work-center 가 제품 전환 시 셋업(changeoverKey=제품 gtin) → OEE Availability.
|
|
9
|
-
* ④ OEE = base 가
|
|
9
|
+
* ④ OEE = base 가 설비(설비)별 계측(가동/셋업/기아/품질). cut=cutter·weld=welder 이종 자원.
|
|
10
10
|
*/
|
|
11
11
|
import { firstFitPolicy } from "./allocation-policy.js";
|
|
12
12
|
import { FlowEngine } from "./flow-engine.js";
|
|
@@ -44,10 +44,10 @@ export const MES_PRODUCTS = PRODUCTS.map(p => ({ gtin: p.gtin, label: p.key }));
|
|
|
44
44
|
* 각 스테이션 = 이종 자원(resource) + 제품 전환 시 체인지오버 셋업.
|
|
45
45
|
*/
|
|
46
46
|
const ROUTE = [
|
|
47
|
-
{ kind: 'cut',
|
|
48
|
-
{ kind: 'weld',
|
|
49
|
-
{ kind: 'paint',
|
|
50
|
-
{ kind: 'assembly',
|
|
47
|
+
{ kind: 'cut', locationType: 'cut-station', resource: 'cutter' },
|
|
48
|
+
{ kind: 'weld', locationType: 'weld-station', resource: 'welder' },
|
|
49
|
+
{ kind: 'paint', locationType: 'paint-booth', resource: 'painter' },
|
|
50
|
+
{ kind: 'assembly', locationType: 'assembly-line', resource: 'assembler' }
|
|
51
51
|
];
|
|
52
52
|
export class MesKernel extends FlowEngine {
|
|
53
53
|
wipSeq = 0;
|
|
@@ -58,8 +58,29 @@ export class MesKernel extends FlowEngine {
|
|
|
58
58
|
super(tenantId, policy);
|
|
59
59
|
this.mesSpec = mesSpec;
|
|
60
60
|
/* 정의가 있으면 그 안의 오퍼레이션 명세(소요·변동·모수)를 커널이 소비한다 — 없으면 기본값 경로. */
|
|
61
|
-
if (mesSpec?.definition?.operations)
|
|
61
|
+
if (mesSpec?.definition?.operations) {
|
|
62
62
|
this.loadOperations(mesSpec.definition.operations);
|
|
63
|
+
this.assertNoDoubleProduction(mesSpec.definition.operations);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* **산출을 두 곳에서 만들지 않는다** — 기동 때 막는다.
|
|
68
|
+
*
|
|
69
|
+
* MES 는 레시피로 산출을 만든다: WIP 사슬(스텝마다 앞의 WIP 을 먹고 다음 WIP 을 낸다) · **직렬번호**
|
|
70
|
+
* (회사 프리픽스 + 일련번호) · **수율**(양품/불량이 처분을 가른다) · 생산오더 연결. 코어의 일반
|
|
71
|
+
* 자재 명세(`use: 'produced'`)는 **비직렬 클래스+수량**이라 그 넷을 표현하지 못한다.
|
|
72
|
+
*
|
|
73
|
+
* 그래서 MES 를 일반 선언으로 옮기지 않는다 — 옮기면 정체성·수율·오더 연결을 **잃는다**(§4-20).
|
|
74
|
+
* 대신 둘이 겹치면 **같은 산출을 두 번 만들게** 되므로, 그 조합을 기동 시점에 거부한다.
|
|
75
|
+
* 런타임에 조용히 두 배가 되는 것보다 기동이 실패하는 편이 낫다(재고가 거짓이 되면 그 위의
|
|
76
|
+
* 모든 계산이 거짓이 된다).
|
|
77
|
+
*/
|
|
78
|
+
assertNoDoubleProduction(ops) {
|
|
79
|
+
const bad = ops.filter(o => (o.materialSpecification ?? []).some(m => m.use === 'produced')).map(o => o.key);
|
|
80
|
+
if (!bad.length)
|
|
81
|
+
return;
|
|
82
|
+
throw new Error(`MES recipe already produces outputs — operations [${bad.join(', ')}] must not also declare materialSpecification use:'produced' ` +
|
|
83
|
+
'(that would create the same output twice). Consumption specs are fine; declare production in the recipe.');
|
|
63
84
|
}
|
|
64
85
|
productOf(gtin) { return PRODUCTS.find(p => p.gtin === gtin); }
|
|
65
86
|
/**
|
|
@@ -73,7 +94,7 @@ export class MesKernel extends FlowEngine {
|
|
|
73
94
|
const a = cmd.args;
|
|
74
95
|
if (!a?.resourceId || !a?.gtin)
|
|
75
96
|
return { commandId: cmd.commandId, accepted: false, errorCode: 'changeover-needs-args', error: 'mes.changeover: resourceId and gtin required' };
|
|
76
|
-
const m = this.
|
|
97
|
+
const m = this.equipment.get(a.resourceId);
|
|
77
98
|
if (!m)
|
|
78
99
|
return { commandId: cmd.commandId, accepted: false, errorCode: 'resource-not-found', errorParams: { resourceId: a.resourceId }, error: `resource-not-found: ${a.resourceId}` };
|
|
79
100
|
if (m.lastChangeoverKey !== a.gtin) {
|
|
@@ -81,7 +102,7 @@ export class MesKernel extends FlowEngine {
|
|
|
81
102
|
* 오퍼레이션별 셋업 명세는 작업 생성 경로(emitStation*)가 소비한다. */
|
|
82
103
|
m.setupMs += DEFAULT_SETUP_MS; // 셋업 = OEE 가용성 손실
|
|
83
104
|
m.lastChangeoverKey = a.gtin;
|
|
84
|
-
this.
|
|
105
|
+
this.emitEquipment(m);
|
|
85
106
|
}
|
|
86
107
|
return { commandId: cmd.commandId, accepted: true };
|
|
87
108
|
}
|
|
@@ -91,7 +112,7 @@ export class MesKernel extends FlowEngine {
|
|
|
91
112
|
onArrival(spec) {
|
|
92
113
|
if (this.mesSpec)
|
|
93
114
|
return this.onArrivalDef(spec);
|
|
94
|
-
const rawStore = this.
|
|
115
|
+
const rawStore = this.locationByType('raw-store');
|
|
95
116
|
if (!rawStore)
|
|
96
117
|
return;
|
|
97
118
|
const gtin = this.pickGtin(spec.content.skuMix);
|
|
@@ -110,7 +131,7 @@ export class MesKernel extends FlowEngine {
|
|
|
110
131
|
const product = PRODUCTS[this.orderSeq % PRODUCTS.length];
|
|
111
132
|
const id = `order-${++this.orderSeq}`;
|
|
112
133
|
const wo = gdtiUri(CP, '403', ++this.soSeq);
|
|
113
|
-
const order = { id, kind: 'workorder', status: 'created', gtin: product.gtin, requested: 1, fulfilled: 0, bizTransaction: wo, allocated: [], picked: [] };
|
|
134
|
+
const order = { id, kind: 'workorder', status: 'created', gtin: product.gtin, requested: 1, fulfilled: 0, bizTransaction: wo, allocated: [], picked: [], ...this.promiseOf(_spec) };
|
|
114
135
|
this.orders.set(id, order);
|
|
115
136
|
this.emitOrder(order);
|
|
116
137
|
}
|
|
@@ -119,7 +140,7 @@ export class MesKernel extends FlowEngine {
|
|
|
119
140
|
if (this.mesSpec)
|
|
120
141
|
return this.allocateDef(o);
|
|
121
142
|
const s0 = ROUTE[0];
|
|
122
|
-
const first = this.
|
|
143
|
+
const first = this.locationByType(s0.locationType);
|
|
123
144
|
const product = this.productOf(o.gtin);
|
|
124
145
|
if (!first || !product)
|
|
125
146
|
return;
|
|
@@ -127,7 +148,7 @@ export class MesKernel extends FlowEngine {
|
|
|
127
148
|
const picks = [];
|
|
128
149
|
for (const line of product.bom) {
|
|
129
150
|
const available = [...this.items.values()]
|
|
130
|
-
.filter(i => i.gtin === line.part.gtin && i.disposition === DISP.sellable && this.
|
|
151
|
+
.filter(i => i.gtin === line.part.gtin && i.disposition === DISP.sellable && this.locations.get(i.location)?.type === 'raw-store')
|
|
131
152
|
.map(i => ({ epc: i.epc, location: i.location, qty: 1 }));
|
|
132
153
|
const chosen = this.policy.selectStock({ gtin: line.part.gtin, qty: line.qty, available });
|
|
133
154
|
if (chosen.length < line.qty)
|
|
@@ -144,25 +165,41 @@ export class MesKernel extends FlowEngine {
|
|
|
144
165
|
}
|
|
145
166
|
/** 라우트 스테이션 태스크 발행(공통) — 제자리 가공(process), 이종 자원, 제품 전환 셋업. */
|
|
146
167
|
emitStation(o, stage, itemEpc, changeoverKey) {
|
|
147
|
-
const
|
|
148
|
-
const task = { id: `task-${++this.taskSeq}`, kind: stage.kind, status: 'created', itemEpc, fromNode:
|
|
168
|
+
const loc = this.locationByType(stage.locationType);
|
|
169
|
+
const task = { id: `task-${++this.taskSeq}`, kind: stage.kind, status: 'created', itemEpc, fromNode: loc.id, toNode: loc.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: stage.kind, fromNode: loc.id, toNode: loc.id, resourceKind: stage.resource }, DEFAULT_CYCLE_MS), orderId: o.id, resourceType: stage.resource, changeoverKey, setupMs: this.paramDuration(stage.kind, OP_PARAM.setupDuration) ?? DEFAULT_SETUP_MS, intent: 'process' };
|
|
149
170
|
this.tasks.set(task.id, task);
|
|
150
171
|
this.emitTask(task);
|
|
151
172
|
}
|
|
152
173
|
/** op 완료 = 변환. 라우트 인덱스로 분기: 첫=BOM 소비→WIP, 중간=WIP→WIP, 마지막(조립)=WIP→완성차(수율→OEE 품질). */
|
|
174
|
+
/**
|
|
175
|
+
* MES 는 완료 시점에 **오더와 제품 정의**를 딛고 선다 — 둘 중 하나만 없어도 끝맺을 수 없다.
|
|
176
|
+
*
|
|
177
|
+
* 씨앗이 이미 이행된 오더를 심지 않으므로(남은 수량 0) 그 오더에 딸린 작업만 남을 수 있고,
|
|
178
|
+
* 관측이 오더 연결을 담지 못한 작업도 있다. 그때 예전에는 `order.gtin` 에서 던졌고 **그 예외
|
|
179
|
+
* 하나가 정확도 추세 전체를 죽였다.** 이제 코어가 이 답을 보고 그 작업만 접는다.
|
|
180
|
+
*/
|
|
181
|
+
canComplete(t) {
|
|
182
|
+
if (this.mesSpec)
|
|
183
|
+
return !!(t.orderId && this.orders.get(t.orderId));
|
|
184
|
+
const order = t.orderId ? this.orders.get(t.orderId) : undefined;
|
|
185
|
+
return !!order && !!this.productOf(order.gtin);
|
|
186
|
+
}
|
|
153
187
|
onTaskComplete(t) {
|
|
154
188
|
if (this.mesSpec)
|
|
155
189
|
return this.onTaskCompleteDef(t);
|
|
190
|
+
/* 여기 도달했다면 `canComplete` 가 이미 확인했다 — 그래도 단정(`!`)은 쓰지 않는다. */
|
|
156
191
|
const order = this.orders.get(t.orderId);
|
|
157
|
-
const product = this.productOf(order.gtin);
|
|
192
|
+
const product = order && this.productOf(order.gtin);
|
|
193
|
+
if (!order || !product)
|
|
194
|
+
throw new Error(`task ${t.id}: order/product vanished between the core check and the domain hook`);
|
|
158
195
|
const i = ROUTE.findIndex(s => s.kind === t.kind);
|
|
159
|
-
const
|
|
196
|
+
const loc = this.locations.get(t.toNode);
|
|
160
197
|
const isLast = i === ROUTE.length - 1;
|
|
161
198
|
if (!isLast) {
|
|
162
199
|
// 중간 스테이션 — 입력(첫=BOM 전부 / 이후=이전 WIP) → 다음 WIP, 다음 스테이션 태스크.
|
|
163
200
|
const inputs = order.allocated.slice();
|
|
164
201
|
const wip = sgtinUri(CP, WIP_ITEMREF, ++this.wipSeq);
|
|
165
|
-
this.transform(inputs, [{ epc: wip, gtin: WIP_GTIN, qty: 1, location:
|
|
202
|
+
this.transform(inputs, [{ epc: wip, gtin: WIP_GTIN, qty: 1, location: loc.id, disposition: DISP.in_progress }], { bizStep: MES_BIZSTEP.producing, disposition: DISP.in_progress, transformationId: order.bizTransaction, readPoint: loc.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
|
|
166
203
|
order.allocated = [wip];
|
|
167
204
|
const next = ROUTE[i + 1];
|
|
168
205
|
this.emitStation(order, next, wip, product.gtin);
|
|
@@ -171,13 +208,13 @@ export class MesKernel extends FlowEngine {
|
|
|
171
208
|
return;
|
|
172
209
|
}
|
|
173
210
|
// 마지막 스테이션(조립) → 완성차 (transform 1→1, disposition 으로 수율 loss → OEE 품질)
|
|
174
|
-
const fgStore = this.
|
|
211
|
+
const fgStore = this.locationByType('fg-store');
|
|
175
212
|
const wip = order.allocated[0];
|
|
176
213
|
const good = this.rng() < (this.paramNumber(t.kind, OP_PARAM.yield) ?? DEFAULT_YIELD);
|
|
177
214
|
this.recordOutput(t.resource, good); // OEE 품질(마지막 자원별 양품/불량)
|
|
178
215
|
const disp = good ? DISP.sellable : DISP.non_sellable;
|
|
179
216
|
const outputEpc = sgtinUri(CP, product.ref, ++this.prodSeq);
|
|
180
|
-
this.transform([wip], [{ epc: outputEpc, gtin: product.gtin, qty: 1, location: fgStore.id, disposition: disp }], { bizStep: MES_BIZSTEP.producing, disposition: disp, transformationId: order.bizTransaction, readPoint:
|
|
217
|
+
this.transform([wip], [{ epc: outputEpc, gtin: product.gtin, qty: 1, location: fgStore.id, disposition: disp }], { bizStep: MES_BIZSTEP.producing, disposition: disp, transformationId: order.bizTransaction, readPoint: loc.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
|
|
181
218
|
this.emit(objectEvent({ eventTime: this.now(), action: 'OBSERVE', bizStep: MES_BIZSTEP.storing, disposition: disp, epcList: [outputEpc], quantityList: [{ epcClass: product.gtin, quantity: 1 }], readPoint: fgStore.id, bizLocation: fgStore.id }));
|
|
182
219
|
order.allocated = [];
|
|
183
220
|
order.fulfilled = 1;
|
|
@@ -196,6 +233,18 @@ export class MesKernel extends FlowEngine {
|
|
|
196
233
|
serialOf(materialKey, serial) {
|
|
197
234
|
return sgtinUri(this.mesSpec.companyPrefix, this.mesSpec.binding[materialKey], serial);
|
|
198
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* 라우트를 용량 계산에 알려 준다 — 수율을 거슬러 올릴 때 순서가 곧 계산이다.
|
|
238
|
+
*
|
|
239
|
+
* 순서를 모르면 선언 순서대로 세는데, 그러면 하류에서 잃는 몫이 엉뚱한 공정에 얹힌다(조용히
|
|
240
|
+
* 틀린다). 생산 정의를 가진 커널만 이 답을 안다.
|
|
241
|
+
*/
|
|
242
|
+
routeKeys() {
|
|
243
|
+
if (!this.mesSpec)
|
|
244
|
+
return undefined;
|
|
245
|
+
const d = this.mesSpec.definition;
|
|
246
|
+
return d.routes?.find(r => r.key === this.recipeDef().route)?.steps;
|
|
247
|
+
}
|
|
199
248
|
/** recipe.route → 오퍼레이션 시퀀스 해소. */
|
|
200
249
|
routeOps() {
|
|
201
250
|
const d = this.mesSpec.definition;
|
|
@@ -204,10 +253,13 @@ export class MesKernel extends FlowEngine {
|
|
|
204
253
|
}
|
|
205
254
|
/** 정의 모드 수령 — skuMix gtin 이 레시피 입력 자재면 raw-store 에 생성. */
|
|
206
255
|
onArrivalDef(spec) {
|
|
207
|
-
const rawStore = this.
|
|
256
|
+
const rawStore = this.locationByType('raw-store');
|
|
208
257
|
if (!rawStore)
|
|
209
258
|
return;
|
|
210
259
|
const gtin = this.pickGtin(spec.content.skuMix);
|
|
260
|
+
/* 품목 구성이 비어 있으면 도착을 만들지 않는다 — 무엇이 왔는지 말할 수 없는 입고는 사실이 아니다. */
|
|
261
|
+
if (!gtin)
|
|
262
|
+
return;
|
|
211
263
|
const inputKey = this.recipeDef().inputs.map(i => i.material).find(k => this.classOf(k) === gtin);
|
|
212
264
|
if (!inputKey)
|
|
213
265
|
return;
|
|
@@ -221,21 +273,21 @@ export class MesKernel extends FlowEngine {
|
|
|
221
273
|
const rc = this.recipeDef();
|
|
222
274
|
const id = `order-${++this.orderSeq}`;
|
|
223
275
|
const wo = gdtiUri(this.mesSpec.companyPrefix, '403', ++this.soSeq);
|
|
224
|
-
const order = { id, kind: 'workorder', status: 'created', gtin: this.classOf(rc.outputs[0].material), requested: 1, fulfilled: 0, bizTransaction: wo, allocated: [], picked: [] };
|
|
276
|
+
const order = { id, kind: 'workorder', status: 'created', gtin: this.classOf(rc.outputs[0].material), requested: 1, fulfilled: 0, bizTransaction: wo, allocated: [], picked: [], ...this.promiseOf(_spec) };
|
|
225
277
|
this.orders.set(id, order);
|
|
226
278
|
this.emitOrder(order);
|
|
227
279
|
}
|
|
228
280
|
/** 정의 모드 할당 — 레시피 입력 BOM 전량 확보 후 첫 라우트 스텝 태스크. */
|
|
229
281
|
allocateDef(o) {
|
|
230
282
|
const ops = this.routeOps();
|
|
231
|
-
if (!ops.length || !ops[0].
|
|
283
|
+
if (!ops.length || !ops[0].locationType || !this.locationByType(ops[0].locationType))
|
|
232
284
|
return;
|
|
233
285
|
const rc = this.recipeDef();
|
|
234
286
|
const picks = [];
|
|
235
287
|
for (const line of rc.inputs) {
|
|
236
288
|
const g = this.classOf(line.material);
|
|
237
289
|
const available = [...this.items.values()]
|
|
238
|
-
.filter(i => i.gtin === g && i.disposition === DISP.sellable && this.
|
|
290
|
+
.filter(i => i.gtin === g && i.disposition === DISP.sellable && this.locations.get(i.location)?.type === 'raw-store')
|
|
239
291
|
.map(i => ({ epc: i.epc, location: i.location, qty: 1 }));
|
|
240
292
|
const chosen = this.policy.selectStock({ gtin: g, qty: line.qty, available });
|
|
241
293
|
if (chosen.length < line.qty)
|
|
@@ -251,25 +303,27 @@ export class MesKernel extends FlowEngine {
|
|
|
251
303
|
this.emitOrder(o);
|
|
252
304
|
}
|
|
253
305
|
emitStationDef(o, op, itemEpc) {
|
|
254
|
-
const
|
|
255
|
-
const task = { id: `task-${++this.taskSeq}`, kind: op.key, status: 'created', itemEpc, fromNode:
|
|
306
|
+
const loc = this.locationByType(op.locationType);
|
|
307
|
+
const task = { id: `task-${++this.taskSeq}`, kind: op.key, status: 'created', itemEpc, fromNode: loc.id, toNode: loc.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: op.key, fromNode: loc.id, toNode: loc.id, resourceKind: op.resourceType }, DEFAULT_CYCLE_MS), orderId: o.id, resourceType: op.resourceType, changeoverKey: o.gtin, setupMs: this.paramDuration(op.key, OP_PARAM.setupDuration) ?? DEFAULT_SETUP_MS, intent: op.intent };
|
|
256
308
|
this.tasks.set(task.id, task);
|
|
257
309
|
this.emitTask(task);
|
|
258
310
|
}
|
|
259
311
|
/** 정의 모드 완료 — 라우트 인덱스: 중간=WIP 변환+다음 스텝, 마지막=완제품(수율). */
|
|
260
312
|
onTaskCompleteDef(t) {
|
|
261
313
|
const order = this.orders.get(t.orderId);
|
|
314
|
+
if (!order)
|
|
315
|
+
throw new Error(`task ${t.id}: order vanished between the core check and the domain hook`);
|
|
262
316
|
const rc = this.recipeDef();
|
|
263
317
|
const ops = this.routeOps();
|
|
264
318
|
const i = ops.findIndex(s => s.key === t.kind);
|
|
265
|
-
const
|
|
319
|
+
const loc = this.locations.get(t.toNode);
|
|
266
320
|
const bizStep = ops[i]?.bizStep ?? MES_BIZSTEP.producing;
|
|
267
321
|
const isLast = i === ops.length - 1;
|
|
268
322
|
if (!isLast) {
|
|
269
323
|
const inputs = order.allocated.slice();
|
|
270
324
|
const wip = sgtinUri(this.mesSpec.companyPrefix, 'WIP', ++this.wipSeq);
|
|
271
325
|
const wipGtin = sgtinClass(this.mesSpec.companyPrefix, 'WIP');
|
|
272
|
-
this.transform(inputs, [{ epc: wip, gtin: wipGtin, qty: 1, location:
|
|
326
|
+
this.transform(inputs, [{ epc: wip, gtin: wipGtin, qty: 1, location: loc.id, disposition: DISP.in_progress }], { bizStep, disposition: DISP.in_progress, transformationId: order.bizTransaction, readPoint: loc.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
|
|
273
327
|
order.allocated = [wip];
|
|
274
328
|
const next = ops[i + 1];
|
|
275
329
|
this.emitStationDef(order, next, wip);
|
|
@@ -277,14 +331,14 @@ export class MesKernel extends FlowEngine {
|
|
|
277
331
|
this.emitOrder(order);
|
|
278
332
|
return;
|
|
279
333
|
}
|
|
280
|
-
const fgStore = this.
|
|
334
|
+
const fgStore = this.locationByType('fg-store');
|
|
281
335
|
const wip = order.allocated[0];
|
|
282
336
|
const good = this.rng() < (this.paramNumber(t.kind, OP_PARAM.yield) ?? DEFAULT_YIELD);
|
|
283
337
|
this.recordOutput(t.resource, good);
|
|
284
338
|
const disp = good ? DISP.sellable : DISP.non_sellable;
|
|
285
339
|
const outEpc = this.serialOf(rc.outputs[0].material, ++this.prodSeq);
|
|
286
340
|
const outGtin = this.classOf(rc.outputs[0].material);
|
|
287
|
-
this.transform([wip], [{ epc: outEpc, gtin: outGtin, qty: 1, location: fgStore.id, disposition: disp }], { bizStep, disposition: disp, transformationId: order.bizTransaction, readPoint:
|
|
341
|
+
this.transform([wip], [{ epc: outEpc, gtin: outGtin, qty: 1, location: fgStore.id, disposition: disp }], { bizStep, disposition: disp, transformationId: order.bizTransaction, readPoint: loc.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
|
|
288
342
|
this.emit(objectEvent({ eventTime: this.now(), action: 'OBSERVE', bizStep: MES_BIZSTEP.storing, disposition: disp, epcList: [outEpc], quantityList: [{ epcClass: outGtin, quantity: 1 }], readPoint: fgStore.id, bizLocation: fgStore.id }));
|
|
289
343
|
order.allocated = [];
|
|
290
344
|
order.fulfilled = 1;
|
package/dist/mes-profile.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export declare const MES_BIZSTEP: {
|
|
|
8
8
|
export declare const BTT_PRODORDER = "urn:epcglobal:cbv:btt:prodorder";
|
|
9
9
|
/** 직렬 SGTIN URI (원자재 단위·완제품). */
|
|
10
10
|
export declare function sgtinUri(companyPrefix: string, itemRef: string, serial: number): string;
|
|
11
|
-
/** MES
|
|
11
|
+
/** MES 자리 타입 카탈로그 — 커널이 키로 쓰는 제조 로케이션 타입(locationByType/.type). 도메인 SSOT.
|
|
12
12
|
* 트레일러 제조 라인: 자재→프레임 절단→용접→도장→조립→완성차(kernel ROUTE 와 일치). */
|
|
13
|
-
export declare const
|
|
13
|
+
export declare const MES_LOCATION_TYPES: readonly ["raw-store", "cut-station", "weld-station", "paint-booth", "assembly-line", "fg-store"];
|
|
14
14
|
export declare const MES_TYPES: TwinTypeInfo[];
|