@operato/twin-kernel 0.0.5 → 0.1.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/dist/contract.d.ts +19 -7
- package/dist/domain-catalog.js +4 -3
- package/dist/flow-engine.js +28 -30
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/kernel.js +1 -1
- package/dist/mes-kernel.js +2 -2
- package/dist/mes-profile.js +13 -12
- package/dist/task-fold.d.ts +56 -0
- package/dist/task-fold.js +60 -0
- package/dist/wms-profile.js +3 -3
- package/dist/yms-profile.js +2 -3
- package/dist-cjs/index.cjs +89 -56
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -92,28 +92,34 @@ export interface Attention {
|
|
|
92
92
|
kind: string;
|
|
93
93
|
severity: AttentionSeverity;
|
|
94
94
|
state?: AttentionState;
|
|
95
|
-
title: string;
|
|
96
|
-
detail?: string;
|
|
97
95
|
anchor: {
|
|
98
96
|
nodeId?: string;
|
|
99
97
|
moverId?: string;
|
|
100
98
|
orderId?: string;
|
|
101
99
|
};
|
|
102
100
|
since?: number;
|
|
103
|
-
|
|
101
|
+
/**
|
|
102
|
+
* 표현용 원시 파라미터(언어 중립). 커널은 사람이 읽는 문장을 만들지 않는다 — kind + params 만 방출하고
|
|
103
|
+
* title/detail/rationale 는 표현계층(클라 i18next 템플릿)이 kind 로 키를 골라 params 를 보간해 렌더.
|
|
104
|
+
* 예: bottleneck → { nodeId, occupancy, capacity, ratioPct, saturated }.
|
|
105
|
+
*/
|
|
106
|
+
params?: Record<string, string | number>;
|
|
104
107
|
recommendedActions?: RecommendedAction[];
|
|
105
108
|
suggestedAction?: {
|
|
109
|
+
code: string;
|
|
106
110
|
command: string;
|
|
107
111
|
args?: unknown;
|
|
108
|
-
label: string;
|
|
109
112
|
};
|
|
110
113
|
}
|
|
111
|
-
/**
|
|
114
|
+
/**
|
|
115
|
+
* 조치방향 — code=안정 조치 키(언어 중립). command 있으면 원클릭 실행, 없으면 권고.
|
|
116
|
+
* 라벨·힌트(사람 언어)는 표현계층이 code 로 렌더(커널은 문장 미보유). command 보유 조치는 code=command 문자열,
|
|
117
|
+
* 권고만(hint only)이던 조치는 'advice.*' 코드.
|
|
118
|
+
*/
|
|
112
119
|
export interface RecommendedAction {
|
|
113
|
-
|
|
120
|
+
code: string;
|
|
114
121
|
command?: string;
|
|
115
122
|
args?: unknown;
|
|
116
|
-
hint?: string;
|
|
117
123
|
}
|
|
118
124
|
export interface StateSnapshot {
|
|
119
125
|
revision: number;
|
|
@@ -135,6 +141,12 @@ export interface Command<T = unknown> {
|
|
|
135
141
|
export interface CommandAck {
|
|
136
142
|
commandId: string;
|
|
137
143
|
accepted: boolean;
|
|
144
|
+
/**
|
|
145
|
+
* 거절 사유 — 언어 중립. errorCode(안정 코드) + errorParams(원시값)로 방출하고 사람 언어는
|
|
146
|
+
* 표현계층(클라 i18next)이 렌더한다(무방언·다국어, attention 과 동형). error 는 개발자/로그용 영어 폴백.
|
|
147
|
+
*/
|
|
148
|
+
errorCode?: string;
|
|
149
|
+
errorParams?: Record<string, string | number>;
|
|
138
150
|
error?: string;
|
|
139
151
|
}
|
|
140
152
|
/**
|
package/dist/domain-catalog.js
CHANGED
|
@@ -3,9 +3,10 @@ import { YMS_TYPES } from "./yms-profile.js";
|
|
|
3
3
|
import { MES_TYPES } from "./mes-profile.js";
|
|
4
4
|
const nodeKeys = (types) => types.filter(t => t.role === 'node').map(t => t.key);
|
|
5
5
|
export const DOMAIN_CATALOG = {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
// label 은 언어 중립 i18n 키(twin.system.<code>) — 사람 언어는 표현계층이 렌더(L2).
|
|
7
|
+
wms: { system: 'wms', label: 'twin.system.wms', types: WMS_TYPES, nodeTypes: nodeKeys(WMS_TYPES) },
|
|
8
|
+
yms: { system: 'yms', label: 'twin.system.yms', types: YMS_TYPES, nodeTypes: nodeKeys(YMS_TYPES) },
|
|
9
|
+
mes: { system: 'mes', label: 'twin.system.mes', types: MES_TYPES, nodeTypes: nodeKeys(MES_TYPES) }
|
|
9
10
|
};
|
|
10
11
|
export const DOMAIN_SYSTEMS = ['wms', 'yms', 'mes'];
|
|
11
12
|
/** 타입 키 → 능력 프로파일(커널 SSOT). 호스트가 라이브 페이로드에 투영, 컴포넌트가 능력을 렌더. */
|
package/dist/flow-engine.js
CHANGED
|
@@ -29,19 +29,20 @@ function mulberry32(seed) {
|
|
|
29
29
|
* sim/live 가 같은 임계값·규칙을 쓴다. good/scrap 미제공(관측 상태에 없음)이면 scrap-high 는 자연 스킵(metric 층 갭).
|
|
30
30
|
*/
|
|
31
31
|
export function deriveAttentions(view, acked) {
|
|
32
|
+
// 언어 중립: kind + params(원시값) + 조치 code 만 방출. 사람이 읽는 title/detail/rationale/라벨은
|
|
33
|
+
// 표현계층(클라 i18next)이 kind/code 로 렌더한다(무방언·다국어 — design/plans/i18n.md L3).
|
|
32
34
|
const out = [];
|
|
33
35
|
for (const m of view.movers) {
|
|
34
36
|
if (m.status === 'down') {
|
|
35
|
-
const where = m.location ?? '해당 공정';
|
|
36
37
|
out.push({
|
|
37
|
-
id: `breakdown:${m.id}`, kind: 'breakdown', severity: 'critical',
|
|
38
|
+
id: `breakdown:${m.id}`, kind: 'breakdown', severity: 'critical',
|
|
38
39
|
anchor: { moverId: m.id, nodeId: m.location },
|
|
39
|
-
|
|
40
|
+
params: { moverId: m.id, ...(m.location ? { nodeId: m.location } : {}) },
|
|
40
41
|
recommendedActions: [
|
|
41
|
-
{
|
|
42
|
-
{
|
|
42
|
+
{ code: 'act.repair', command: CMD.resourceRepair, args: { resourceId: m.id } },
|
|
43
|
+
{ code: 'act.hold-until-repair', command: CMD.resourceHold, args: { resourceId: m.id } }
|
|
43
44
|
],
|
|
44
|
-
suggestedAction: { command: CMD.resourceRepair, args: { resourceId: m.id }
|
|
45
|
+
suggestedAction: { code: 'act.repair', command: CMD.resourceRepair, args: { resourceId: m.id } }
|
|
45
46
|
});
|
|
46
47
|
}
|
|
47
48
|
}
|
|
@@ -52,13 +53,10 @@ export function deriveAttentions(view, acked) {
|
|
|
52
53
|
const saturated = r >= 1;
|
|
53
54
|
out.push({
|
|
54
55
|
id: `bottleneck:${n.id}`, kind: 'bottleneck', severity: saturated ? 'high' : 'medium',
|
|
55
|
-
title: `${saturated ? '병목' : '혼잡'} · ${n.id} 점유 ${n.occupancy}/${n.capacity}`,
|
|
56
56
|
anchor: { nodeId: n.id },
|
|
57
|
-
|
|
58
|
-
recommendedActions: [
|
|
59
|
-
|
|
60
|
-
{ label: '하류 우선 처리', hint: '적체 해소를 위해 배출 우선순위 조정' }
|
|
61
|
-
]
|
|
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.* 코드로 렌더
|
|
62
60
|
});
|
|
63
61
|
}
|
|
64
62
|
}
|
|
@@ -70,25 +68,24 @@ export function deriveAttentions(view, acked) {
|
|
|
70
68
|
if (rate >= 0.15)
|
|
71
69
|
out.push({
|
|
72
70
|
id: `scrap:${m.id}`, kind: 'scrap-high', severity: rate >= 0.3 ? 'high' : 'medium',
|
|
73
|
-
title: `불량률 ${Math.round(rate * 100)}% · ${m.id}`, detail: `양품 ${m.goodCount} / 불량 ${m.scrapCount}`,
|
|
74
71
|
anchor: { moverId: m.id, nodeId: m.location },
|
|
75
|
-
|
|
72
|
+
params: { moverId: m.id, goodCount: m.goodCount ?? 0, scrapCount: m.scrapCount ?? 0, ratePct: Math.round(rate * 100) },
|
|
76
73
|
recommendedActions: [
|
|
77
|
-
{
|
|
78
|
-
{
|
|
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 } }
|
|
79
76
|
],
|
|
80
|
-
suggestedAction: { command: CMD.resourceHold, args: { resourceId: m.id }
|
|
77
|
+
suggestedAction: { code: 'act.hold-for-inspection', command: CMD.resourceHold, args: { resourceId: m.id } }
|
|
81
78
|
});
|
|
82
79
|
}
|
|
83
80
|
}
|
|
84
81
|
for (const o of view.orders) {
|
|
85
82
|
if (o.held)
|
|
86
83
|
out.push({
|
|
87
|
-
id: `hold:${o.id}`, kind: 'hold', severity: 'medium',
|
|
84
|
+
id: `hold:${o.id}`, kind: 'hold', severity: 'medium',
|
|
88
85
|
anchor: { orderId: o.id },
|
|
89
|
-
|
|
90
|
-
recommendedActions: [{
|
|
91
|
-
suggestedAction: { command: CMD.orderResume, args: { 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 } }
|
|
92
89
|
});
|
|
93
90
|
}
|
|
94
91
|
if (acked)
|
|
@@ -221,14 +218,15 @@ export class FlowEngine {
|
|
|
221
218
|
_acked = new Set(); // 확인(ack)된 주목 신호 id — 조건 지속돼도 acknowledged 로 표시(재발 시 재활성)
|
|
222
219
|
dispatch(cmd) {
|
|
223
220
|
const ok = () => ({ commandId: cmd.commandId, accepted: true });
|
|
224
|
-
|
|
221
|
+
// 거절 사유 = 언어 중립 코드 + 원시 파라미터. error 는 영어 폴백(로그·개발자용).
|
|
222
|
+
const fail = (errorCode, errorParams) => ({ commandId: cmd.commandId, accepted: false, errorCode, errorParams, error: errorCode });
|
|
225
223
|
switch (cmd.type) {
|
|
226
224
|
case CMD.orderHold:
|
|
227
225
|
case CMD.orderResume: {
|
|
228
226
|
const orderId = cmd.args?.orderId;
|
|
229
227
|
const order = orderId ? this.orders.get(orderId) : undefined;
|
|
230
228
|
if (!order)
|
|
231
|
-
return fail(
|
|
229
|
+
return fail('order-not-found', { orderId: orderId ?? '' });
|
|
232
230
|
order.held = cmd.type === CMD.orderHold;
|
|
233
231
|
this.emitOrder(order);
|
|
234
232
|
return ok();
|
|
@@ -244,7 +242,7 @@ export class FlowEngine {
|
|
|
244
242
|
case CMD.resourceResume: {
|
|
245
243
|
const m = this.movers.get(cmd.args?.resourceId ?? '');
|
|
246
244
|
if (!m)
|
|
247
|
-
return fail('resource
|
|
245
|
+
return fail('resource-not-found');
|
|
248
246
|
m.held = cmd.type === CMD.resourceHold; // 계획 정지 → 배정 스킵
|
|
249
247
|
this.emitMover(m);
|
|
250
248
|
return ok();
|
|
@@ -253,7 +251,7 @@ export class FlowEngine {
|
|
|
253
251
|
const a = cmd.args;
|
|
254
252
|
const m = this.movers.get(a?.resourceId ?? '');
|
|
255
253
|
if (!m)
|
|
256
|
-
return fail('resource
|
|
254
|
+
return fail('resource-not-found');
|
|
257
255
|
if (m.status !== 'down') {
|
|
258
256
|
m.status = 'down'; // 비계획 고장 주입 — 기존 고장 machinery(processFailures)가 downMs 누적·수리 처리
|
|
259
257
|
m.repairUntilMs = this.clockMs + (Number(a?.durationMs) || m.mttrMs || 60_000);
|
|
@@ -264,7 +262,7 @@ export class FlowEngine {
|
|
|
264
262
|
case CMD.resourceRepair: {
|
|
265
263
|
const m = this.movers.get(cmd.args?.resourceId ?? '');
|
|
266
264
|
if (!m)
|
|
267
|
-
return fail('resource
|
|
265
|
+
return fail('resource-not-found');
|
|
268
266
|
if (m.status === 'down') {
|
|
269
267
|
m.status = m.taskId ? 'busy' : 'idle'; // 진행중 task 있으면 재개(고장모델 수리 로직과 동형)
|
|
270
268
|
m.repairUntilMs = undefined;
|
|
@@ -277,7 +275,7 @@ export class FlowEngine {
|
|
|
277
275
|
case CMD.resourceResetMetrics: {
|
|
278
276
|
const m = this.movers.get(cmd.args?.resourceId ?? '');
|
|
279
277
|
if (!m)
|
|
280
|
-
return fail('resource
|
|
278
|
+
return fail('resource-not-found');
|
|
281
279
|
m.runMs = 0;
|
|
282
280
|
m.setupMs = 0;
|
|
283
281
|
m.downMs = 0;
|
|
@@ -293,9 +291,9 @@ export class FlowEngine {
|
|
|
293
291
|
// 새 무버는 즉시 배정 대상(다음 tick). 유일 id 생성(충돌 회피). 좌표/persistence 는 호스트 몫(커널=위상만).
|
|
294
292
|
const a = cmd.args;
|
|
295
293
|
if (!a?.kind)
|
|
296
|
-
return fail('kind
|
|
294
|
+
return fail('kind-required');
|
|
297
295
|
if (!a?.homeNode || !this.nodes.has(a.homeNode))
|
|
298
|
-
return fail(
|
|
296
|
+
return fail('home-node-not-found', { homeNode: a?.homeNode ?? '' });
|
|
299
297
|
const count = Math.max(1, Math.min(50, Number(a.count) || 1));
|
|
300
298
|
let seq = this.movers.size;
|
|
301
299
|
for (let i = 0; i < count; i++) {
|
|
@@ -315,7 +313,7 @@ export class FlowEngine {
|
|
|
315
313
|
}
|
|
316
314
|
/** 도메인 커맨드 처리(order.release 등). 기본은 거절 — 도메인이 override. */
|
|
317
315
|
handleCommand(cmd) {
|
|
318
|
-
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}` };
|
|
319
317
|
}
|
|
320
318
|
scenario = {
|
|
321
319
|
load: (def) => {
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export * from './domain-catalog.ts';
|
|
|
12
12
|
export * from './allocation-policy.ts';
|
|
13
13
|
export * from './duration-estimator.ts';
|
|
14
14
|
export * from './state-projector.ts';
|
|
15
|
+
export * from './task-fold.ts';
|
|
15
16
|
export * from './face2-adapter.ts';
|
|
16
17
|
export * from './runtime.ts';
|
|
17
18
|
export * from './yms-profile.ts';
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export * from "./domain-catalog.js";
|
|
|
12
12
|
export * from "./allocation-policy.js";
|
|
13
13
|
export * from "./duration-estimator.js";
|
|
14
14
|
export * from "./state-projector.js";
|
|
15
|
+
export * from "./task-fold.js";
|
|
15
16
|
export * from "./face2-adapter.js";
|
|
16
17
|
export * from "./runtime.js";
|
|
17
18
|
export * from "./yms-profile.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.js
CHANGED
|
@@ -65,10 +65,10 @@ export class MesKernel extends FlowEngine {
|
|
|
65
65
|
if (cmd.type === MES_CMD.changeover) {
|
|
66
66
|
const a = cmd.args;
|
|
67
67
|
if (!a?.resourceId || !a?.gtin)
|
|
68
|
-
return { commandId: cmd.commandId, accepted: false, error: 'mes.changeover: resourceId
|
|
68
|
+
return { commandId: cmd.commandId, accepted: false, errorCode: 'changeover-needs-args', error: 'mes.changeover: resourceId and gtin required' };
|
|
69
69
|
const m = this.movers.get(a.resourceId);
|
|
70
70
|
if (!m)
|
|
71
|
-
return { commandId: cmd.commandId, accepted: false, error: `resource
|
|
71
|
+
return { commandId: cmd.commandId, accepted: false, errorCode: 'resource-not-found', errorParams: { resourceId: a.resourceId }, error: `resource-not-found: ${a.resourceId}` };
|
|
72
72
|
if (m.lastChangeoverKey !== a.gtin) {
|
|
73
73
|
m.setupMs += SETUP_MS; // 셋업 = OEE 가용성 손실
|
|
74
74
|
m.lastChangeoverKey = a.gtin;
|
package/dist/mes-profile.js
CHANGED
|
@@ -17,18 +17,19 @@ export const MES_NODE_TYPES = ['raw-store', 'cut-station', 'weld-station', 'pain
|
|
|
17
17
|
* 트윈 타입 서술(ADR-0018 확장) — 노드 키는 MES_NODE_TYPES 단일 출처에서 파생 + 무버(자원) 타입 추가.
|
|
18
18
|
* MES=ISA-95 앵커: 스테이션=WorkCenter, 저장소=bizLocation. 무버(가공설비)=Equipment/자산(GIAI). key 는 flow resourceType 과 일치.
|
|
19
19
|
*/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'
|
|
20
|
+
// label 은 언어 중립 i18n 키(twin.type.<key>) — cls(표준 클래스)만 노드별 메타로 유지. 사람 언어는 표현계층(L2).
|
|
21
|
+
const MES_NODE_CLS = {
|
|
22
|
+
'raw-store': { epcis: 'bizLocation' },
|
|
23
|
+
'cut-station': { isa95: 'WorkCenter', epcis: 'bizLocation' },
|
|
24
|
+
'weld-station': { isa95: 'WorkCenter', epcis: 'bizLocation' },
|
|
25
|
+
'paint-booth': { isa95: 'WorkCenter', epcis: 'bizLocation' },
|
|
26
|
+
'assembly-line': { isa95: 'WorkCenter', epcis: 'bizLocation' },
|
|
27
|
+
'fg-store': { epcis: 'bizLocation' }
|
|
27
28
|
};
|
|
28
29
|
export const MES_TYPES = [
|
|
29
|
-
...MES_NODE_TYPES.map((k) => ({ key: k, role: 'node', label:
|
|
30
|
-
{ key: 'cutter', role: 'mover', label: '
|
|
31
|
-
{ key: 'welder', role: 'mover', label: '
|
|
32
|
-
{ key: 'painter', role: 'mover', label: '
|
|
33
|
-
{ key: 'assembler', role: 'mover', label: '
|
|
30
|
+
...MES_NODE_TYPES.map((k) => ({ key: k, role: 'node', label: `twin.type.${k}`, standardClass: MES_NODE_CLS[k] ?? {}, identity: { scheme: 'gs1:SGLN' }, capabilities: ['storable'] })),
|
|
31
|
+
{ key: 'cutter', role: 'mover', label: 'twin.type.cutter', standardClass: { isa95: 'Equipment', iso55000: 'Asset' }, identity: { scheme: 'gs1:GIAI' }, capabilities: ['processable', 'operable'] },
|
|
32
|
+
{ key: 'welder', role: 'mover', label: 'twin.type.welder', standardClass: { isa95: 'Equipment', iso55000: 'Asset' }, identity: { scheme: 'gs1:GIAI' }, capabilities: ['processable', 'operable'] },
|
|
33
|
+
{ key: 'painter', role: 'mover', label: 'twin.type.painter', standardClass: { isa95: 'Equipment', iso55000: 'Asset' }, identity: { scheme: 'gs1:GIAI' }, capabilities: ['processable', 'operable'] },
|
|
34
|
+
{ key: 'assembler', role: 'mover', label: 'twin.type.assembler', standardClass: { isa95: 'Equipment', iso55000: 'Asset' }, identity: { scheme: 'gs1:GIAI' }, capabilities: ['processable', 'operable'] }
|
|
34
35
|
];
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { TaskStatusDelta } from './contract.ts';
|
|
2
|
+
/**
|
|
3
|
+
* 되읽을 저널 한 줄 — 필요한 것만.
|
|
4
|
+
*
|
|
5
|
+
* `data`(정본 봉투) 와 `payload`(호스트가 감싸 저장한 형태) 를 모두 받는다. 저장 계층마다 감싸는 방식이
|
|
6
|
+
* 달라 소비처가 각자 벗기고 있었고, 그 벗기는 규칙이 또 갈라졌다.
|
|
7
|
+
*/
|
|
8
|
+
export interface TaskDeltaRow {
|
|
9
|
+
/** ISO 시각. 파싱 불가하면 그 줄은 버린다(조용히 0 으로 세지 않는다). */
|
|
10
|
+
eventTime?: string;
|
|
11
|
+
data?: Partial<TaskStatusDelta>;
|
|
12
|
+
payload?: {
|
|
13
|
+
data?: Partial<TaskStatusDelta>;
|
|
14
|
+
} | Partial<TaskStatusDelta>;
|
|
15
|
+
}
|
|
16
|
+
/** 작업에 붙어 있는 축의 값 — 어느 전이에서 왔든 모은다. */
|
|
17
|
+
export interface TaskFacets {
|
|
18
|
+
/** 수행 자원(무버). 설계상 자원을 쓰지 않는 공정(체류)에서는 없는 것이 정상이다. */
|
|
19
|
+
resource?: string;
|
|
20
|
+
/** 작업 종류(커널 어휘 — 사람이 읽는 라벨은 표현 계층의 몫). */
|
|
21
|
+
kind?: string;
|
|
22
|
+
/** 소속 오더. **없으면 없는 것이다** — 다른 식별자로 대체하지 않는다. */
|
|
23
|
+
order?: string;
|
|
24
|
+
/** 가장 최근 도착 지점. 진행에 따라 바뀌므로 마지막 것이 사실이다. */
|
|
25
|
+
node?: string;
|
|
26
|
+
}
|
|
27
|
+
/** 작업 하나의 이정표 — 소비처가 이것으로 구간·지표를 만든다. */
|
|
28
|
+
export interface TaskRecord {
|
|
29
|
+
taskId: string;
|
|
30
|
+
/** 생성 시각. 같은 전이가 여러 번 오면 **마지막** 것(등록 정보 정정이 뒤에 온다). */
|
|
31
|
+
createdMs?: number;
|
|
32
|
+
/** 착수 시각. **처음** 것 — 재시도로 다시 착수 전이가 와도 실제 시작은 처음이다. */
|
|
33
|
+
startedMs?: number;
|
|
34
|
+
/** 완료 시각. **나중** 것 — 완료는 종결 상태라 작업당 하나이고 재전송이면 나중 것이 사실이다. */
|
|
35
|
+
completedMs?: number;
|
|
36
|
+
facets: TaskFacets;
|
|
37
|
+
}
|
|
38
|
+
export interface TaskFoldResult {
|
|
39
|
+
/** 작업별 기록(입력에서 처음 등장한 순서). */
|
|
40
|
+
records: TaskRecord[];
|
|
41
|
+
/** 읽어들인 줄 수(시각을 파싱한 것만). 저널이 비었는지 구별하는 값. */
|
|
42
|
+
rowsRead: number;
|
|
43
|
+
/** 본 시각 중 가장 늦은 것(ms). 없으면 0 — 라이브 엣지·창 기준의 재료. */
|
|
44
|
+
latestMs: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* `task.status` 델타를 작업 단위 기록으로 접는다.
|
|
48
|
+
*
|
|
49
|
+
* 입력은 **시간순일 필요가 없다**(revision 순·역순 모두 허용) — 이정표는 min/max 로 고르고 축의 값은
|
|
50
|
+
* 처음 본 것을 남기므로 순서에 의존하지 않는다. 단 `createdMs` 는 "마지막 생성 전이" 이므로 같은 작업의
|
|
51
|
+
* 생성이 두 번 오면 **입력 순서상 나중** 것이 남는다.
|
|
52
|
+
*
|
|
53
|
+
* 자원이 실리지 않은 전이도 읽는다 — 생성 전이에만 종류·오더가 실리는 구현이 있어, 그 줄을 건너뛰면
|
|
54
|
+
* 축이 통째로 빈다. "자원이 없어 그릴 수 없다" 는 판단은 소비처가 기록을 보고 한다.
|
|
55
|
+
*/
|
|
56
|
+
export declare function foldTaskRecords(rows: TaskDeltaRow[]): TaskFoldResult;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
function ms(value) {
|
|
2
|
+
if (!value)
|
|
3
|
+
return null;
|
|
4
|
+
const t = Date.parse(value);
|
|
5
|
+
return Number.isFinite(t) ? t : null;
|
|
6
|
+
}
|
|
7
|
+
/** 저장 형태 차이를 여기서 한 번만 벗긴다(소비처가 각자 벗기면 그 규칙이 갈라진다). */
|
|
8
|
+
function deltaOf(row) {
|
|
9
|
+
const p = row.payload;
|
|
10
|
+
return (row.data ?? p?.data ?? p ?? {});
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* `task.status` 델타를 작업 단위 기록으로 접는다.
|
|
14
|
+
*
|
|
15
|
+
* 입력은 **시간순일 필요가 없다**(revision 순·역순 모두 허용) — 이정표는 min/max 로 고르고 축의 값은
|
|
16
|
+
* 처음 본 것을 남기므로 순서에 의존하지 않는다. 단 `createdMs` 는 "마지막 생성 전이" 이므로 같은 작업의
|
|
17
|
+
* 생성이 두 번 오면 **입력 순서상 나중** 것이 남는다.
|
|
18
|
+
*
|
|
19
|
+
* 자원이 실리지 않은 전이도 읽는다 — 생성 전이에만 종류·오더가 실리는 구현이 있어, 그 줄을 건너뛰면
|
|
20
|
+
* 축이 통째로 빈다. "자원이 없어 그릴 수 없다" 는 판단은 소비처가 기록을 보고 한다.
|
|
21
|
+
*/
|
|
22
|
+
export function foldTaskRecords(rows) {
|
|
23
|
+
const byTask = new Map();
|
|
24
|
+
let rowsRead = 0;
|
|
25
|
+
let latestMs = 0;
|
|
26
|
+
for (const row of rows) {
|
|
27
|
+
const at = ms(row.eventTime);
|
|
28
|
+
if (at === null)
|
|
29
|
+
continue;
|
|
30
|
+
const d = deltaOf(row);
|
|
31
|
+
const id = d.taskId;
|
|
32
|
+
if (!id)
|
|
33
|
+
continue;
|
|
34
|
+
rowsRead++;
|
|
35
|
+
latestMs = Math.max(latestMs, at);
|
|
36
|
+
let rec = byTask.get(id);
|
|
37
|
+
if (!rec) {
|
|
38
|
+
rec = { taskId: id, facets: {} };
|
|
39
|
+
byTask.set(id, rec);
|
|
40
|
+
}
|
|
41
|
+
/* 축의 값 — 처음 본 것을 남긴다(작업의 종류·오더·자원은 바뀌지 않는다). 노드는 예외로 마지막 것. */
|
|
42
|
+
const f = rec.facets;
|
|
43
|
+
if (f.resource === undefined && d.resourceRef)
|
|
44
|
+
f.resource = d.resourceRef;
|
|
45
|
+
if (f.kind === undefined && d.kind)
|
|
46
|
+
f.kind = d.kind;
|
|
47
|
+
if (f.order === undefined && d.orderId)
|
|
48
|
+
f.order = d.orderId;
|
|
49
|
+
const node = d.toNode ?? d.fromNode;
|
|
50
|
+
if (node)
|
|
51
|
+
f.node = node;
|
|
52
|
+
if (d.status === 'created')
|
|
53
|
+
rec.createdMs = at;
|
|
54
|
+
else if (d.status === 'in-progress')
|
|
55
|
+
rec.startedMs = rec.startedMs != null ? Math.min(rec.startedMs, at) : at;
|
|
56
|
+
else if (d.status === 'completed')
|
|
57
|
+
rec.completedMs = rec.completedMs != null ? Math.max(rec.completedMs, at) : at;
|
|
58
|
+
}
|
|
59
|
+
return { records: [...byTask.values()], rowsRead, latestMs };
|
|
60
|
+
}
|
package/dist/wms-profile.js
CHANGED
|
@@ -22,8 +22,8 @@ export const WMS_NODE_TYPES = ['dock', 'storage', 'staging', 'dock-ship'];
|
|
|
22
22
|
* 트윈 타입 서술(ADR-0018 확장) — 노드 키는 WMS_NODE_TYPES 단일 출처에서 파생 + 무버 타입 추가.
|
|
23
23
|
* WMS=EPCIS 도메인: 로케이션=bizLocation(SGLN), 무버(지게차)=추적 오브젝트/자산(GIAI). 능력은 씬 소유라 미포함.
|
|
24
24
|
*/
|
|
25
|
-
|
|
25
|
+
// label 은 언어 중립 i18n 키(twin.type.<key>) — 사람 언어는 표현계층이 렌더(design/plans/i18n.md L2).
|
|
26
26
|
export const WMS_TYPES = [
|
|
27
|
-
...WMS_NODE_TYPES.map((k) => ({ key: k, role: 'node', label:
|
|
28
|
-
{ key: 'forklift', role: 'mover', label: '
|
|
27
|
+
...WMS_NODE_TYPES.map((k) => ({ key: k, role: 'node', label: `twin.type.${k}`, standardClass: { epcis: 'bizLocation' }, identity: { scheme: 'gs1:SGLN' }, capabilities: ['storable'] })),
|
|
28
|
+
{ key: 'forklift', role: 'mover', label: 'twin.type.forklift', standardClass: { epcis: 'object', iso55000: 'Asset' }, identity: { scheme: 'gs1:GIAI' }, capabilities: ['mobile', 'operable'] }
|
|
29
29
|
];
|
package/dist/yms-profile.js
CHANGED
|
@@ -18,8 +18,7 @@ export const YMS_NODE_TYPES = ['gate', 'yard-slot', 'dock-door', 'staging'];
|
|
|
18
18
|
* 트윈 타입 서술(ADR-0018 확장) — 노드 키는 YMS_NODE_TYPES 단일 출처에서 파생 + 무버 타입 추가.
|
|
19
19
|
* YMS=EPCIS zone: 로케이션/존=bizLocation(SGLN), 무버(야드 트랙터)=오브젝트/자산(GIAI).
|
|
20
20
|
*/
|
|
21
|
-
const YMS_NODE_LABELS = { gate: '게이트', 'yard-slot': '야드 슬롯', 'dock-door': '도크 도어', staging: '스테이징' };
|
|
22
21
|
export const YMS_TYPES = [
|
|
23
|
-
...YMS_NODE_TYPES.map((k) => ({ key: k, role: 'node', label:
|
|
24
|
-
{ key: 'hostler', role: 'mover', label: '
|
|
22
|
+
...YMS_NODE_TYPES.map((k) => ({ key: k, role: 'node', label: `twin.type.${k}`, standardClass: { epcis: 'bizLocation' }, identity: { scheme: 'gs1:SGLN' }, capabilities: ['storable'] })),
|
|
23
|
+
{ key: 'hostler', role: 'mover', label: 'twin.type.hostler', standardClass: { epcis: 'object', iso55000: 'Asset' }, identity: { scheme: 'gs1:GIAI' }, capabilities: ['mobile', 'operable'] }
|
|
25
24
|
];
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -61,6 +61,7 @@ __export(index_exports, {
|
|
|
61
61
|
deriveAttentions: () => deriveAttentions,
|
|
62
62
|
fefoPolicy: () => fefoPolicy,
|
|
63
63
|
firstFitPolicy: () => firstFitPolicy,
|
|
64
|
+
foldTaskRecords: () => foldTaskRecords,
|
|
64
65
|
gdtiUri: () => gdtiUri,
|
|
65
66
|
graiUri: () => graiUri,
|
|
66
67
|
ingest: () => ingest,
|
|
@@ -531,10 +532,9 @@ var BTT = {
|
|
|
531
532
|
so: "urn:epcglobal:cbv:btt:so"
|
|
532
533
|
};
|
|
533
534
|
var WMS_NODE_TYPES = ["dock", "storage", "staging", "dock-ship"];
|
|
534
|
-
var WMS_NODE_LABELS = { dock: "\uC785\uACE0 \uB3C4\uD06C", storage: "\uBCF4\uAD00 \uC704\uCE58", staging: "\uC2A4\uD14C\uC774\uC9D5", "dock-ship": "\uCD9C\uACE0 \uB3C4\uD06C" };
|
|
535
535
|
var WMS_TYPES = [
|
|
536
|
-
...WMS_NODE_TYPES.map((k) => ({ key: k, role: "node", label:
|
|
537
|
-
{ key: "forklift", role: "mover", label: "
|
|
536
|
+
...WMS_NODE_TYPES.map((k) => ({ key: k, role: "node", label: `twin.type.${k}`, standardClass: { epcis: "bizLocation" }, identity: { scheme: "gs1:SGLN" }, capabilities: ["storable"] })),
|
|
537
|
+
{ key: "forklift", role: "mover", label: "twin.type.forklift", standardClass: { epcis: "object", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["mobile", "operable"] }
|
|
538
538
|
];
|
|
539
539
|
|
|
540
540
|
// src/capability.ts
|
|
@@ -602,10 +602,9 @@ function graiUri(companyPrefix, assetType, serial) {
|
|
|
602
602
|
return `urn:epc:id:grai:${companyPrefix}.${assetType}.${String(serial).padStart(6, "0")}`;
|
|
603
603
|
}
|
|
604
604
|
var YMS_NODE_TYPES = ["gate", "yard-slot", "dock-door", "staging"];
|
|
605
|
-
var YMS_NODE_LABELS = { gate: "\uAC8C\uC774\uD2B8", "yard-slot": "\uC57C\uB4DC \uC2AC\uB86F", "dock-door": "\uB3C4\uD06C \uB3C4\uC5B4", staging: "\uC2A4\uD14C\uC774\uC9D5" };
|
|
606
605
|
var YMS_TYPES = [
|
|
607
|
-
...YMS_NODE_TYPES.map((k) => ({ key: k, role: "node", label:
|
|
608
|
-
{ key: "hostler", role: "mover", label: "
|
|
606
|
+
...YMS_NODE_TYPES.map((k) => ({ key: k, role: "node", label: `twin.type.${k}`, standardClass: { epcis: "bizLocation" }, identity: { scheme: "gs1:SGLN" }, capabilities: ["storable"] })),
|
|
607
|
+
{ key: "hostler", role: "mover", label: "twin.type.hostler", standardClass: { epcis: "object", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["mobile", "operable"] }
|
|
609
608
|
];
|
|
610
609
|
|
|
611
610
|
// src/mes-profile.ts
|
|
@@ -622,28 +621,29 @@ function sgtinUri(companyPrefix, itemRef, serial) {
|
|
|
622
621
|
return `urn:epc:id:sgtin:${companyPrefix}.${itemRef}.${serial}`;
|
|
623
622
|
}
|
|
624
623
|
var MES_NODE_TYPES = ["raw-store", "cut-station", "weld-station", "paint-booth", "assembly-line", "fg-store"];
|
|
625
|
-
var
|
|
626
|
-
"raw-store": {
|
|
627
|
-
"cut-station": {
|
|
628
|
-
"weld-station": {
|
|
629
|
-
"paint-booth": {
|
|
630
|
-
"assembly-line": {
|
|
631
|
-
"fg-store": {
|
|
624
|
+
var MES_NODE_CLS = {
|
|
625
|
+
"raw-store": { epcis: "bizLocation" },
|
|
626
|
+
"cut-station": { isa95: "WorkCenter", epcis: "bizLocation" },
|
|
627
|
+
"weld-station": { isa95: "WorkCenter", epcis: "bizLocation" },
|
|
628
|
+
"paint-booth": { isa95: "WorkCenter", epcis: "bizLocation" },
|
|
629
|
+
"assembly-line": { isa95: "WorkCenter", epcis: "bizLocation" },
|
|
630
|
+
"fg-store": { epcis: "bizLocation" }
|
|
632
631
|
};
|
|
633
632
|
var MES_TYPES = [
|
|
634
|
-
...MES_NODE_TYPES.map((k) => ({ key: k, role: "node", label:
|
|
635
|
-
{ key: "cutter", role: "mover", label: "
|
|
636
|
-
{ key: "welder", role: "mover", label: "
|
|
637
|
-
{ key: "painter", role: "mover", label: "
|
|
638
|
-
{ key: "assembler", role: "mover", label: "
|
|
633
|
+
...MES_NODE_TYPES.map((k) => ({ key: k, role: "node", label: `twin.type.${k}`, standardClass: MES_NODE_CLS[k] ?? {}, identity: { scheme: "gs1:SGLN" }, capabilities: ["storable"] })),
|
|
634
|
+
{ key: "cutter", role: "mover", label: "twin.type.cutter", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] },
|
|
635
|
+
{ key: "welder", role: "mover", label: "twin.type.welder", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] },
|
|
636
|
+
{ key: "painter", role: "mover", label: "twin.type.painter", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] },
|
|
637
|
+
{ key: "assembler", role: "mover", label: "twin.type.assembler", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] }
|
|
639
638
|
];
|
|
640
639
|
|
|
641
640
|
// src/domain-catalog.ts
|
|
642
641
|
var nodeKeys = (types) => types.filter((t) => t.role === "node").map((t) => t.key);
|
|
643
642
|
var DOMAIN_CATALOG = {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
643
|
+
// label 은 언어 중립 i18n 키(twin.system.<code>) — 사람 언어는 표현계층이 렌더(L2).
|
|
644
|
+
wms: { system: "wms", label: "twin.system.wms", types: WMS_TYPES, nodeTypes: nodeKeys(WMS_TYPES) },
|
|
645
|
+
yms: { system: "yms", label: "twin.system.yms", types: YMS_TYPES, nodeTypes: nodeKeys(YMS_TYPES) },
|
|
646
|
+
mes: { system: "mes", label: "twin.system.mes", types: MES_TYPES, nodeTypes: nodeKeys(MES_TYPES) }
|
|
647
647
|
};
|
|
648
648
|
var DOMAIN_SYSTEMS = ["wms", "yms", "mes"];
|
|
649
649
|
function capabilitiesForType(system, typeKey) {
|
|
@@ -680,7 +680,47 @@ var fefoPolicy = {
|
|
|
680
680
|
};
|
|
681
681
|
|
|
682
682
|
// src/duration-estimator.ts
|
|
683
|
-
var constantDuration = (
|
|
683
|
+
var constantDuration = (ms2) => ({ estimate: () => ms2 });
|
|
684
|
+
|
|
685
|
+
// src/task-fold.ts
|
|
686
|
+
function ms(value) {
|
|
687
|
+
if (!value) return null;
|
|
688
|
+
const t = Date.parse(value);
|
|
689
|
+
return Number.isFinite(t) ? t : null;
|
|
690
|
+
}
|
|
691
|
+
function deltaOf(row) {
|
|
692
|
+
const p = row.payload;
|
|
693
|
+
return row.data ?? p?.data ?? p ?? {};
|
|
694
|
+
}
|
|
695
|
+
function foldTaskRecords(rows) {
|
|
696
|
+
const byTask = /* @__PURE__ */ new Map();
|
|
697
|
+
let rowsRead = 0;
|
|
698
|
+
let latestMs = 0;
|
|
699
|
+
for (const row of rows) {
|
|
700
|
+
const at = ms(row.eventTime);
|
|
701
|
+
if (at === null) continue;
|
|
702
|
+
const d = deltaOf(row);
|
|
703
|
+
const id = d.taskId;
|
|
704
|
+
if (!id) continue;
|
|
705
|
+
rowsRead++;
|
|
706
|
+
latestMs = Math.max(latestMs, at);
|
|
707
|
+
let rec = byTask.get(id);
|
|
708
|
+
if (!rec) {
|
|
709
|
+
rec = { taskId: id, facets: {} };
|
|
710
|
+
byTask.set(id, rec);
|
|
711
|
+
}
|
|
712
|
+
const f = rec.facets;
|
|
713
|
+
if (f.resource === void 0 && d.resourceRef) f.resource = d.resourceRef;
|
|
714
|
+
if (f.kind === void 0 && d.kind) f.kind = d.kind;
|
|
715
|
+
if (f.order === void 0 && d.orderId) f.order = d.orderId;
|
|
716
|
+
const node = d.toNode ?? d.fromNode;
|
|
717
|
+
if (node) f.node = node;
|
|
718
|
+
if (d.status === "created") rec.createdMs = at;
|
|
719
|
+
else if (d.status === "in-progress") rec.startedMs = rec.startedMs != null ? Math.min(rec.startedMs, at) : at;
|
|
720
|
+
else if (d.status === "completed") rec.completedMs = rec.completedMs != null ? Math.max(rec.completedMs, at) : at;
|
|
721
|
+
}
|
|
722
|
+
return { records: [...byTask.values()], rowsRead, latestMs };
|
|
723
|
+
}
|
|
684
724
|
|
|
685
725
|
// src/face2-adapter.ts
|
|
686
726
|
function get(obj, path) {
|
|
@@ -801,19 +841,17 @@ function deriveAttentions(view, acked) {
|
|
|
801
841
|
const out = [];
|
|
802
842
|
for (const m of view.movers) {
|
|
803
843
|
if (m.status === "down") {
|
|
804
|
-
const where = m.location ?? "\uD574\uB2F9 \uACF5\uC815";
|
|
805
844
|
out.push({
|
|
806
845
|
id: `breakdown:${m.id}`,
|
|
807
846
|
kind: "breakdown",
|
|
808
847
|
severity: "critical",
|
|
809
|
-
title: `\uC124\uBE44 \uACE0\uC7A5 \xB7 ${m.id}`,
|
|
810
848
|
anchor: { moverId: m.id, nodeId: m.location },
|
|
811
|
-
|
|
849
|
+
params: { moverId: m.id, ...m.location ? { nodeId: m.location } : {} },
|
|
812
850
|
recommendedActions: [
|
|
813
|
-
{
|
|
814
|
-
{
|
|
851
|
+
{ code: "act.repair", command: CMD.resourceRepair, args: { resourceId: m.id } },
|
|
852
|
+
{ code: "act.hold-until-repair", command: CMD.resourceHold, args: { resourceId: m.id } }
|
|
815
853
|
],
|
|
816
|
-
suggestedAction: { command: CMD.resourceRepair, args: { resourceId: m.id }
|
|
854
|
+
suggestedAction: { code: "act.repair", command: CMD.resourceRepair, args: { resourceId: m.id } }
|
|
817
855
|
});
|
|
818
856
|
}
|
|
819
857
|
}
|
|
@@ -826,13 +864,10 @@ function deriveAttentions(view, acked) {
|
|
|
826
864
|
id: `bottleneck:${n.id}`,
|
|
827
865
|
kind: "bottleneck",
|
|
828
866
|
severity: saturated ? "high" : "medium",
|
|
829
|
-
title: `${saturated ? "\uBCD1\uBAA9" : "\uD63C\uC7A1"} \xB7 ${n.id} \uC810\uC720 ${n.occupancy}/${n.capacity}`,
|
|
830
867
|
anchor: { nodeId: n.id },
|
|
831
|
-
|
|
832
|
-
recommendedActions: [
|
|
833
|
-
|
|
834
|
-
{ label: "\uD558\uB958 \uC6B0\uC120 \uCC98\uB9AC", hint: "\uC801\uCCB4 \uD574\uC18C\uB97C \uC704\uD574 \uBC30\uCD9C \uC6B0\uC120\uC21C\uC704 \uC870\uC815" }
|
|
835
|
-
]
|
|
868
|
+
params: { nodeId: n.id, occupancy: n.occupancy ?? 0, capacity: n.capacity ?? 0, ratioPct: Math.round(r * 100), saturated: saturated ? 1 : 0 },
|
|
869
|
+
recommendedActions: [{ code: "advice.add-resource" }, { code: "advice.downstream-priority" }]
|
|
870
|
+
// 권고만(command 없음) — 표현계층이 advice.* 코드로 렌더
|
|
836
871
|
});
|
|
837
872
|
}
|
|
838
873
|
}
|
|
@@ -845,15 +880,13 @@ function deriveAttentions(view, acked) {
|
|
|
845
880
|
id: `scrap:${m.id}`,
|
|
846
881
|
kind: "scrap-high",
|
|
847
882
|
severity: rate >= 0.3 ? "high" : "medium",
|
|
848
|
-
title: `\uBD88\uB7C9\uB960 ${Math.round(rate * 100)}% \xB7 ${m.id}`,
|
|
849
|
-
detail: `\uC591\uD488 ${m.goodCount} / \uBD88\uB7C9 ${m.scrapCount}`,
|
|
850
883
|
anchor: { moverId: m.id, nodeId: m.location },
|
|
851
|
-
|
|
884
|
+
params: { moverId: m.id, goodCount: m.goodCount ?? 0, scrapCount: m.scrapCount ?? 0, ratePct: Math.round(rate * 100) },
|
|
852
885
|
recommendedActions: [
|
|
853
|
-
{
|
|
854
|
-
{
|
|
886
|
+
{ code: "act.hold-for-inspection", command: CMD.resourceHold, args: { resourceId: m.id } },
|
|
887
|
+
{ code: "act.reset-metrics", command: CMD.resourceResetMetrics, args: { resourceId: m.id } }
|
|
855
888
|
],
|
|
856
|
-
suggestedAction: { command: CMD.resourceHold, args: { resourceId: m.id }
|
|
889
|
+
suggestedAction: { code: "act.hold-for-inspection", command: CMD.resourceHold, args: { resourceId: m.id } }
|
|
857
890
|
});
|
|
858
891
|
}
|
|
859
892
|
}
|
|
@@ -862,11 +895,10 @@ function deriveAttentions(view, acked) {
|
|
|
862
895
|
id: `hold:${o.id}`,
|
|
863
896
|
kind: "hold",
|
|
864
897
|
severity: "medium",
|
|
865
|
-
title: `\uC624\uB354 \uBCF4\uB958 \xB7 ${o.id}`,
|
|
866
898
|
anchor: { orderId: o.id },
|
|
867
|
-
|
|
868
|
-
recommendedActions: [{
|
|
869
|
-
suggestedAction: { command: CMD.orderResume, args: { orderId: o.id }
|
|
899
|
+
params: { orderId: o.id },
|
|
900
|
+
recommendedActions: [{ code: "act.resume-order", command: CMD.orderResume, args: { orderId: o.id } }],
|
|
901
|
+
suggestedAction: { code: "act.resume-order", command: CMD.orderResume, args: { orderId: o.id } }
|
|
870
902
|
});
|
|
871
903
|
}
|
|
872
904
|
if (acked) {
|
|
@@ -997,13 +1029,13 @@ var FlowEngine = class {
|
|
|
997
1029
|
// 확인(ack)된 주목 신호 id — 조건 지속돼도 acknowledged 로 표시(재발 시 재활성)
|
|
998
1030
|
dispatch(cmd) {
|
|
999
1031
|
const ok = () => ({ commandId: cmd.commandId, accepted: true });
|
|
1000
|
-
const fail = (
|
|
1032
|
+
const fail = (errorCode, errorParams) => ({ commandId: cmd.commandId, accepted: false, errorCode, errorParams, error: errorCode });
|
|
1001
1033
|
switch (cmd.type) {
|
|
1002
1034
|
case CMD.orderHold:
|
|
1003
1035
|
case CMD.orderResume: {
|
|
1004
1036
|
const orderId = cmd.args?.orderId;
|
|
1005
1037
|
const order = orderId ? this.orders.get(orderId) : void 0;
|
|
1006
|
-
if (!order) return fail(
|
|
1038
|
+
if (!order) return fail("order-not-found", { orderId: orderId ?? "" });
|
|
1007
1039
|
order.held = cmd.type === CMD.orderHold;
|
|
1008
1040
|
this.emitOrder(order);
|
|
1009
1041
|
return ok();
|
|
@@ -1017,7 +1049,7 @@ var FlowEngine = class {
|
|
|
1017
1049
|
case CMD.resourceHold:
|
|
1018
1050
|
case CMD.resourceResume: {
|
|
1019
1051
|
const m = this.movers.get(cmd.args?.resourceId ?? "");
|
|
1020
|
-
if (!m) return fail("resource
|
|
1052
|
+
if (!m) return fail("resource-not-found");
|
|
1021
1053
|
m.held = cmd.type === CMD.resourceHold;
|
|
1022
1054
|
this.emitMover(m);
|
|
1023
1055
|
return ok();
|
|
@@ -1025,7 +1057,7 @@ var FlowEngine = class {
|
|
|
1025
1057
|
case CMD.resourceDown: {
|
|
1026
1058
|
const a = cmd.args;
|
|
1027
1059
|
const m = this.movers.get(a?.resourceId ?? "");
|
|
1028
|
-
if (!m) return fail("resource
|
|
1060
|
+
if (!m) return fail("resource-not-found");
|
|
1029
1061
|
if (m.status !== "down") {
|
|
1030
1062
|
m.status = "down";
|
|
1031
1063
|
m.repairUntilMs = this.clockMs + (Number(a?.durationMs) || m.mttrMs || 6e4);
|
|
@@ -1035,7 +1067,7 @@ var FlowEngine = class {
|
|
|
1035
1067
|
}
|
|
1036
1068
|
case CMD.resourceRepair: {
|
|
1037
1069
|
const m = this.movers.get(cmd.args?.resourceId ?? "");
|
|
1038
|
-
if (!m) return fail("resource
|
|
1070
|
+
if (!m) return fail("resource-not-found");
|
|
1039
1071
|
if (m.status === "down") {
|
|
1040
1072
|
m.status = m.taskId ? "busy" : "idle";
|
|
1041
1073
|
m.repairUntilMs = void 0;
|
|
@@ -1046,7 +1078,7 @@ var FlowEngine = class {
|
|
|
1046
1078
|
}
|
|
1047
1079
|
case CMD.resourceResetMetrics: {
|
|
1048
1080
|
const m = this.movers.get(cmd.args?.resourceId ?? "");
|
|
1049
|
-
if (!m) return fail("resource
|
|
1081
|
+
if (!m) return fail("resource-not-found");
|
|
1050
1082
|
m.runMs = 0;
|
|
1051
1083
|
m.setupMs = 0;
|
|
1052
1084
|
m.downMs = 0;
|
|
@@ -1059,8 +1091,8 @@ var FlowEngine = class {
|
|
|
1059
1091
|
}
|
|
1060
1092
|
case CMD.resourceAdd: {
|
|
1061
1093
|
const a = cmd.args;
|
|
1062
|
-
if (!a?.kind) return fail("kind
|
|
1063
|
-
if (!a?.homeNode || !this.nodes.has(a.homeNode)) return fail(
|
|
1094
|
+
if (!a?.kind) return fail("kind-required");
|
|
1095
|
+
if (!a?.homeNode || !this.nodes.has(a.homeNode)) return fail("home-node-not-found", { homeNode: a?.homeNode ?? "" });
|
|
1064
1096
|
const count = Math.max(1, Math.min(50, Number(a.count) || 1));
|
|
1065
1097
|
let seq = this.movers.size;
|
|
1066
1098
|
for (let i = 0; i < count; i++) {
|
|
@@ -1078,7 +1110,7 @@ var FlowEngine = class {
|
|
|
1078
1110
|
}
|
|
1079
1111
|
/** 도메인 커맨드 처리(order.release 등). 기본은 거절 — 도메인이 override. */
|
|
1080
1112
|
handleCommand(cmd) {
|
|
1081
|
-
return { commandId: cmd.commandId, accepted: false,
|
|
1113
|
+
return { commandId: cmd.commandId, accepted: false, errorCode: "unknown-command", errorParams: { type: cmd.type }, error: `unknown-command: ${cmd.type}` };
|
|
1082
1114
|
}
|
|
1083
1115
|
scenario = {
|
|
1084
1116
|
load: (def) => {
|
|
@@ -1455,7 +1487,7 @@ var WmsKernel = class extends FlowEngine {
|
|
|
1455
1487
|
if (cmd.type === CMD.orderRelease) {
|
|
1456
1488
|
const a = cmd.args;
|
|
1457
1489
|
const lines = a?.lines ?? (a?.gtin ? [{ gtin: a.gtin, qty: a.qty ?? 1 }] : []);
|
|
1458
|
-
if (lines.length === 0) return { commandId: cmd.commandId, accepted: false, error: "order.release: gtin
|
|
1490
|
+
if (lines.length === 0) return { commandId: cmd.commandId, accepted: false, errorCode: "order-release-needs-lines", error: "order.release: gtin or lines required" };
|
|
1459
1491
|
this.createSalesOrder(lines);
|
|
1460
1492
|
return { commandId: cmd.commandId, accepted: true };
|
|
1461
1493
|
}
|
|
@@ -1755,9 +1787,9 @@ var MesKernel = class extends FlowEngine {
|
|
|
1755
1787
|
handleCommand(cmd) {
|
|
1756
1788
|
if (cmd.type === MES_CMD.changeover) {
|
|
1757
1789
|
const a = cmd.args;
|
|
1758
|
-
if (!a?.resourceId || !a?.gtin) return { commandId: cmd.commandId, accepted: false, error: "mes.changeover: resourceId
|
|
1790
|
+
if (!a?.resourceId || !a?.gtin) return { commandId: cmd.commandId, accepted: false, errorCode: "changeover-needs-args", error: "mes.changeover: resourceId and gtin required" };
|
|
1759
1791
|
const m = this.movers.get(a.resourceId);
|
|
1760
|
-
if (!m) return { commandId: cmd.commandId, accepted: false, error: `resource
|
|
1792
|
+
if (!m) return { commandId: cmd.commandId, accepted: false, errorCode: "resource-not-found", errorParams: { resourceId: a.resourceId }, error: `resource-not-found: ${a.resourceId}` };
|
|
1761
1793
|
if (m.lastChangeoverKey !== a.gtin) {
|
|
1762
1794
|
m.setupMs += SETUP_MS;
|
|
1763
1795
|
m.lastChangeoverKey = a.gtin;
|
|
@@ -1999,6 +2031,7 @@ var MesKernel = class extends FlowEngine {
|
|
|
1999
2031
|
deriveAttentions,
|
|
2000
2032
|
fefoPolicy,
|
|
2001
2033
|
firstFitPolicy,
|
|
2034
|
+
foldTaskRecords,
|
|
2002
2035
|
gdtiUri,
|
|
2003
2036
|
graiUri,
|
|
2004
2037
|
ingest,
|
package/package.json
CHANGED