@operato/twin-kernel 0.7.42 → 0.7.43
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/allocation-policy.d.ts +12 -1
- package/dist/allocation-policy.js +56 -12
- package/dist/contract.d.ts +12 -0
- package/dist/face2-adapter.d.ts +3 -2
- package/dist/face2-adapter.js +17 -7
- package/dist/flow-engine.d.ts +14 -0
- package/dist/flow-engine.js +7 -0
- package/dist/mes-kernel.d.ts +15 -1
- package/dist/mes-kernel.js +66 -2
- package/dist-cjs/index.cjs +95 -22
- package/package.json +1 -1
|
@@ -23,7 +23,18 @@ export interface PlacementContext {
|
|
|
23
23
|
export interface StockRequest {
|
|
24
24
|
gtin: string;
|
|
25
25
|
qty: number;
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* 후보 재고 — **한 번만 순회할 수 있는 것으로 본다.**
|
|
28
|
+
*
|
|
29
|
+
* ── 왜 배열이 아닌가 (2026-08-21 프로파일) ────────────────────────────────
|
|
30
|
+
* 예전에는 배열이었다. 그래서 호출부가 자리의 물품 전체를 배열로 만들어 넘겼고, 그것이 틱 시간의
|
|
31
|
+
* 25.1% 였다(물품 16,000 규모). 오더 하나가 요구하는 수는 보통 한 자리인데 후보 전체를 실물로
|
|
32
|
+
* 만들어 낸 것이다.
|
|
33
|
+
*
|
|
34
|
+
* 지연 순회로 바꾸면 기본 정책은 만들지 않고 고를 수 있다. 전체가 필요한 정책(FEFO 는 만료 순으로
|
|
35
|
+
* 정렬해야 한다)은 `[...available]` 로 펼치면 된다 — 그 비용은 그 정책이 실제로 필요해서 치르는 것이다.
|
|
36
|
+
*/
|
|
37
|
+
available: Iterable<StockView>;
|
|
27
38
|
}
|
|
28
39
|
export interface AllocationPolicy {
|
|
29
40
|
/** 배치 목적지 슬롯 id. 수용 불가면 null(도크 대기 → 다음 tick 재시도). */
|
|
@@ -5,15 +5,55 @@
|
|
|
5
5
|
* 결정은 정책에 위임한다. 고객은 FIFO/FEFO/nearest/zone·부분할당 등을 정책 교체로만
|
|
6
6
|
* 확장 — 코어 수정 없이. (설계 gap #5 "할당 정책 플러그")
|
|
7
7
|
*/
|
|
8
|
+
/*
|
|
9
|
+
* ── 식별자 비교에 `localeCompare` 를 쓰지 않는다 (2026-08-21 프로파일) ───────
|
|
10
|
+
*
|
|
11
|
+
* 정렬 비용이 틱 시간의 17.2% 였다(`sortByEpc` 와 그 비교 함수 합). 원인이 둘이다.
|
|
12
|
+
*
|
|
13
|
+
* ① `localeCompare` 는 **로케일 대조**다. 사람이 읽는 목록을 그 언어의 관행대로 늘어놓기 위한 것이고,
|
|
14
|
+
* 그래서 비교 한 번이 문자열 비교보다 훨씬 비싸다. 그런데 여기서 비교하는 것은 **식별자**다 —
|
|
15
|
+
* 사람에게 보이지 않고, 필요한 것은 「언제 물어도 같은 순서」뿐이다. 부호 순서로 충분하다.
|
|
16
|
+
*
|
|
17
|
+
* ② 전량을 정렬한 뒤 앞의 `qty` 개만 취했다. 오더 하나가 요구하는 수는 보통 한 자리인데 재고는
|
|
18
|
+
* 수만 개가 될 수 있다 — 목표 규모는 물품 10만이다. **가장 작은 k 개**만 찾으면 된다.
|
|
19
|
+
*
|
|
20
|
+
* 결과는 바뀌지 않는다: 같은 순서에서 가장 작은 k 개는 정렬한 뒤 앞의 k 개와 같은 것들이고 순서도 같다.
|
|
21
|
+
*/
|
|
22
|
+
const byId = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
8
23
|
function freeBinsFirstFit(slots) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
24
|
+
/* 정렬하지 않는다 — 가장 작은 id 하나만 찾으면 되므로 한 번 순회로 답한다. */
|
|
25
|
+
let best;
|
|
26
|
+
for (const b of slots) {
|
|
27
|
+
if (b.occupancy + b.reserved >= b.capacity)
|
|
28
|
+
continue;
|
|
29
|
+
if (!best || byId(b.id, best.id) < 0)
|
|
30
|
+
best = b;
|
|
31
|
+
}
|
|
32
|
+
return best?.id ?? null;
|
|
14
33
|
}
|
|
15
|
-
|
|
16
|
-
|
|
34
|
+
/**
|
|
35
|
+
* `epc` 가 가장 작은 **k 개** — 전량 정렬 없이.
|
|
36
|
+
*
|
|
37
|
+
* 작은 정렬 버퍼를 들고 한 번 순회한다. k 가 작을 때(오더가 요구하는 수는 보통 한 자리다) 전량 정렬보다
|
|
38
|
+
* 훨씬 싸고, 결과는 정렬한 뒤 앞의 k 개와 **같다**.
|
|
39
|
+
*/
|
|
40
|
+
function smallestByEpc(available, k) {
|
|
41
|
+
if (k <= 0)
|
|
42
|
+
return { picked: [], total: 0 };
|
|
43
|
+
const out = [];
|
|
44
|
+
let total = 0;
|
|
45
|
+
for (const s of available) {
|
|
46
|
+
total++;
|
|
47
|
+
if (out.length === k && byId(s.epc, out[out.length - 1].epc) >= 0)
|
|
48
|
+
continue;
|
|
49
|
+
let i = out.length;
|
|
50
|
+
while (i > 0 && byId(out[i - 1].epc, s.epc) > 0)
|
|
51
|
+
i--;
|
|
52
|
+
out.splice(i, 0, s);
|
|
53
|
+
if (out.length > k)
|
|
54
|
+
out.pop();
|
|
55
|
+
}
|
|
56
|
+
return { picked: out, total };
|
|
17
57
|
}
|
|
18
58
|
/**
|
|
19
59
|
* 기본 정책 — first-fit slot + all-or-nothing 오더 할당.
|
|
@@ -22,9 +62,11 @@ function sortByEpc(available) {
|
|
|
22
62
|
export const firstFitPolicy = {
|
|
23
63
|
selectPlacement: ({ slots }) => freeBinsFirstFit(slots),
|
|
24
64
|
selectStock: ({ qty, available }) => {
|
|
25
|
-
|
|
65
|
+
/* 고르는 것과 세는 것을 **한 번의 순회**로 한다 — 전량 확보 판정에 개수가 필요하다. */
|
|
66
|
+
const { picked, total } = smallestByEpc(available, qty);
|
|
67
|
+
if (total < qty)
|
|
26
68
|
return []; // 전량 확보 전엔 대기
|
|
27
|
-
return
|
|
69
|
+
return picked.map(s => s.epc);
|
|
28
70
|
}
|
|
29
71
|
};
|
|
30
72
|
/**
|
|
@@ -33,7 +75,7 @@ export const firstFitPolicy = {
|
|
|
33
75
|
*/
|
|
34
76
|
export const partialFitPolicy = {
|
|
35
77
|
selectPlacement: ({ slots }) => freeBinsFirstFit(slots),
|
|
36
|
-
selectStock: ({ qty, available }) =>
|
|
78
|
+
selectStock: ({ qty, available }) => smallestByEpc(available, qty).picked.map(s => s.epc)
|
|
37
79
|
};
|
|
38
80
|
/**
|
|
39
81
|
* FEFO 정책 — First-Expired-First-Out. 만료 임박 로트를 먼저 출고(신선/제약 도메인).
|
|
@@ -42,9 +84,11 @@ export const partialFitPolicy = {
|
|
|
42
84
|
export const fefoPolicy = {
|
|
43
85
|
selectPlacement: ({ slots }) => freeBinsFirstFit(slots),
|
|
44
86
|
selectStock: ({ qty, available }) => {
|
|
45
|
-
|
|
87
|
+
/* 만료 순이 필요하므로 전체를 펼친다 — 이 정책이 실제로 필요해서 치르는 비용이다. */
|
|
88
|
+
const all = [...available];
|
|
89
|
+
if (all.length < qty)
|
|
46
90
|
return []; // 전량 확보 전엔 대기
|
|
47
|
-
const byExpiry =
|
|
91
|
+
const byExpiry = all.sort((a, b) => (a.expiry ?? Infinity) - (b.expiry ?? Infinity) || byId(a.epc, b.epc));
|
|
48
92
|
return byExpiry.slice(0, qty).map(s => s.epc);
|
|
49
93
|
}
|
|
50
94
|
};
|
package/dist/contract.d.ts
CHANGED
|
@@ -1163,6 +1163,18 @@ export interface OrderState {
|
|
|
1163
1163
|
recipeKey?: string;
|
|
1164
1164
|
/** 씨앗이 이 오더의 확보분을 다 심지 못했다 — 이 오더의 답은 부족한 씨앗 위에 있다. */
|
|
1165
1165
|
seedIncomplete?: boolean;
|
|
1166
|
+
/**
|
|
1167
|
+
* **이 오더가 왜 대기하는가** — 모자란 투입 줄(자재 키 · 필요 · 확보 가능).
|
|
1168
|
+
*
|
|
1169
|
+
* 확보는 전량 아니면 대기다. 그 거동은 옳은데 이유를 말하지 않으면 화면은 「running」만 보여 주고,
|
|
1170
|
+
* 사람은 영구히 대기하는 오더를 정상으로 읽는다. 모자란 줄을 **전부** 싣는다 — 하나만 알려 주면
|
|
1171
|
+
* 채운 뒤 다음 줄에서 또 막히고 같은 진단을 반복하게 된다.
|
|
1172
|
+
*/
|
|
1173
|
+
shortage?: {
|
|
1174
|
+
material: string;
|
|
1175
|
+
need: number;
|
|
1176
|
+
have: number;
|
|
1177
|
+
}[];
|
|
1166
1178
|
progress?: number;
|
|
1167
1179
|
held?: boolean;
|
|
1168
1180
|
/**
|
package/dist/face2-adapter.d.ts
CHANGED
|
@@ -103,8 +103,9 @@ export declare function isAggregationRecord(record: unknown): boolean;
|
|
|
103
103
|
* 변환은 **들어간 것과 나온 것**으로 말한다. 개체 하나의 관측(`epc`)이나 담김(`parentID`)과 섞이지
|
|
104
104
|
* 않게, 그 둘이 없고 입력·출력 중 하나라도 있으면 변환으로 본다.
|
|
105
105
|
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
106
|
+
* 한쪽이라도 있으면 **변환하려는 레코드로 본다.** 양쪽이 다 있어야 유효한 이벤트가 되지만(아래),
|
|
107
|
+
* 판정은 「무엇을 말하려는 레코드인가」이므로 한쪽만 있어도 이 갈래로 보내야 한다 — 그러지 않으면
|
|
108
|
+
* 반쪽만 온 레코드가 개체 관측으로 잘못 흘러가고, 무엇이 빠졌는지 아무도 말해 주지 않는다.
|
|
108
109
|
*/
|
|
109
110
|
export declare function isTransformationRecord(record: unknown): boolean;
|
|
110
111
|
/** 룰: 소스 레코드의 판별자(sourceType)로 매핑을 고른다. */
|
package/dist/face2-adapter.js
CHANGED
|
@@ -27,8 +27,9 @@ export function isAggregationRecord(record) {
|
|
|
27
27
|
* 변환은 **들어간 것과 나온 것**으로 말한다. 개체 하나의 관측(`epc`)이나 담김(`parentID`)과 섞이지
|
|
28
28
|
* 않게, 그 둘이 없고 입력·출력 중 하나라도 있으면 변환으로 본다.
|
|
29
29
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
30
|
+
* 한쪽이라도 있으면 **변환하려는 레코드로 본다.** 양쪽이 다 있어야 유효한 이벤트가 되지만(아래),
|
|
31
|
+
* 판정은 「무엇을 말하려는 레코드인가」이므로 한쪽만 있어도 이 갈래로 보내야 한다 — 그러지 않으면
|
|
32
|
+
* 반쪽만 온 레코드가 개체 관측으로 잘못 흘러가고, 무엇이 빠졌는지 아무도 말해 주지 않는다.
|
|
32
33
|
*/
|
|
33
34
|
export function isTransformationRecord(record) {
|
|
34
35
|
if (!record || typeof record !== 'object')
|
|
@@ -111,16 +112,25 @@ export function mapRecordChecked(record, mapping, eventTime) {
|
|
|
111
112
|
const outputEPCList = resolveList(mapping.outputEPCList, record, 'outputEPCList', errors);
|
|
112
113
|
const outputQuantityList = resolveQuantityList(mapping.outputQuantityList, record, 'outputQuantityList', errors);
|
|
113
114
|
const transformationID = resolve(mapping.transformationID, record, 'transformationID', errors);
|
|
114
|
-
/*
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
/*
|
|
116
|
+
* **양쪽이 다 있어야 한다** — EPCIS 2.0 §7.4.5 의 정의다: 「objects … are fully or partially
|
|
117
|
+
* consumed as inputs **and** one or more objects … are produced as outputs」. 네 목록 필드가
|
|
118
|
+
* 각각 선택인 것은 개체로 말하든 수량으로 말하든 되기 때문이고, 한쪽을 빼도 된다는 뜻이 아니다.
|
|
119
|
+
*
|
|
120
|
+
* 검증기도 같은 것을 본다. 여기서 함께 말하는 이유는 **가리키는 곳이 다르기** 때문이다 — 검증
|
|
121
|
+
* 메시지는 이벤트를 가리키고 이 메시지는 매핑을 가리킨다. 커넥터가 고칠 자리가 다르다.
|
|
122
|
+
*/
|
|
123
|
+
if (!inputEPCList.length && !inputQuantityList.length) {
|
|
124
|
+
errors.push('입력이 비었다 — 변환은 무엇이 들어갔는지 말해야 한다(EPCIS 2.0 §7.4.5)');
|
|
125
|
+
}
|
|
126
|
+
if (!outputEPCList.length && !outputQuantityList.length) {
|
|
127
|
+
errors.push('출력이 비었다 — 변환은 무엇이 나왔는지 말해야 한다(EPCIS 2.0 §7.4.5)');
|
|
118
128
|
}
|
|
119
129
|
return {
|
|
120
130
|
event: transformationEvent({
|
|
121
131
|
eventTime, bizStep, disposition,
|
|
122
132
|
/* 없는 쪽은 **필드를 만들지 않는다** — 빈 배열을 실으면 「없다」고 말하는 것이 된다.
|
|
123
|
-
|
|
133
|
+
개체로 말한 쪽과 수량으로 말한 쪽 중 하나만 쓰는 것이 정상이다. */
|
|
124
134
|
...(inputEPCList.length ? { inputEPCList } : {}),
|
|
125
135
|
...(inputQuantityList.length ? { inputQuantityList } : {}),
|
|
126
136
|
...(outputEPCList.length ? { outputEPCList } : {}),
|
package/dist/flow-engine.d.ts
CHANGED
|
@@ -233,6 +233,20 @@ export interface FlowOrder {
|
|
|
233
233
|
* 말한다.** 이 표시가 없으면 그 상황과 「우리 계산의 결함」을 구별할 수 없다.
|
|
234
234
|
*/
|
|
235
235
|
seedIncomplete?: boolean;
|
|
236
|
+
/**
|
|
237
|
+
* **이 오더가 왜 대기하는가** — 마지막 확보 시도에서 모자랐던 투입 줄들.
|
|
238
|
+
*
|
|
239
|
+
* 확보는 전량 아니면 대기다(부분 투입을 하면 재고가 거짓이 된다). 그 거동은 옳은데 **조용했다**:
|
|
240
|
+
* 실측에서 투입 18줄 중 6줄이 입고되지 않아 오더 다섯이 영구히 대기했고 화면에는 「running」이라
|
|
241
|
+
* 적혔다. 어느 자리도 이유를 말하지 않았다.
|
|
242
|
+
*
|
|
243
|
+
* 매 틱 새로 쓰인다(확보를 다시 시도하므로). 채워지면 지운다 — 남겨 두면 해결된 것을 계속 말한다.
|
|
244
|
+
*/
|
|
245
|
+
shortage?: {
|
|
246
|
+
material: string;
|
|
247
|
+
need: number;
|
|
248
|
+
have: number;
|
|
249
|
+
}[];
|
|
236
250
|
/** 우선순위 — 표준 `OperationsRequest.Priority`(작은 값이 급하다). 할당 순서를 정한다. */
|
|
237
251
|
priority?: number;
|
|
238
252
|
/** 예정 창 — 표준 `StartTime`/`EndTime`. 납기가 있어야 "늦었나" 를 물을 수 있다. */
|
package/dist/flow-engine.js
CHANGED
|
@@ -373,6 +373,11 @@ export class ItemStore {
|
|
|
373
373
|
out.set(k, structuredClone(it));
|
|
374
374
|
return out;
|
|
375
375
|
}
|
|
376
|
+
/*
|
|
377
|
+
* 지연 순회(`*iterAt`)를 만들어 배열을 없애 보았으나 **더 느렸다** — 물품 16,000 규모에서 틱 합계가
|
|
378
|
+
* 1562ms 에서 2723ms 로 늘었다. 항목마다 생성기 규약을 지나는 비용이 배열 하나를 만드는 비용보다
|
|
379
|
+
* 크다. 측정이 그렇게 답했으므로 배열을 유지한다.
|
|
380
|
+
*/
|
|
376
381
|
/** 그 자리에 있는 물품들 — 색인이 답한다(전체 순회가 아니다). */
|
|
377
382
|
at(location) {
|
|
378
383
|
const keys = this.byLocation.get(location);
|
|
@@ -1325,6 +1330,8 @@ export class FlowEngine {
|
|
|
1325
1330
|
...(o.recipeKey ? { recipeKey: o.recipeKey } : {}),
|
|
1326
1331
|
/* 씨앗이 다 심지 못했다는 사실 — 조용히 자르지 않는다(그 오더의 답은 부족한 씨앗 위에 있다). */
|
|
1327
1332
|
...(o.seedIncomplete ? { seedIncomplete: true } : {}),
|
|
1333
|
+
/* 왜 대기하는지 — 없으면 화면은 「running」만 보여 주고 사람은 원인을 찾을 자리가 없다. */
|
|
1334
|
+
...(o.shortage?.length ? { shortage: o.shortage.map(x => ({ ...x })) } : {}),
|
|
1328
1335
|
...(o.lines?.length ? { lines: o.lines.map(l => ({ gtin: l.gtin, requested: l.requested })) } : {}),
|
|
1329
1336
|
...(o.priority !== undefined ? { priority: o.priority } : {}),
|
|
1330
1337
|
...(o.startTime ? { startTime: o.startTime } : {}),
|
package/dist/mes-kernel.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GeneratorSpec, Command, CommandAck, ProductionSpec, TwinModelDef, IdentityGroundingView } from './contract.ts';
|
|
1
|
+
import type { GeneratorSpec, Command, CommandAck, ProductionSpec, TwinModelDef, IdentityGroundingView, Attention } 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';
|
|
@@ -60,6 +60,20 @@ export declare class MesKernel extends FlowEngine {
|
|
|
60
60
|
* 이미 그 제품이면 no-op, 아니면 셋업(기본값, OEE 가용성 손실) + lastChangeoverKey 각인
|
|
61
61
|
* (이후 그 제품 task 는 자동 셋업 생략). command → 변이 → State 델타(폐루프).
|
|
62
62
|
*/
|
|
63
|
+
/**
|
|
64
|
+
* **왜 대기하는지 화면에 말한다** — 자재가 모자라 확보가 미뤄진 오더.
|
|
65
|
+
*
|
|
66
|
+
* ── 왜 필요한가 (2026-08-21 실측) ─────────────────────────────────────────
|
|
67
|
+
* 확보는 전량 아니면 대기다(부분 투입을 하면 재고가 거짓이 된다). 그 거동은 옳다. 그런데 조용해서,
|
|
68
|
+
* 투입 18줄 중 6줄이 한 번도 입고되지 않은 공장에서 **오더 다섯이 영구히 대기하는 동안 화면에는
|
|
69
|
+
* 「running」이라 적혔다.** 아무 자리도 이유를 말하지 않았다.
|
|
70
|
+
*
|
|
71
|
+
* 판단만 여기서 한다 — 확인(ack)·처음 성립한 시각·사라진 조건 정리는 `computeAttentions` 가
|
|
72
|
+
* 한 곳에서 맡는다(확장점 규약).
|
|
73
|
+
*
|
|
74
|
+
* 사람이 읽는 문장은 만들지 않는다. `kind` 와 언어 중립 `params` 만 내고 문장은 표현계층이 만든다.
|
|
75
|
+
*/
|
|
76
|
+
protected collectAttentions(): Attention[];
|
|
63
77
|
protected handleCommand(cmd: Command): CommandAck;
|
|
64
78
|
/** 부품 수령 — 도착한 품목이 선언된 입력 자재면 그 자재가 선언한 자리에 생성. */
|
|
65
79
|
protected onArrival(spec: GeneratorSpec): void;
|
package/dist/mes-kernel.js
CHANGED
|
@@ -149,6 +149,43 @@ export class MesKernel extends FlowEngine {
|
|
|
149
149
|
* 이미 그 제품이면 no-op, 아니면 셋업(기본값, OEE 가용성 손실) + lastChangeoverKey 각인
|
|
150
150
|
* (이후 그 제품 task 는 자동 셋업 생략). command → 변이 → State 델타(폐루프).
|
|
151
151
|
*/
|
|
152
|
+
/**
|
|
153
|
+
* **왜 대기하는지 화면에 말한다** — 자재가 모자라 확보가 미뤄진 오더.
|
|
154
|
+
*
|
|
155
|
+
* ── 왜 필요한가 (2026-08-21 실측) ─────────────────────────────────────────
|
|
156
|
+
* 확보는 전량 아니면 대기다(부분 투입을 하면 재고가 거짓이 된다). 그 거동은 옳다. 그런데 조용해서,
|
|
157
|
+
* 투입 18줄 중 6줄이 한 번도 입고되지 않은 공장에서 **오더 다섯이 영구히 대기하는 동안 화면에는
|
|
158
|
+
* 「running」이라 적혔다.** 아무 자리도 이유를 말하지 않았다.
|
|
159
|
+
*
|
|
160
|
+
* 판단만 여기서 한다 — 확인(ack)·처음 성립한 시각·사라진 조건 정리는 `computeAttentions` 가
|
|
161
|
+
* 한 곳에서 맡는다(확장점 규약).
|
|
162
|
+
*
|
|
163
|
+
* 사람이 읽는 문장은 만들지 않는다. `kind` 와 언어 중립 `params` 만 내고 문장은 표현계층이 만든다.
|
|
164
|
+
*/
|
|
165
|
+
collectAttentions() {
|
|
166
|
+
const out = super.collectAttentions();
|
|
167
|
+
for (const o of this.orders.values()) {
|
|
168
|
+
const short = o.shortage;
|
|
169
|
+
if (!short?.length)
|
|
170
|
+
continue;
|
|
171
|
+
out.push({
|
|
172
|
+
id: `material-short:${o.id}`,
|
|
173
|
+
kind: 'material-short',
|
|
174
|
+
/* 한 줄이 모자란 것과 여러 줄이 모자란 것은 할 일이 다르다 — 여럿이면 자재 공급 자체가 문제다. */
|
|
175
|
+
severity: short.length > 1 ? 'high' : 'medium',
|
|
176
|
+
anchor: { orderId: o.id },
|
|
177
|
+
params: {
|
|
178
|
+
/* 모자란 줄 수와 투입 줄 수 — 「6/18」이 「하나 모자람」과 다른 상황임을 한눈에 말한다. */
|
|
179
|
+
shortLines: short.length,
|
|
180
|
+
/* 자재 키를 그대로 낸다. 커널이 사람이 읽는 이름을 만들지 않는다 — 이름은 모델이 갖는다. */
|
|
181
|
+
materials: short.map(x => x.material).join(', '),
|
|
182
|
+
need: short.reduce((n, x) => n + x.need, 0),
|
|
183
|
+
have: short.reduce((n, x) => n + x.have, 0)
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
152
189
|
handleCommand(cmd) {
|
|
153
190
|
if (cmd.type === MES_CMD.changeover) {
|
|
154
191
|
const a = cmd.args;
|
|
@@ -417,6 +454,7 @@ export class MesKernel extends FlowEngine {
|
|
|
417
454
|
}
|
|
418
455
|
const rc = this.recipeDef(o);
|
|
419
456
|
const picks = [];
|
|
457
|
+
const short = [];
|
|
420
458
|
/*
|
|
421
459
|
* ── 자리별 색인을 사용한다 (2026-08-21) ────────────────────────────────────
|
|
422
460
|
* 예전에는 투입 줄마다 `[...this.items.values()].filter(...)` 로 **물품 전체를 순회**했다. 오더 하나에
|
|
@@ -438,6 +476,11 @@ export class MesKernel extends FlowEngine {
|
|
|
438
476
|
const g = this.classOf(line.material);
|
|
439
477
|
/* 자리 필터는 남는다 — 완제품도 `sellable` 이라, 반제품→완제품 체인에서 이것이 없으면 산출물을 자재로 소비한다. */
|
|
440
478
|
const fromType = this.locationTypeOfMaterial(line.material);
|
|
479
|
+
/*
|
|
480
|
+
* 후보를 배열로 모은다. 지연 순회(생성기)로 바꿔 보았으나 **더 느렸다** — 물품 16,000 규모에서
|
|
481
|
+
* 1562ms → 2723ms 였다. 항목마다 생성기 규약을 지나는 비용이 배열을 만드는 비용보다 크다.
|
|
482
|
+
* 측정이 그렇게 답했으므로 배열을 유지한다(§StockRequest.available 은 지연도 받는다).
|
|
483
|
+
*/
|
|
441
484
|
const available = [];
|
|
442
485
|
for (const n of locsOfType.get(fromType) ?? []) {
|
|
443
486
|
for (const i of this.items.at(n.id)) {
|
|
@@ -446,10 +489,31 @@ export class MesKernel extends FlowEngine {
|
|
|
446
489
|
}
|
|
447
490
|
}
|
|
448
491
|
const chosen = this.policy.selectStock({ gtin: g, qty: line.qty, available });
|
|
449
|
-
if (chosen.length < line.qty)
|
|
450
|
-
|
|
492
|
+
if (chosen.length < line.qty) {
|
|
493
|
+
/*
|
|
494
|
+
* **왜 대기하는지 남긴다** (2026-08-21).
|
|
495
|
+
*
|
|
496
|
+
* 예전에는 첫 부족한 줄에서 그대로 반환했다. 거동은 옳다(전량 확보 전에는 만들지 않는다) —
|
|
497
|
+
* 문제는 **조용한 것**이었다. 실측: Rosarito MES 는 투입 18줄 중 6줄이 한 번도 입고되지 않아
|
|
498
|
+
* 오더 다섯이 영구히 대기했고, 화면에는 「running」이라 적혔다. 어느 자리도 이유를 말하지 않았다.
|
|
499
|
+
*
|
|
500
|
+
* 첫 줄에서 멈추지 않고 **모자란 줄을 다 센다.** 하나만 알려 주면 그것을 채운 뒤 다음 줄에서
|
|
501
|
+
* 또 막히고, 사람은 같은 진단을 여섯 번 반복한다.
|
|
502
|
+
*/
|
|
503
|
+
short.push({ material: line.material, need: line.qty, have: chosen.length });
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
451
506
|
picks.push(...chosen);
|
|
452
507
|
}
|
|
508
|
+
if (short.length) {
|
|
509
|
+
/* 확보는 하지 않는다 — 사실만 남기고 다음 틱에 다시 본다(값은 매 틱 새로 쓰인다). */
|
|
510
|
+
o.shortage = short;
|
|
511
|
+
this.emitOrder(o);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
/* 채워졌으면 표시를 지운다 — 남겨 두면 화면이 이미 해결된 것을 계속 말한다. */
|
|
515
|
+
if (o.shortage)
|
|
516
|
+
delete o.shortage;
|
|
453
517
|
for (const epc of picks)
|
|
454
518
|
o.allocated.push(epc);
|
|
455
519
|
this.reserve(picks, MES_BIZSTEP.producing);
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -307,14 +307,14 @@ function classIndex(defs) {
|
|
|
307
307
|
}
|
|
308
308
|
var EMPTY_CLASS_INDEX = /* @__PURE__ */ new Map();
|
|
309
309
|
function classClosure(directIds, defs, at) {
|
|
310
|
-
const
|
|
310
|
+
const byId2 = classIndex(defs);
|
|
311
311
|
const inWindow = (d) => !d || effectivityAt(d, at) === void 0;
|
|
312
312
|
const out = /* @__PURE__ */ new Set();
|
|
313
313
|
const stack = [...directIds ?? []];
|
|
314
314
|
while (stack.length) {
|
|
315
315
|
const id = stack.pop();
|
|
316
316
|
if (out.has(id)) continue;
|
|
317
|
-
const def =
|
|
317
|
+
const def = byId2.get(id);
|
|
318
318
|
if (!inWindow(def)) continue;
|
|
319
319
|
out.add(id);
|
|
320
320
|
for (const b of def?.baseIds ?? []) if (!out.has(b)) stack.push(b);
|
|
@@ -2122,11 +2122,11 @@ var EMS_TYPES = [
|
|
|
2122
2122
|
}
|
|
2123
2123
|
];
|
|
2124
2124
|
function electricalUpstreamOf(locations, id) {
|
|
2125
|
-
const
|
|
2126
|
-
const self =
|
|
2125
|
+
const byId2 = new Map((locations ?? []).filter((l) => l?.id).map((l) => [String(l.id), l]));
|
|
2126
|
+
const self = byId2.get(String(id));
|
|
2127
2127
|
if (!self) return void 0;
|
|
2128
2128
|
if (self.upstreamId) return String(self.upstreamId);
|
|
2129
|
-
const parent = self.parentId ?
|
|
2129
|
+
const parent = self.parentId ? byId2.get(String(self.parentId)) : void 0;
|
|
2130
2130
|
if (!parent) return void 0;
|
|
2131
2131
|
return isElectricalLocationType(String(parent.type ?? "")) ? String(parent.id) : void 0;
|
|
2132
2132
|
}
|
|
@@ -2671,30 +2671,47 @@ function capabilitiesForType(system, typeKey) {
|
|
|
2671
2671
|
}
|
|
2672
2672
|
|
|
2673
2673
|
// src/allocation-policy.ts
|
|
2674
|
+
var byId = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
2674
2675
|
function freeBinsFirstFit(slots) {
|
|
2675
|
-
|
|
2676
|
-
for (const b of
|
|
2677
|
-
|
|
2676
|
+
let best;
|
|
2677
|
+
for (const b of slots) {
|
|
2678
|
+
if (b.occupancy + b.reserved >= b.capacity) continue;
|
|
2679
|
+
if (!best || byId(b.id, best.id) < 0) best = b;
|
|
2680
|
+
}
|
|
2681
|
+
return best?.id ?? null;
|
|
2678
2682
|
}
|
|
2679
|
-
function
|
|
2680
|
-
return [
|
|
2683
|
+
function smallestByEpc(available, k) {
|
|
2684
|
+
if (k <= 0) return { picked: [], total: 0 };
|
|
2685
|
+
const out = [];
|
|
2686
|
+
let total = 0;
|
|
2687
|
+
for (const s of available) {
|
|
2688
|
+
total++;
|
|
2689
|
+
if (out.length === k && byId(s.epc, out[out.length - 1].epc) >= 0) continue;
|
|
2690
|
+
let i = out.length;
|
|
2691
|
+
while (i > 0 && byId(out[i - 1].epc, s.epc) > 0) i--;
|
|
2692
|
+
out.splice(i, 0, s);
|
|
2693
|
+
if (out.length > k) out.pop();
|
|
2694
|
+
}
|
|
2695
|
+
return { picked: out, total };
|
|
2681
2696
|
}
|
|
2682
2697
|
var firstFitPolicy = {
|
|
2683
2698
|
selectPlacement: ({ slots }) => freeBinsFirstFit(slots),
|
|
2684
2699
|
selectStock: ({ qty, available }) => {
|
|
2685
|
-
|
|
2686
|
-
|
|
2700
|
+
const { picked, total } = smallestByEpc(available, qty);
|
|
2701
|
+
if (total < qty) return [];
|
|
2702
|
+
return picked.map((s) => s.epc);
|
|
2687
2703
|
}
|
|
2688
2704
|
};
|
|
2689
2705
|
var partialFitPolicy = {
|
|
2690
2706
|
selectPlacement: ({ slots }) => freeBinsFirstFit(slots),
|
|
2691
|
-
selectStock: ({ qty, available }) =>
|
|
2707
|
+
selectStock: ({ qty, available }) => smallestByEpc(available, qty).picked.map((s) => s.epc)
|
|
2692
2708
|
};
|
|
2693
2709
|
var fefoPolicy = {
|
|
2694
2710
|
selectPlacement: ({ slots }) => freeBinsFirstFit(slots),
|
|
2695
2711
|
selectStock: ({ qty, available }) => {
|
|
2696
|
-
|
|
2697
|
-
|
|
2712
|
+
const all = [...available];
|
|
2713
|
+
if (all.length < qty) return [];
|
|
2714
|
+
const byExpiry = all.sort((a, b) => (a.expiry ?? Infinity) - (b.expiry ?? Infinity) || byId(a.epc, b.epc));
|
|
2698
2715
|
return byExpiry.slice(0, qty).map((s) => s.epc);
|
|
2699
2716
|
}
|
|
2700
2717
|
};
|
|
@@ -2861,8 +2878,11 @@ function mapRecordChecked(record, mapping, eventTime) {
|
|
|
2861
2878
|
const outputEPCList = resolveList(mapping.outputEPCList, record, "outputEPCList", errors);
|
|
2862
2879
|
const outputQuantityList = resolveQuantityList(mapping.outputQuantityList, record, "outputQuantityList", errors);
|
|
2863
2880
|
const transformationID = resolve(mapping.transformationID, record, "transformationID", errors);
|
|
2864
|
-
if (!inputEPCList.length && !inputQuantityList.length
|
|
2865
|
-
errors.push("\uC785\uB825\
|
|
2881
|
+
if (!inputEPCList.length && !inputQuantityList.length) {
|
|
2882
|
+
errors.push("\uC785\uB825\uC774 \uBE44\uC5C8\uB2E4 \u2014 \uBCC0\uD658\uC740 \uBB34\uC5C7\uC774 \uB4E4\uC5B4\uAC14\uB294\uC9C0 \uB9D0\uD574\uC57C \uD55C\uB2E4(EPCIS 2.0 \xA77.4.5)");
|
|
2883
|
+
}
|
|
2884
|
+
if (!outputEPCList.length && !outputQuantityList.length) {
|
|
2885
|
+
errors.push("\uCD9C\uB825\uC774 \uBE44\uC5C8\uB2E4 \u2014 \uBCC0\uD658\uC740 \uBB34\uC5C7\uC774 \uB098\uC654\uB294\uC9C0 \uB9D0\uD574\uC57C \uD55C\uB2E4(EPCIS 2.0 \xA77.4.5)");
|
|
2866
2886
|
}
|
|
2867
2887
|
return {
|
|
2868
2888
|
event: transformationEvent({
|
|
@@ -2870,7 +2890,7 @@ function mapRecordChecked(record, mapping, eventTime) {
|
|
|
2870
2890
|
bizStep,
|
|
2871
2891
|
disposition,
|
|
2872
2892
|
/* 없는 쪽은 **필드를 만들지 않는다** — 빈 배열을 실으면 「없다」고 말하는 것이 된다.
|
|
2873
|
-
|
|
2893
|
+
개체로 말한 쪽과 수량으로 말한 쪽 중 하나만 쓰는 것이 정상이다. */
|
|
2874
2894
|
...inputEPCList.length ? { inputEPCList } : {},
|
|
2875
2895
|
...inputQuantityList.length ? { inputQuantityList } : {},
|
|
2876
2896
|
...outputEPCList.length ? { outputEPCList } : {},
|
|
@@ -3429,6 +3449,11 @@ var ItemStore = class _ItemStore {
|
|
|
3429
3449
|
for (const [k, it] of this.map) out.set(k, structuredClone(it));
|
|
3430
3450
|
return out;
|
|
3431
3451
|
}
|
|
3452
|
+
/*
|
|
3453
|
+
* 지연 순회(`*iterAt`)를 만들어 배열을 없애 보았으나 **더 느렸다** — 물품 16,000 규모에서 틱 합계가
|
|
3454
|
+
* 1562ms 에서 2723ms 로 늘었다. 항목마다 생성기 규약을 지나는 비용이 배열 하나를 만드는 비용보다
|
|
3455
|
+
* 크다. 측정이 그렇게 답했으므로 배열을 유지한다.
|
|
3456
|
+
*/
|
|
3432
3457
|
/** 그 자리에 있는 물품들 — 색인이 답한다(전체 순회가 아니다). */
|
|
3433
3458
|
at(location) {
|
|
3434
3459
|
const keys = this.byLocation.get(location);
|
|
@@ -4286,6 +4311,8 @@ var FlowEngine = class {
|
|
|
4286
4311
|
...o.recipeKey ? { recipeKey: o.recipeKey } : {},
|
|
4287
4312
|
/* 씨앗이 다 심지 못했다는 사실 — 조용히 자르지 않는다(그 오더의 답은 부족한 씨앗 위에 있다). */
|
|
4288
4313
|
...o.seedIncomplete ? { seedIncomplete: true } : {},
|
|
4314
|
+
/* 왜 대기하는지 — 없으면 화면은 「running」만 보여 주고 사람은 원인을 찾을 자리가 없다. */
|
|
4315
|
+
...o.shortage?.length ? { shortage: o.shortage.map((x) => ({ ...x })) } : {},
|
|
4289
4316
|
...o.lines?.length ? { lines: o.lines.map((l) => ({ gtin: l.gtin, requested: l.requested })) } : {},
|
|
4290
4317
|
...o.priority !== void 0 ? { priority: o.priority } : {},
|
|
4291
4318
|
...o.startTime ? { startTime: o.startTime } : {},
|
|
@@ -6620,6 +6647,42 @@ var MesKernel = class extends FlowEngine {
|
|
|
6620
6647
|
* 이미 그 제품이면 no-op, 아니면 셋업(기본값, OEE 가용성 손실) + lastChangeoverKey 각인
|
|
6621
6648
|
* (이후 그 제품 task 는 자동 셋업 생략). command → 변이 → State 델타(폐루프).
|
|
6622
6649
|
*/
|
|
6650
|
+
/**
|
|
6651
|
+
* **왜 대기하는지 화면에 말한다** — 자재가 모자라 확보가 미뤄진 오더.
|
|
6652
|
+
*
|
|
6653
|
+
* ── 왜 필요한가 (2026-08-21 실측) ─────────────────────────────────────────
|
|
6654
|
+
* 확보는 전량 아니면 대기다(부분 투입을 하면 재고가 거짓이 된다). 그 거동은 옳다. 그런데 조용해서,
|
|
6655
|
+
* 투입 18줄 중 6줄이 한 번도 입고되지 않은 공장에서 **오더 다섯이 영구히 대기하는 동안 화면에는
|
|
6656
|
+
* 「running」이라 적혔다.** 아무 자리도 이유를 말하지 않았다.
|
|
6657
|
+
*
|
|
6658
|
+
* 판단만 여기서 한다 — 확인(ack)·처음 성립한 시각·사라진 조건 정리는 `computeAttentions` 가
|
|
6659
|
+
* 한 곳에서 맡는다(확장점 규약).
|
|
6660
|
+
*
|
|
6661
|
+
* 사람이 읽는 문장은 만들지 않는다. `kind` 와 언어 중립 `params` 만 내고 문장은 표현계층이 만든다.
|
|
6662
|
+
*/
|
|
6663
|
+
collectAttentions() {
|
|
6664
|
+
const out = super.collectAttentions();
|
|
6665
|
+
for (const o of this.orders.values()) {
|
|
6666
|
+
const short = o.shortage;
|
|
6667
|
+
if (!short?.length) continue;
|
|
6668
|
+
out.push({
|
|
6669
|
+
id: `material-short:${o.id}`,
|
|
6670
|
+
kind: "material-short",
|
|
6671
|
+
/* 한 줄이 모자란 것과 여러 줄이 모자란 것은 할 일이 다르다 — 여럿이면 자재 공급 자체가 문제다. */
|
|
6672
|
+
severity: short.length > 1 ? "high" : "medium",
|
|
6673
|
+
anchor: { orderId: o.id },
|
|
6674
|
+
params: {
|
|
6675
|
+
/* 모자란 줄 수와 투입 줄 수 — 「6/18」이 「하나 모자람」과 다른 상황임을 한눈에 말한다. */
|
|
6676
|
+
shortLines: short.length,
|
|
6677
|
+
/* 자재 키를 그대로 낸다. 커널이 사람이 읽는 이름을 만들지 않는다 — 이름은 모델이 갖는다. */
|
|
6678
|
+
materials: short.map((x) => x.material).join(", "),
|
|
6679
|
+
need: short.reduce((n, x) => n + x.need, 0),
|
|
6680
|
+
have: short.reduce((n, x) => n + x.have, 0)
|
|
6681
|
+
}
|
|
6682
|
+
});
|
|
6683
|
+
}
|
|
6684
|
+
return out;
|
|
6685
|
+
}
|
|
6623
6686
|
handleCommand(cmd) {
|
|
6624
6687
|
if (cmd.type === MES_CMD.changeover) {
|
|
6625
6688
|
const a = cmd.args;
|
|
@@ -6862,6 +6925,7 @@ var MesKernel = class extends FlowEngine {
|
|
|
6862
6925
|
}
|
|
6863
6926
|
const rc = this.recipeDef(o);
|
|
6864
6927
|
const picks = [];
|
|
6928
|
+
const short = [];
|
|
6865
6929
|
const locsOfType = /* @__PURE__ */ new Map();
|
|
6866
6930
|
for (const n of this.locations.values()) {
|
|
6867
6931
|
const bin = locsOfType.get(n.type);
|
|
@@ -6878,9 +6942,18 @@ var MesKernel = class extends FlowEngine {
|
|
|
6878
6942
|
}
|
|
6879
6943
|
}
|
|
6880
6944
|
const chosen = this.policy.selectStock({ gtin: g, qty: line.qty, available });
|
|
6881
|
-
if (chosen.length < line.qty)
|
|
6945
|
+
if (chosen.length < line.qty) {
|
|
6946
|
+
short.push({ material: line.material, need: line.qty, have: chosen.length });
|
|
6947
|
+
continue;
|
|
6948
|
+
}
|
|
6882
6949
|
picks.push(...chosen);
|
|
6883
6950
|
}
|
|
6951
|
+
if (short.length) {
|
|
6952
|
+
o.shortage = short;
|
|
6953
|
+
this.emitOrder(o);
|
|
6954
|
+
return;
|
|
6955
|
+
}
|
|
6956
|
+
if (o.shortage) delete o.shortage;
|
|
6884
6957
|
for (const epc of picks) o.allocated.push(epc);
|
|
6885
6958
|
this.reserve(picks, MES_BIZSTEP.producing);
|
|
6886
6959
|
this.emit(transactionEvent({ eventTime: this.now(), action: "ADD", bizStep: MES_BIZSTEP.producing, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: o.bizTransaction }], epcList: o.allocated.slice() }));
|
|
@@ -8073,7 +8146,7 @@ function ingestOperationalRecords(records, opts) {
|
|
|
8073
8146
|
// src/energy-attribution.ts
|
|
8074
8147
|
var near = (a, b, eps = 1e-9) => Math.abs(a - b) <= eps;
|
|
8075
8148
|
function attributeEnergy(opts) {
|
|
8076
|
-
const
|
|
8149
|
+
const byId2 = new Map(opts.consumers.map((c) => [c.id, c]));
|
|
8077
8150
|
const dedicated = /* @__PURE__ */ new Map();
|
|
8078
8151
|
for (const c of opts.consumers) if (c.meterId) dedicated.set(c.meterId, c);
|
|
8079
8152
|
const shares = [];
|
|
@@ -8090,7 +8163,7 @@ function attributeEnergy(opts) {
|
|
|
8090
8163
|
}
|
|
8091
8164
|
if (pool.overhead) {
|
|
8092
8165
|
const alloc = opts.overheadAllocation;
|
|
8093
|
-
const targets = (alloc?.processConsumerIds ?? []).map((id) =>
|
|
8166
|
+
const targets = (alloc?.processConsumerIds ?? []).map((id) => byId2.get(id)).filter((c) => !!c);
|
|
8094
8167
|
const wOf = (c) => alloc?.weightKind === "equal" ? 1 : Number.isFinite(Number(c.weight)) && Number(c.weight) > 0 ? Number(c.weight) : 0;
|
|
8095
8168
|
const sharing2 = alloc?.weightKind === "equal" ? targets : targets.filter((c) => wOf(c) > 0);
|
|
8096
8169
|
const ws = sharing2.map(wOf);
|
|
@@ -8116,7 +8189,7 @@ function attributeEnergy(opts) {
|
|
|
8116
8189
|
});
|
|
8117
8190
|
continue;
|
|
8118
8191
|
}
|
|
8119
|
-
const covered = (pool.consumerIds ?? []).map((id) =>
|
|
8192
|
+
const covered = (pool.consumerIds ?? []).map((id) => byId2.get(id)).filter((c) => !!c);
|
|
8120
8193
|
if (!covered.length) {
|
|
8121
8194
|
unattributed.push({ poolMeterId: pool.meterId, kWh, reason: pool.remainder ? "not-submetered" : "no-consumers" });
|
|
8122
8195
|
continue;
|
package/package.json
CHANGED