@operato/twin-kernel 0.0.4 → 0.0.6
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.d.ts +50 -0
- package/dist/capability.js +44 -0
- package/dist/contract.d.ts +91 -1
- package/dist/contract.js +11 -2
- package/dist/domain-catalog.d.ts +25 -1
- package/dist/domain-catalog.js +12 -11
- package/dist/domain-definition.d.ts +91 -0
- package/dist/domain-definition.js +76 -0
- package/dist/flow-engine.d.ts +77 -1
- package/dist/flow-engine.js +271 -30
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -1
- package/dist/kernel.js +1 -1
- package/dist/mes-kernel.d.ts +48 -4
- package/dist/mes-kernel.js +185 -24
- package/dist/mes-profile.d.ts +5 -2
- package/dist/mes-profile.js +23 -7
- package/dist/state-projector.js +2 -2
- package/dist/wms-profile.d.ts +2 -0
- package/dist/wms-profile.js +9 -5
- package/dist/yms-profile.d.ts +2 -0
- package/dist/yms-profile.js +8 -8
- package/dist-cjs/index.cjs +573 -62
- package/package.json +1 -1
package/dist/flow-engine.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* (roadmap Phase5 발견 → 추출. [[project_flow_single_base_vision]] FlowNode 단일 base 방향과 정합.)
|
|
11
11
|
*/
|
|
12
12
|
import { OP_EVENT, CMD } from "./contract.js";
|
|
13
|
-
import { transformationEvent, aggregationEvent, objectEvent } from "./epcis.js";
|
|
13
|
+
import { transformationEvent, aggregationEvent, objectEvent, DISP } from "./epcis.js";
|
|
14
14
|
const BASE_EPOCH = Date.parse('2026-01-01T00:00:00Z');
|
|
15
15
|
function mulberry32(seed) {
|
|
16
16
|
let a = seed >>> 0;
|
|
@@ -23,6 +23,94 @@ function mulberry32(seed) {
|
|
|
23
23
|
Object.defineProperty(fn, 'state', { get: () => a, set: (v) => { a = v >>> 0; } });
|
|
24
24
|
return fn;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* 주목신호 계산(순수) — State 스냅샷(movers/nodes/orders)에서 attentions 파생.
|
|
28
|
+
* FlowEngine.computeAttentions(sim) 와 live projector 미러가 **공유**하는 계산 층(face2-inbound-live §1.1):
|
|
29
|
+
* sim/live 가 같은 임계값·규칙을 쓴다. good/scrap 미제공(관측 상태에 없음)이면 scrap-high 는 자연 스킵(metric 층 갭).
|
|
30
|
+
*/
|
|
31
|
+
export function deriveAttentions(view, acked) {
|
|
32
|
+
// 언어 중립: kind + params(원시값) + 조치 code 만 방출. 사람이 읽는 title/detail/rationale/라벨은
|
|
33
|
+
// 표현계층(클라 i18next)이 kind/code 로 렌더한다(무방언·다국어 — design/plans/i18n.md L3).
|
|
34
|
+
const out = [];
|
|
35
|
+
for (const m of view.movers) {
|
|
36
|
+
if (m.status === 'down') {
|
|
37
|
+
out.push({
|
|
38
|
+
id: `breakdown:${m.id}`, kind: 'breakdown', severity: 'critical',
|
|
39
|
+
anchor: { moverId: m.id, nodeId: m.location },
|
|
40
|
+
params: { moverId: m.id, ...(m.location ? { nodeId: m.location } : {}) },
|
|
41
|
+
recommendedActions: [
|
|
42
|
+
{ code: 'act.repair', command: CMD.resourceRepair, args: { resourceId: m.id } },
|
|
43
|
+
{ code: 'act.hold-until-repair', command: CMD.resourceHold, args: { resourceId: m.id } }
|
|
44
|
+
],
|
|
45
|
+
suggestedAction: { code: 'act.repair', command: CMD.resourceRepair, args: { resourceId: m.id } }
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for (const n of view.nodes) {
|
|
50
|
+
if ((n.capacity ?? 0) > 0) {
|
|
51
|
+
const r = (n.occupancy ?? 0) / n.capacity;
|
|
52
|
+
if (r >= 0.9) {
|
|
53
|
+
const saturated = r >= 1;
|
|
54
|
+
out.push({
|
|
55
|
+
id: `bottleneck:${n.id}`, kind: 'bottleneck', severity: saturated ? 'high' : 'medium',
|
|
56
|
+
anchor: { nodeId: n.id },
|
|
57
|
+
params: { nodeId: n.id, occupancy: n.occupancy ?? 0, capacity: n.capacity ?? 0, ratioPct: Math.round(r * 100), saturated: saturated ? 1 : 0 },
|
|
58
|
+
recommendedActions: [{ code: 'advice.add-resource' }, { code: 'advice.downstream-priority' }]
|
|
59
|
+
// 권고만(command 없음) — 표현계층이 advice.* 코드로 렌더
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const m of view.movers) {
|
|
65
|
+
const total = (m.goodCount ?? 0) + (m.scrapCount ?? 0);
|
|
66
|
+
if (total >= 10) {
|
|
67
|
+
const rate = (m.scrapCount ?? 0) / total;
|
|
68
|
+
if (rate >= 0.15)
|
|
69
|
+
out.push({
|
|
70
|
+
id: `scrap:${m.id}`, kind: 'scrap-high', severity: rate >= 0.3 ? 'high' : 'medium',
|
|
71
|
+
anchor: { moverId: m.id, nodeId: m.location },
|
|
72
|
+
params: { moverId: m.id, goodCount: m.goodCount ?? 0, scrapCount: m.scrapCount ?? 0, ratePct: Math.round(rate * 100) },
|
|
73
|
+
recommendedActions: [
|
|
74
|
+
{ code: 'act.hold-for-inspection', command: CMD.resourceHold, args: { resourceId: m.id } },
|
|
75
|
+
{ code: 'act.reset-metrics', command: CMD.resourceResetMetrics, args: { resourceId: m.id } }
|
|
76
|
+
],
|
|
77
|
+
suggestedAction: { code: 'act.hold-for-inspection', command: CMD.resourceHold, args: { resourceId: m.id } }
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const o of view.orders) {
|
|
82
|
+
if (o.held)
|
|
83
|
+
out.push({
|
|
84
|
+
id: `hold:${o.id}`, kind: 'hold', severity: 'medium',
|
|
85
|
+
anchor: { orderId: o.id },
|
|
86
|
+
params: { orderId: o.id },
|
|
87
|
+
recommendedActions: [{ code: 'act.resume-order', command: CMD.orderResume, args: { orderId: o.id } }],
|
|
88
|
+
suggestedAction: { code: 'act.resume-order', command: CMD.orderResume, args: { orderId: o.id } }
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (acked)
|
|
92
|
+
for (const a of out)
|
|
93
|
+
if (acked.has(a.id))
|
|
94
|
+
a.state = 'acknowledged';
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* OEE 공식(순수) — 누적 카운터 + 현재 시각 → OEE. sim(oeeOf)과 live 가 **공유하는 계산 층**(face2-inbound-live §1.1).
|
|
99
|
+
* ⚠ attentions 와 달리: 입력(카운터)이 관측 State·이벤트에 없다 → live 는 카운터를 텔레메트리(정확) 또는
|
|
100
|
+
* 이벤트 누적기(근사)로 별도 공급해야 한다. 공식만 공유되고 입력원은 live 데이터-생산 결정.
|
|
101
|
+
*/
|
|
102
|
+
export function computeOee(c, nowMs) {
|
|
103
|
+
const planned = Math.max(0, nowMs - (c.metricsSinceMs ?? 0) - (c.holdMs ?? 0));
|
|
104
|
+
const uptime = Math.max(0, planned - c.setupMs - c.downMs); // 가용시간(셋업·고장 제외)
|
|
105
|
+
const availability = planned > 0 ? uptime / planned : 1;
|
|
106
|
+
const performance = uptime > 0 ? Math.min(1, c.runMs / uptime) : (c.runMs > 0 ? 1 : 0);
|
|
107
|
+
const totalQ = c.goodCount + c.scrapCount;
|
|
108
|
+
const quality = totalQ > 0 ? c.goodCount / totalQ : 1;
|
|
109
|
+
return {
|
|
110
|
+
availability, performance, quality, overall: availability * performance * quality,
|
|
111
|
+
runMs: c.runMs, setupMs: c.setupMs, downMs: c.downMs, idleMs: Math.max(0, uptime - c.runMs), goodCount: c.goodCount, scrapCount: c.scrapCount
|
|
112
|
+
};
|
|
113
|
+
}
|
|
26
114
|
export class FlowEngine {
|
|
27
115
|
tenantId;
|
|
28
116
|
nodes = new Map();
|
|
@@ -52,7 +140,7 @@ export class FlowEngine {
|
|
|
52
140
|
// ── TwinKernel (mechanics, 도메인 무관) ───────────────────────────────────
|
|
53
141
|
loadBoard(def) {
|
|
54
142
|
for (const n of def.nodes)
|
|
55
|
-
this.nodes.set(n.id, { id: n.id, type: n.type, capacity: n.capacity, occupancy: 0, status: 'idle' });
|
|
143
|
+
this.nodes.set(n.id, { id: n.id, type: n.type, capacity: n.capacity, occupancy: 0, status: 'idle', parentId: n.parentId });
|
|
56
144
|
for (const m of def.movers) {
|
|
57
145
|
const mover = { id: m.id, kind: m.kind, location: m.homeNode, status: 'idle', taskId: null, runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, scrapCount: 0 };
|
|
58
146
|
if (m.mtbfMs !== undefined) {
|
|
@@ -63,6 +151,60 @@ export class FlowEngine {
|
|
|
63
151
|
this.movers.set(m.id, mover);
|
|
64
152
|
}
|
|
65
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* what-if 구성 변주 — fork(또는 실행 중) 엔진에 무버 추가. loadBoard 무버 삽입과 동일 규약.
|
|
156
|
+
* 기본은 mtbf 미지정(고장 없는 신뢰 자원) → sampleExp(rng) 무소비라 baseline fork 와 깨끗이 비교 가능.
|
|
157
|
+
*/
|
|
158
|
+
addMover(m) {
|
|
159
|
+
if (this.movers.has(m.id))
|
|
160
|
+
return;
|
|
161
|
+
const mover = { id: m.id, kind: m.kind, location: m.homeNode, status: 'idle', taskId: null, runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, scrapCount: 0 };
|
|
162
|
+
if (m.mtbfMs !== undefined) {
|
|
163
|
+
mover.mtbfMs = m.mtbfMs;
|
|
164
|
+
mover.mttrMs = m.mttrMs;
|
|
165
|
+
mover.nextFailureMs = this.sampleExp(m.mtbfMs);
|
|
166
|
+
}
|
|
167
|
+
this.movers.set(m.id, mover);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* 관측 상태 주입(라이브 예측용, kernel-unification P1) — 외부 관측 스냅샷(재고·무버·노드)과
|
|
171
|
+
* 저널 오더(원값+라인)로 이 커널의 맵을 채운다. tick 으로 만든 게 아니라 "현재 관측된 현실"을 심어
|
|
172
|
+
* 이후 fork/tick 으로 예측한다. 라이브 런타임은 여전히 projector 미러 — 이 커널은 예측용 임시본.
|
|
173
|
+
* 오더는 남은 데맨드(라인별 requested-fulfilled)를 'created' 로 복원(현재 재고에서 재계획).
|
|
174
|
+
* 진행 중 개별 task 의 내부 상태는 관측만으론 복원 불가 → 재계획에 맡김(정직한 한계).
|
|
175
|
+
*/
|
|
176
|
+
hydrateObserved(snap, orders = []) {
|
|
177
|
+
for (const n of snap.nodes)
|
|
178
|
+
this.nodes.set(n.id, { id: n.id, type: n.type, capacity: n.capacity ?? 0, occupancy: n.occupancy ?? 0, status: 'idle', parentId: n.parentId });
|
|
179
|
+
this.items.clear();
|
|
180
|
+
for (const it of snap.items)
|
|
181
|
+
this.items.set(it.epc, { epc: it.epc, location: it.location, disposition: it.disposition ?? DISP.sellable, gtin: it.gtin, qty: it.qty ?? 1 });
|
|
182
|
+
for (const m of snap.movers)
|
|
183
|
+
this.movers.set(m.id, { id: m.id, kind: m.kind, location: m.location ?? '', status: 'idle', taskId: null, runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, scrapCount: 0 });
|
|
184
|
+
for (const o of orders) {
|
|
185
|
+
const lines = (o.lines ?? []).map(l => ({ gtin: l.gtin, requested: l.requested - (l.fulfilled ?? 0) })).filter(l => l.requested > 0);
|
|
186
|
+
const remaining = lines.reduce((s, l) => s + l.requested, 0);
|
|
187
|
+
if (remaining <= 0)
|
|
188
|
+
continue; // 이미 이행 완료 → 예측 대상 아님
|
|
189
|
+
this.orders.set(o.orderId, { id: o.orderId, kind: o.kind, status: 'created', requested: remaining, fulfilled: 0, bizTransaction: '', allocated: [], picked: [], shipmentEpc: null, lines });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/** what-if 구성 변주 — 노드 용량 변경(fork 대상). 존재하면 true. */
|
|
193
|
+
setNodeCapacity(nodeId, capacity) {
|
|
194
|
+
const n = this.nodes.get(nodeId);
|
|
195
|
+
if (!n)
|
|
196
|
+
return false;
|
|
197
|
+
n.capacity = Math.max(0, capacity);
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* forecast 몬테카를로 — fork 의 RNG 만 재시드(시나리오·상태·gens·in-flight 는 보존).
|
|
202
|
+
* "현재 조건 지속"을 유지한 채 **미래 확률만** 변주(도착·고장 타이밍 등) → run 마다 다른 표본.
|
|
203
|
+
* (monteCarloForecast 은 scenario.load 로 gens 를 갈아끼우므로 "현재 조건"이 깨진다 — 그 대안.)
|
|
204
|
+
*/
|
|
205
|
+
reseed(seed) {
|
|
206
|
+
this.rng = mulberry32(seed >>> 0);
|
|
207
|
+
}
|
|
66
208
|
onEvent(handler) {
|
|
67
209
|
this.handlers.push(handler);
|
|
68
210
|
return () => { const i = this.handlers.indexOf(handler); if (i >= 0)
|
|
@@ -73,27 +215,105 @@ export class FlowEngine {
|
|
|
73
215
|
* 도메인 커맨드(order.release 등)는 handleCommand 로 위임. 커맨드는 sim 상태를 변이하고
|
|
74
216
|
* State 델타를 유발한다(command → 행위 → 관측 폐루프).
|
|
75
217
|
*/
|
|
218
|
+
_acked = new Set(); // 확인(ack)된 주목 신호 id — 조건 지속돼도 acknowledged 로 표시(재발 시 재활성)
|
|
76
219
|
dispatch(cmd) {
|
|
77
220
|
const ok = () => ({ commandId: cmd.commandId, accepted: true });
|
|
78
|
-
|
|
221
|
+
// 거절 사유 = 언어 중립 코드 + 원시 파라미터. error 는 영어 폴백(로그·개발자용).
|
|
222
|
+
const fail = (errorCode, errorParams) => ({ commandId: cmd.commandId, accepted: false, errorCode, errorParams, error: errorCode });
|
|
79
223
|
switch (cmd.type) {
|
|
80
224
|
case CMD.orderHold:
|
|
81
225
|
case CMD.orderResume: {
|
|
82
226
|
const orderId = cmd.args?.orderId;
|
|
83
227
|
const order = orderId ? this.orders.get(orderId) : undefined;
|
|
84
228
|
if (!order)
|
|
85
|
-
return fail(
|
|
229
|
+
return fail('order-not-found', { orderId: orderId ?? '' });
|
|
86
230
|
order.held = cmd.type === CMD.orderHold;
|
|
87
231
|
this.emitOrder(order);
|
|
88
232
|
return ok();
|
|
89
233
|
}
|
|
234
|
+
case CMD.attentionAck: {
|
|
235
|
+
const id = cmd.args?.id;
|
|
236
|
+
if (id)
|
|
237
|
+
this._acked.add(id);
|
|
238
|
+
return ok();
|
|
239
|
+
}
|
|
240
|
+
// Operable 코어 — 자원(설비·무버) 제어. capability-keyed(resourceId), 모든 operable 자원 공통.
|
|
241
|
+
case CMD.resourceHold:
|
|
242
|
+
case CMD.resourceResume: {
|
|
243
|
+
const m = this.movers.get(cmd.args?.resourceId ?? '');
|
|
244
|
+
if (!m)
|
|
245
|
+
return fail('resource-not-found');
|
|
246
|
+
m.held = cmd.type === CMD.resourceHold; // 계획 정지 → 배정 스킵
|
|
247
|
+
this.emitMover(m);
|
|
248
|
+
return ok();
|
|
249
|
+
}
|
|
250
|
+
case CMD.resourceDown: {
|
|
251
|
+
const a = cmd.args;
|
|
252
|
+
const m = this.movers.get(a?.resourceId ?? '');
|
|
253
|
+
if (!m)
|
|
254
|
+
return fail('resource-not-found');
|
|
255
|
+
if (m.status !== 'down') {
|
|
256
|
+
m.status = 'down'; // 비계획 고장 주입 — 기존 고장 machinery(processFailures)가 downMs 누적·수리 처리
|
|
257
|
+
m.repairUntilMs = this.clockMs + (Number(a?.durationMs) || m.mttrMs || 60_000);
|
|
258
|
+
this.emitMover(m);
|
|
259
|
+
}
|
|
260
|
+
return ok();
|
|
261
|
+
}
|
|
262
|
+
case CMD.resourceRepair: {
|
|
263
|
+
const m = this.movers.get(cmd.args?.resourceId ?? '');
|
|
264
|
+
if (!m)
|
|
265
|
+
return fail('resource-not-found');
|
|
266
|
+
if (m.status === 'down') {
|
|
267
|
+
m.status = m.taskId ? 'busy' : 'idle'; // 진행중 task 있으면 재개(고장모델 수리 로직과 동형)
|
|
268
|
+
m.repairUntilMs = undefined;
|
|
269
|
+
if (m.mtbfMs !== undefined)
|
|
270
|
+
m.nextFailureMs = this.clockMs + this.sampleExp(m.mtbfMs);
|
|
271
|
+
this.emitMover(m);
|
|
272
|
+
}
|
|
273
|
+
return ok();
|
|
274
|
+
}
|
|
275
|
+
case CMD.resourceResetMetrics: {
|
|
276
|
+
const m = this.movers.get(cmd.args?.resourceId ?? '');
|
|
277
|
+
if (!m)
|
|
278
|
+
return fail('resource-not-found');
|
|
279
|
+
m.runMs = 0;
|
|
280
|
+
m.setupMs = 0;
|
|
281
|
+
m.downMs = 0;
|
|
282
|
+
m.goodCount = 0;
|
|
283
|
+
m.scrapCount = 0;
|
|
284
|
+
m.holdMs = 0;
|
|
285
|
+
m.metricsSinceMs = this.clockMs; // 계측 창을 지금부터 재시작
|
|
286
|
+
this.emitMover(m);
|
|
287
|
+
return ok();
|
|
288
|
+
}
|
|
289
|
+
case CMD.resourceAdd: {
|
|
290
|
+
// 라이브 자원 추가(실제 act) — addMover(what-if 와 동일 경로)로 런타임에 무버 삽입 + equipment 델타 방출.
|
|
291
|
+
// 새 무버는 즉시 배정 대상(다음 tick). 유일 id 생성(충돌 회피). 좌표/persistence 는 호스트 몫(커널=위상만).
|
|
292
|
+
const a = cmd.args;
|
|
293
|
+
if (!a?.kind)
|
|
294
|
+
return fail('kind-required');
|
|
295
|
+
if (!a?.homeNode || !this.nodes.has(a.homeNode))
|
|
296
|
+
return fail('home-node-not-found', { homeNode: a?.homeNode ?? '' });
|
|
297
|
+
const count = Math.max(1, Math.min(50, Number(a.count) || 1));
|
|
298
|
+
let seq = this.movers.size;
|
|
299
|
+
for (let i = 0; i < count; i++) {
|
|
300
|
+
let id = `${a.kind}-${++seq}`;
|
|
301
|
+
while (this.movers.has(id))
|
|
302
|
+
id = `${a.kind}-${++seq}`;
|
|
303
|
+
this.addMover({ id, kind: a.kind, homeNode: a.homeNode });
|
|
304
|
+
const m = this.movers.get(id);
|
|
305
|
+
if (m)
|
|
306
|
+
this.emitMover(m); // equipment.status 델타 → 상태/저널에 새 무버 반영
|
|
307
|
+
}
|
|
308
|
+
return ok();
|
|
309
|
+
}
|
|
90
310
|
default:
|
|
91
311
|
return this.handleCommand(cmd);
|
|
92
312
|
}
|
|
93
313
|
}
|
|
94
314
|
/** 도메인 커맨드 처리(order.release 등). 기본은 거절 — 도메인이 override. */
|
|
95
315
|
handleCommand(cmd) {
|
|
96
|
-
return { commandId: cmd.commandId, accepted: false,
|
|
316
|
+
return { commandId: cmd.commandId, accepted: false, errorCode: 'unknown-command', errorParams: { type: cmd.type }, error: `unknown-command: ${cmd.type}` };
|
|
97
317
|
}
|
|
98
318
|
scenario = {
|
|
99
319
|
load: (def) => {
|
|
@@ -128,16 +348,32 @@ export class FlowEngine {
|
|
|
128
348
|
nodes: [...this.nodes.values()].map(n => ({ ...n })),
|
|
129
349
|
items: [...this.items.values()].map(i => ({ epc: i.epc, gtin: i.gtin, qty: i.qty, location: i.location, disposition: i.disposition, expiry: i.expiry })),
|
|
130
350
|
movers: [...this.movers.values()].map(m => {
|
|
131
|
-
const s = { id: m.id, kind: m.kind, location: m.location, status: m.status, taskId: m.taskId ?? undefined, oee: this.oeeOf(m) };
|
|
351
|
+
const s = { id: m.id, kind: m.kind, location: m.location, status: m.status, taskId: m.taskId ?? undefined, oee: this.oeeOf(m), held: m.held };
|
|
132
352
|
const t = m.taskId ? this.tasks.get(m.taskId) : undefined;
|
|
133
353
|
if (t && t.status === 'in-progress' && t.intent !== 'process')
|
|
134
354
|
s.motion = { fromNode: t.fromNode, toNode: t.toNode, startedAtSimMs: this.clockMs - (t.durationMs - t.remainingMs), durationMs: t.durationMs, progress: this.progressOf(t), elapsedMs: t.durationMs - t.remainingMs };
|
|
135
355
|
return s;
|
|
136
356
|
}),
|
|
137
|
-
tasks: [...this.tasks.values()].map(t => ({ id: t.id, kind: t.kind, status: t.status, itemRefs: [t.itemEpc], fromNode: t.fromNode, toNode: t.toNode, resourceRef: t.resource ?? undefined, progress: t.status === 'in-progress' ? this.progressOf(t) : undefined })),
|
|
138
|
-
orders: [...this.orders.values()].map(o => ({ id: o.id, kind: o.kind, status: o.status, progress: o.requested ? o.fulfilled / o.requested : 0, held: o.held }))
|
|
357
|
+
tasks: [...this.tasks.values()].map(t => ({ id: t.id, kind: t.kind, status: t.status, itemRefs: [t.itemEpc], fromNode: t.fromNode, toNode: t.toNode, resourceRef: t.resource ?? undefined, orderId: t.orderId, progress: t.status === 'in-progress' ? this.progressOf(t) : undefined })),
|
|
358
|
+
orders: [...this.orders.values()].map(o => ({ id: o.id, kind: o.kind, status: o.status, progress: o.requested ? o.fulfilled / o.requested : 0, held: o.held })),
|
|
359
|
+
attentions: this.computeAttentions()
|
|
139
360
|
};
|
|
140
361
|
}
|
|
362
|
+
/*
|
|
363
|
+
* 주목 신호 판단 — 상태(노드·무버·오더)에서 도메인 조건을 평가해 Attention 방출.
|
|
364
|
+
* severity=ISA-18.2 우선순위 계열, kind=도메인 라벨. UI 는 판단 안 함(임계값 여기 소유).
|
|
365
|
+
* 도메인별 추가 판단은 서브클래스가 override 로 확장(super.computeAttentions() 합성).
|
|
366
|
+
*/
|
|
367
|
+
computeAttentions() {
|
|
368
|
+
// 계산 층은 순수 함수 deriveAttentions 로 위임 — sim(여기)과 live projector 미러가 공유(face2-inbound-live §1.1).
|
|
369
|
+
const out = deriveAttentions({ movers: [...this.movers.values()], nodes: [...this.nodes.values()], orders: [...this.orders.values()] }, this._acked);
|
|
370
|
+
// 확인(ack) 프루닝 — 사라진 조건은 ack 해제(재발 시 다시 active). 엔진 상태 변이라 여기 유지(marking 은 deriveAttentions).
|
|
371
|
+
const present = new Set(out.map(a => a.id));
|
|
372
|
+
for (const id of [...this._acked])
|
|
373
|
+
if (!present.has(id))
|
|
374
|
+
this._acked.delete(id);
|
|
375
|
+
return out;
|
|
376
|
+
}
|
|
141
377
|
/**
|
|
142
378
|
* fork — 현재 상태를 정확히 복제한 새 엔진 (디지털트윈 본연: "현재로부터 예측").
|
|
143
379
|
* 원본(live/sim)은 계속 진행, fork 는 what-if 를 앞으로 굴려 forecast·발산(predicted vs actual) 검사에 쓴다.
|
|
@@ -266,19 +502,13 @@ export class FlowEngine {
|
|
|
266
502
|
m.goodCount++;
|
|
267
503
|
else
|
|
268
504
|
m.scrapCount++;
|
|
505
|
+
// 품질 델타 방출 — live OEE 누적기가 good/scrap 을 정확 추적(equipment.status 는 quality 미포함). WMS/YMS 는 미호출→무영향.
|
|
506
|
+
this.emitOp(OP_EVENT.quality, { moverId: m.id, good, goodCount: m.goodCount, scrapCount: m.scrapCount });
|
|
269
507
|
}
|
|
270
508
|
/** 무버 OEE(스냅샷 파생) — Availability×Performance×Quality. planned = 설비 존재 sim 시간(clockMs). */
|
|
271
509
|
oeeOf(m) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const availability = planned > 0 ? uptime / planned : 1;
|
|
275
|
-
const performance = uptime > 0 ? Math.min(1, m.runMs / uptime) : (m.runMs > 0 ? 1 : 0);
|
|
276
|
-
const totalQ = m.goodCount + m.scrapCount;
|
|
277
|
-
const quality = totalQ > 0 ? m.goodCount / totalQ : 1;
|
|
278
|
-
return {
|
|
279
|
-
availability, performance, quality, overall: availability * performance * quality,
|
|
280
|
-
runMs: m.runMs, setupMs: m.setupMs, downMs: m.downMs, idleMs: Math.max(0, uptime - m.runMs), goodCount: m.goodCount, scrapCount: m.scrapCount
|
|
281
|
-
};
|
|
510
|
+
// 순수 공식 computeOee 로 위임 — sim(여기)과 live 가 같은 계산 층 공유. planned 는 hold 제외(계획정지 OEE 무영향).
|
|
511
|
+
return computeOee(m, this.clockMs);
|
|
282
512
|
}
|
|
283
513
|
/** 정책에 넘길 특정 타입 노드의 관측 뷰 — 예약(그 노드로 향하는 in-flight task) 포함. */
|
|
284
514
|
slotViews(nodeType) {
|
|
@@ -304,7 +534,7 @@ export class FlowEngine {
|
|
|
304
534
|
for (const h of this.handlers)
|
|
305
535
|
h(e);
|
|
306
536
|
}
|
|
307
|
-
emitTask(t) { this.emitOp(OP_EVENT.task, { taskId: t.id, kind: t.kind, status: t.status, fromNode: t.fromNode, toNode: t.toNode, itemRefs: [t.itemEpc], resourceRef: t.resource ?? undefined }); }
|
|
537
|
+
emitTask(t) { this.emitOp(OP_EVENT.task, { taskId: t.id, orderId: t.orderId, kind: t.kind, status: t.status, fromNode: t.fromNode, toNode: t.toNode, itemRefs: [t.itemEpc], resourceRef: t.resource ?? undefined }); }
|
|
308
538
|
emitMover(m, motion) { this.emitOp(OP_EVENT.equipment, { moverId: m.id, kind: m.kind, status: m.status, location: m.location, motion }); }
|
|
309
539
|
emitOrder(o) { this.emitOp(OP_EVENT.order, { orderId: o.id, kind: o.kind, status: o.status, requested: o.requested, fulfilled: o.fulfilled, held: o.held }); }
|
|
310
540
|
// ── 내부 mechanics ─────────────────────────────────────────────────────────
|
|
@@ -321,19 +551,27 @@ export class FlowEngine {
|
|
|
321
551
|
*/
|
|
322
552
|
processFailures(dt) {
|
|
323
553
|
for (const m of this.movers.values()) {
|
|
324
|
-
if (m.mtbfMs === undefined)
|
|
325
|
-
continue;
|
|
326
554
|
if (m.status === 'down') {
|
|
555
|
+
// down 회계 + 수리 — 확률적 고장·강제 고장(resource.down) 공통. repairUntilMs 도래 시 복구.
|
|
327
556
|
m.downMs += dt;
|
|
328
|
-
if (this.clockMs >=
|
|
329
|
-
m.status = m.taskId ? 'busy' : 'idle'; //
|
|
557
|
+
if (m.repairUntilMs != null && this.clockMs >= m.repairUntilMs) {
|
|
558
|
+
m.status = m.taskId ? 'busy' : 'idle'; // 진행중 task 있으면 재개
|
|
330
559
|
m.repairUntilMs = undefined;
|
|
331
|
-
|
|
560
|
+
if (m.mtbfMs !== undefined)
|
|
561
|
+
m.nextFailureMs = this.clockMs + this.sampleExp(m.mtbfMs);
|
|
332
562
|
this.emitMover(m);
|
|
333
563
|
}
|
|
564
|
+
continue;
|
|
334
565
|
}
|
|
335
|
-
|
|
336
|
-
|
|
566
|
+
if (m.held) {
|
|
567
|
+
// 계획 정지(resource.hold) — 정지 시간 누적(OEE planned 에서 제외). 정지 중엔 고장 스케줄 진행 안 함.
|
|
568
|
+
if (m.status === 'idle')
|
|
569
|
+
m.holdMs = (m.holdMs ?? 0) + dt;
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
// 확률적 고장 — mtbf 지정 무버만(미지정=고장 없음, rng 무소비 → byte-identical baseline).
|
|
573
|
+
if (m.mtbfMs !== undefined && this.clockMs >= (m.nextFailureMs ?? Infinity)) {
|
|
574
|
+
m.status = 'down';
|
|
337
575
|
m.repairUntilMs = this.clockMs + this.sampleExp(m.mttrMs ?? m.mtbfMs);
|
|
338
576
|
m.nextFailureMs = undefined;
|
|
339
577
|
this.emitMover(m);
|
|
@@ -344,10 +582,13 @@ export class FlowEngine {
|
|
|
344
582
|
generate() {
|
|
345
583
|
for (const g of this.gens) {
|
|
346
584
|
while (this.clockMs >= g.nextMs) {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
585
|
+
// 자극 클래스(공급/수요)로 hook 분기 — 도메인 kind 라벨이 아니라 stimulus 로 라우팅(무방언).
|
|
586
|
+
// stimulus 미지정 시 레거시 kind 로 추론(하위호환). 도메인은 kind 를 자유 명명하고 stimulus 로 분류.
|
|
587
|
+
const stimulus = g.spec.stimulus ?? (g.spec.kind === 'outbound-order' ? 'order' : 'arrival');
|
|
588
|
+
if (stimulus === 'order')
|
|
350
589
|
this.onOrder(g.spec);
|
|
590
|
+
else
|
|
591
|
+
this.onArrival(g.spec);
|
|
351
592
|
g.nextMs += this.intervalMs(g.spec);
|
|
352
593
|
}
|
|
353
594
|
}
|
|
@@ -369,7 +610,7 @@ export class FlowEngine {
|
|
|
369
610
|
continue;
|
|
370
611
|
}
|
|
371
612
|
// resourceType 있으면 그 kind 무버만; 없으면 아무 유휴 무버.
|
|
372
|
-
const mover = [...this.movers.values()].find(m => m.status === 'idle' && (t.resourceType === undefined || m.kind === t.resourceType));
|
|
613
|
+
const mover = [...this.movers.values()].find(m => m.status === 'idle' && !m.held && (t.resourceType === undefined || m.kind === t.resourceType));
|
|
373
614
|
if (!mover)
|
|
374
615
|
continue; // 맞는 유휴 자원 없음 → 다음 task(break 아님: 다른 타입은 가용할 수 있음)
|
|
375
616
|
// 체인지오버: task 의 changeoverKey 가 무버 직전 키와 다르면 셋업 부착(첫 작업은 셋업 없음).
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from './contract.ts';
|
|
2
|
+
export * from './domain-definition.ts';
|
|
2
3
|
export * from './counterfactual.ts';
|
|
3
4
|
export * from './forecast.ts';
|
|
4
5
|
export * from './twin-observer.ts';
|
|
@@ -6,6 +7,7 @@ export * from './event-journal.ts';
|
|
|
6
7
|
export * from './divergence.ts';
|
|
7
8
|
export * from './epcis.ts';
|
|
8
9
|
export * from './wms-profile.ts';
|
|
10
|
+
export * from './capability.ts';
|
|
9
11
|
export * from './domain-catalog.ts';
|
|
10
12
|
export * from './allocation-policy.ts';
|
|
11
13
|
export * from './duration-estimator.ts';
|
|
@@ -17,4 +19,5 @@ export * from './mes-profile.ts';
|
|
|
17
19
|
export * from './flow-engine.ts';
|
|
18
20
|
export { WmsKernel } from './kernel.ts';
|
|
19
21
|
export { YmsKernel } from './yms-kernel.ts';
|
|
20
|
-
export { MesKernel, MES_PART_GTINS, MES_PRODUCT_GTINS } from './mes-kernel.ts';
|
|
22
|
+
export { MesKernel, MES_PART_GTINS, MES_PRODUCT_GTINS, MES_PRODUCTS } from './mes-kernel.ts';
|
|
23
|
+
export type { MesDefinitionSpec } from './mes-kernel.ts';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from "./contract.js";
|
|
2
|
+
export * from "./domain-definition.js";
|
|
2
3
|
export * from "./counterfactual.js";
|
|
3
4
|
export * from "./forecast.js";
|
|
4
5
|
export * from "./twin-observer.js";
|
|
@@ -6,6 +7,7 @@ export * from "./event-journal.js";
|
|
|
6
7
|
export * from "./divergence.js";
|
|
7
8
|
export * from "./epcis.js";
|
|
8
9
|
export * from "./wms-profile.js";
|
|
10
|
+
export * from "./capability.js";
|
|
9
11
|
export * from "./domain-catalog.js";
|
|
10
12
|
export * from "./allocation-policy.js";
|
|
11
13
|
export * from "./duration-estimator.js";
|
|
@@ -17,4 +19,4 @@ export * from "./mes-profile.js";
|
|
|
17
19
|
export * from "./flow-engine.js";
|
|
18
20
|
export { WmsKernel } from "./kernel.js";
|
|
19
21
|
export { YmsKernel } from "./yms-kernel.js";
|
|
20
|
-
export { MesKernel, MES_PART_GTINS, MES_PRODUCT_GTINS } from "./mes-kernel.js";
|
|
22
|
+
export { MesKernel, MES_PART_GTINS, MES_PRODUCT_GTINS, MES_PRODUCTS } from "./mes-kernel.js";
|
package/dist/kernel.js
CHANGED
|
@@ -72,7 +72,7 @@ export class WmsKernel extends FlowEngine {
|
|
|
72
72
|
const a = cmd.args;
|
|
73
73
|
const lines = a?.lines ?? (a?.gtin ? [{ gtin: a.gtin, qty: a.qty ?? 1 }] : []);
|
|
74
74
|
if (lines.length === 0)
|
|
75
|
-
return { commandId: cmd.commandId, accepted: false, error: 'order.release: gtin
|
|
75
|
+
return { commandId: cmd.commandId, accepted: false, errorCode: 'order-release-needs-lines', error: 'order.release: gtin or lines required' };
|
|
76
76
|
this.createSalesOrder(lines);
|
|
77
77
|
return { commandId: cmd.commandId, accepted: true };
|
|
78
78
|
}
|
package/dist/mes-kernel.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { GeneratorSpec } from './contract.ts';
|
|
1
|
+
import type { GeneratorSpec, Command, CommandAck } 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
|
+
import type { DomainDefinition } from './domain-definition.ts';
|
|
5
6
|
/** 편의 — 시나리오 skuMix 로 쓸 부품 클래스. */
|
|
6
7
|
export declare const MES_PART_GTINS: {
|
|
7
8
|
partA: string;
|
|
@@ -12,17 +13,60 @@ export declare const MES_PRODUCT_GTINS: {
|
|
|
12
13
|
p1: string;
|
|
13
14
|
p2: string;
|
|
14
15
|
};
|
|
16
|
+
/** 레거시 MES 제품 목록(gtin+라벨) — mes.changeover 대상 제품 소싱용. 정의-구동 전환 시 recipe 출력으로 교체. */
|
|
17
|
+
export declare const MES_PRODUCTS: {
|
|
18
|
+
gtin: string;
|
|
19
|
+
label: string;
|
|
20
|
+
}[];
|
|
21
|
+
/**
|
|
22
|
+
* 정의-구동 모드 스펙 — 커널이 도메인 정의(추상)를 소비. 구체 gtin 은 여기서:
|
|
23
|
+
* 자재 키 → GS1 item reference 바인딩 + company prefix (정의는 gtin 을 모른다, plan §게이트 (b)).
|
|
24
|
+
*/
|
|
25
|
+
export interface MesDefinitionSpec {
|
|
26
|
+
definition: DomainDefinition;
|
|
27
|
+
/** 자재 키 → GS1 item reference. 구체 gtin = sgtin(companyPrefix, itemRef). */
|
|
28
|
+
binding: Record<string, string>;
|
|
29
|
+
companyPrefix: string;
|
|
30
|
+
/** 사용할 recipe 키(미지정 시 첫 recipe). */
|
|
31
|
+
recipeKey?: string;
|
|
32
|
+
}
|
|
15
33
|
export declare class MesKernel extends FlowEngine {
|
|
16
34
|
private wipSeq;
|
|
17
35
|
private prodSeq;
|
|
18
|
-
|
|
36
|
+
/** 정의-구동 모드(선택). 미지정 시 레거시 하드코딩 경로 — byte-identical. */
|
|
37
|
+
private mesSpec?;
|
|
38
|
+
constructor(tenantId: string, policy?: AllocationPolicy, mesSpec?: MesDefinitionSpec);
|
|
19
39
|
private productOf;
|
|
40
|
+
/**
|
|
41
|
+
* MES 도메인 커맨드(Tier 2) — mes.changeover: 설비를 제품 gtin 으로 강제 전환.
|
|
42
|
+
* 자동 체인지오버(task.changeoverKey 상이 시 셋업)의 수동 버전 — 운영자가 사전 전환(툴링 교체) 지시.
|
|
43
|
+
* 이미 그 제품이면 no-op, 아니면 셋업(SETUP_MS, OEE 가용성 손실) + lastChangeoverKey 각인
|
|
44
|
+
* (이후 그 제품 task 는 자동 셋업 생략). command → 변이 → State 델타(폐루프).
|
|
45
|
+
*/
|
|
46
|
+
protected handleCommand(cmd: Command): CommandAck;
|
|
20
47
|
/** 부품 수령(다품종) — skuMix 의 gtin 으로 부품 종류 결정. */
|
|
21
48
|
protected onArrival(spec: GeneratorSpec): void;
|
|
22
49
|
/** 작업지시 — 제품 2종 교대(체인지오버 유발). 제품 gtin 을 오더에 기록. */
|
|
23
50
|
protected onOrder(_spec: GeneratorSpec): void;
|
|
24
|
-
/** 할당 — 제품 BOM 각 라인의 부품 예약(하나라도 부족하면 대기) +
|
|
51
|
+
/** 할당 — 제품 BOM 각 라인의 부품 예약(하나라도 부족하면 대기) + 라우트 첫 스테이션(절단) 태스크. */
|
|
25
52
|
protected allocate(o: FlowOrder): void;
|
|
26
|
-
/**
|
|
53
|
+
/** 라우트 스테이션 태스크 발행(공통) — 제자리 가공(process), 이종 자원, 제품 전환 셋업. */
|
|
54
|
+
private emitStation;
|
|
55
|
+
/** op 완료 = 변환. 라우트 인덱스로 분기: 첫=BOM 소비→WIP, 중간=WIP→WIP, 마지막(조립)=WIP→완성차(수율→OEE 품질). */
|
|
27
56
|
protected onTaskComplete(t: FlowTask): void;
|
|
57
|
+
private recipeDef;
|
|
58
|
+
/** 자재 키 → 구체 gtin 클래스(idpat). 구체 식별은 바인딩+prefix 로 인스턴스가 주입. */
|
|
59
|
+
private classOf;
|
|
60
|
+
private serialOf;
|
|
61
|
+
/** recipe.route → 오퍼레이션 시퀀스 해소. */
|
|
62
|
+
private routeOps;
|
|
63
|
+
/** 정의 모드 수령 — skuMix gtin 이 레시피 입력 자재면 raw-store 에 생성. */
|
|
64
|
+
private onArrivalDef;
|
|
65
|
+
/** 정의 모드 작업지시 — 레시피 산출물 1개. */
|
|
66
|
+
private onOrderDef;
|
|
67
|
+
/** 정의 모드 할당 — 레시피 입력 BOM 전량 확보 후 첫 라우트 스텝 태스크. */
|
|
68
|
+
private allocateDef;
|
|
69
|
+
private emitStationDef;
|
|
70
|
+
/** 정의 모드 완료 — 라우트 인덱스: 중간=WIP 변환+다음 스텝, 마지막=완제품(수율). */
|
|
71
|
+
private onTaskCompleteDef;
|
|
28
72
|
}
|