@operato/twin-kernel 0.0.5 → 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/contract.d.ts +19 -7
- package/dist/domain-catalog.js +4 -3
- package/dist/flow-engine.js +28 -30
- package/dist/kernel.js +1 -1
- package/dist/mes-kernel.js +2 -2
- package/dist/mes-profile.js +13 -12
- package/dist/wms-profile.js +3 -3
- package/dist/yms-profile.js +2 -3
- package/dist-cjs/index.cjs +46 -55
- 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/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
|
];
|
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
|
@@ -531,10 +531,9 @@ var BTT = {
|
|
|
531
531
|
so: "urn:epcglobal:cbv:btt:so"
|
|
532
532
|
};
|
|
533
533
|
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
534
|
var WMS_TYPES = [
|
|
536
|
-
...WMS_NODE_TYPES.map((k) => ({ key: k, role: "node", label:
|
|
537
|
-
{ key: "forklift", role: "mover", label: "
|
|
535
|
+
...WMS_NODE_TYPES.map((k) => ({ key: k, role: "node", label: `twin.type.${k}`, standardClass: { epcis: "bizLocation" }, identity: { scheme: "gs1:SGLN" }, capabilities: ["storable"] })),
|
|
536
|
+
{ key: "forklift", role: "mover", label: "twin.type.forklift", standardClass: { epcis: "object", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["mobile", "operable"] }
|
|
538
537
|
];
|
|
539
538
|
|
|
540
539
|
// src/capability.ts
|
|
@@ -602,10 +601,9 @@ function graiUri(companyPrefix, assetType, serial) {
|
|
|
602
601
|
return `urn:epc:id:grai:${companyPrefix}.${assetType}.${String(serial).padStart(6, "0")}`;
|
|
603
602
|
}
|
|
604
603
|
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
604
|
var YMS_TYPES = [
|
|
607
|
-
...YMS_NODE_TYPES.map((k) => ({ key: k, role: "node", label:
|
|
608
|
-
{ key: "hostler", role: "mover", label: "
|
|
605
|
+
...YMS_NODE_TYPES.map((k) => ({ key: k, role: "node", label: `twin.type.${k}`, standardClass: { epcis: "bizLocation" }, identity: { scheme: "gs1:SGLN" }, capabilities: ["storable"] })),
|
|
606
|
+
{ key: "hostler", role: "mover", label: "twin.type.hostler", standardClass: { epcis: "object", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["mobile", "operable"] }
|
|
609
607
|
];
|
|
610
608
|
|
|
611
609
|
// src/mes-profile.ts
|
|
@@ -622,28 +620,29 @@ function sgtinUri(companyPrefix, itemRef, serial) {
|
|
|
622
620
|
return `urn:epc:id:sgtin:${companyPrefix}.${itemRef}.${serial}`;
|
|
623
621
|
}
|
|
624
622
|
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": {
|
|
623
|
+
var MES_NODE_CLS = {
|
|
624
|
+
"raw-store": { epcis: "bizLocation" },
|
|
625
|
+
"cut-station": { isa95: "WorkCenter", epcis: "bizLocation" },
|
|
626
|
+
"weld-station": { isa95: "WorkCenter", epcis: "bizLocation" },
|
|
627
|
+
"paint-booth": { isa95: "WorkCenter", epcis: "bizLocation" },
|
|
628
|
+
"assembly-line": { isa95: "WorkCenter", epcis: "bizLocation" },
|
|
629
|
+
"fg-store": { epcis: "bizLocation" }
|
|
632
630
|
};
|
|
633
631
|
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: "
|
|
632
|
+
...MES_NODE_TYPES.map((k) => ({ key: k, role: "node", label: `twin.type.${k}`, standardClass: MES_NODE_CLS[k] ?? {}, identity: { scheme: "gs1:SGLN" }, capabilities: ["storable"] })),
|
|
633
|
+
{ key: "cutter", role: "mover", label: "twin.type.cutter", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] },
|
|
634
|
+
{ key: "welder", role: "mover", label: "twin.type.welder", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] },
|
|
635
|
+
{ key: "painter", role: "mover", label: "twin.type.painter", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] },
|
|
636
|
+
{ key: "assembler", role: "mover", label: "twin.type.assembler", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] }
|
|
639
637
|
];
|
|
640
638
|
|
|
641
639
|
// src/domain-catalog.ts
|
|
642
640
|
var nodeKeys = (types) => types.filter((t) => t.role === "node").map((t) => t.key);
|
|
643
641
|
var DOMAIN_CATALOG = {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
642
|
+
// label 은 언어 중립 i18n 키(twin.system.<code>) — 사람 언어는 표현계층이 렌더(L2).
|
|
643
|
+
wms: { system: "wms", label: "twin.system.wms", types: WMS_TYPES, nodeTypes: nodeKeys(WMS_TYPES) },
|
|
644
|
+
yms: { system: "yms", label: "twin.system.yms", types: YMS_TYPES, nodeTypes: nodeKeys(YMS_TYPES) },
|
|
645
|
+
mes: { system: "mes", label: "twin.system.mes", types: MES_TYPES, nodeTypes: nodeKeys(MES_TYPES) }
|
|
647
646
|
};
|
|
648
647
|
var DOMAIN_SYSTEMS = ["wms", "yms", "mes"];
|
|
649
648
|
function capabilitiesForType(system, typeKey) {
|
|
@@ -801,19 +800,17 @@ function deriveAttentions(view, acked) {
|
|
|
801
800
|
const out = [];
|
|
802
801
|
for (const m of view.movers) {
|
|
803
802
|
if (m.status === "down") {
|
|
804
|
-
const where = m.location ?? "\uD574\uB2F9 \uACF5\uC815";
|
|
805
803
|
out.push({
|
|
806
804
|
id: `breakdown:${m.id}`,
|
|
807
805
|
kind: "breakdown",
|
|
808
806
|
severity: "critical",
|
|
809
|
-
title: `\uC124\uBE44 \uACE0\uC7A5 \xB7 ${m.id}`,
|
|
810
807
|
anchor: { moverId: m.id, nodeId: m.location },
|
|
811
|
-
|
|
808
|
+
params: { moverId: m.id, ...m.location ? { nodeId: m.location } : {} },
|
|
812
809
|
recommendedActions: [
|
|
813
|
-
{
|
|
814
|
-
{
|
|
810
|
+
{ code: "act.repair", command: CMD.resourceRepair, args: { resourceId: m.id } },
|
|
811
|
+
{ code: "act.hold-until-repair", command: CMD.resourceHold, args: { resourceId: m.id } }
|
|
815
812
|
],
|
|
816
|
-
suggestedAction: { command: CMD.resourceRepair, args: { resourceId: m.id }
|
|
813
|
+
suggestedAction: { code: "act.repair", command: CMD.resourceRepair, args: { resourceId: m.id } }
|
|
817
814
|
});
|
|
818
815
|
}
|
|
819
816
|
}
|
|
@@ -826,13 +823,10 @@ function deriveAttentions(view, acked) {
|
|
|
826
823
|
id: `bottleneck:${n.id}`,
|
|
827
824
|
kind: "bottleneck",
|
|
828
825
|
severity: saturated ? "high" : "medium",
|
|
829
|
-
title: `${saturated ? "\uBCD1\uBAA9" : "\uD63C\uC7A1"} \xB7 ${n.id} \uC810\uC720 ${n.occupancy}/${n.capacity}`,
|
|
830
826
|
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
|
-
]
|
|
827
|
+
params: { nodeId: n.id, occupancy: n.occupancy ?? 0, capacity: n.capacity ?? 0, ratioPct: Math.round(r * 100), saturated: saturated ? 1 : 0 },
|
|
828
|
+
recommendedActions: [{ code: "advice.add-resource" }, { code: "advice.downstream-priority" }]
|
|
829
|
+
// 권고만(command 없음) — 표현계층이 advice.* 코드로 렌더
|
|
836
830
|
});
|
|
837
831
|
}
|
|
838
832
|
}
|
|
@@ -845,15 +839,13 @@ function deriveAttentions(view, acked) {
|
|
|
845
839
|
id: `scrap:${m.id}`,
|
|
846
840
|
kind: "scrap-high",
|
|
847
841
|
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
842
|
anchor: { moverId: m.id, nodeId: m.location },
|
|
851
|
-
|
|
843
|
+
params: { moverId: m.id, goodCount: m.goodCount ?? 0, scrapCount: m.scrapCount ?? 0, ratePct: Math.round(rate * 100) },
|
|
852
844
|
recommendedActions: [
|
|
853
|
-
{
|
|
854
|
-
{
|
|
845
|
+
{ code: "act.hold-for-inspection", command: CMD.resourceHold, args: { resourceId: m.id } },
|
|
846
|
+
{ code: "act.reset-metrics", command: CMD.resourceResetMetrics, args: { resourceId: m.id } }
|
|
855
847
|
],
|
|
856
|
-
suggestedAction: { command: CMD.resourceHold, args: { resourceId: m.id }
|
|
848
|
+
suggestedAction: { code: "act.hold-for-inspection", command: CMD.resourceHold, args: { resourceId: m.id } }
|
|
857
849
|
});
|
|
858
850
|
}
|
|
859
851
|
}
|
|
@@ -862,11 +854,10 @@ function deriveAttentions(view, acked) {
|
|
|
862
854
|
id: `hold:${o.id}`,
|
|
863
855
|
kind: "hold",
|
|
864
856
|
severity: "medium",
|
|
865
|
-
title: `\uC624\uB354 \uBCF4\uB958 \xB7 ${o.id}`,
|
|
866
857
|
anchor: { orderId: o.id },
|
|
867
|
-
|
|
868
|
-
recommendedActions: [{
|
|
869
|
-
suggestedAction: { command: CMD.orderResume, args: { orderId: o.id }
|
|
858
|
+
params: { orderId: o.id },
|
|
859
|
+
recommendedActions: [{ code: "act.resume-order", command: CMD.orderResume, args: { orderId: o.id } }],
|
|
860
|
+
suggestedAction: { code: "act.resume-order", command: CMD.orderResume, args: { orderId: o.id } }
|
|
870
861
|
});
|
|
871
862
|
}
|
|
872
863
|
if (acked) {
|
|
@@ -997,13 +988,13 @@ var FlowEngine = class {
|
|
|
997
988
|
// 확인(ack)된 주목 신호 id — 조건 지속돼도 acknowledged 로 표시(재발 시 재활성)
|
|
998
989
|
dispatch(cmd) {
|
|
999
990
|
const ok = () => ({ commandId: cmd.commandId, accepted: true });
|
|
1000
|
-
const fail = (
|
|
991
|
+
const fail = (errorCode, errorParams) => ({ commandId: cmd.commandId, accepted: false, errorCode, errorParams, error: errorCode });
|
|
1001
992
|
switch (cmd.type) {
|
|
1002
993
|
case CMD.orderHold:
|
|
1003
994
|
case CMD.orderResume: {
|
|
1004
995
|
const orderId = cmd.args?.orderId;
|
|
1005
996
|
const order = orderId ? this.orders.get(orderId) : void 0;
|
|
1006
|
-
if (!order) return fail(
|
|
997
|
+
if (!order) return fail("order-not-found", { orderId: orderId ?? "" });
|
|
1007
998
|
order.held = cmd.type === CMD.orderHold;
|
|
1008
999
|
this.emitOrder(order);
|
|
1009
1000
|
return ok();
|
|
@@ -1017,7 +1008,7 @@ var FlowEngine = class {
|
|
|
1017
1008
|
case CMD.resourceHold:
|
|
1018
1009
|
case CMD.resourceResume: {
|
|
1019
1010
|
const m = this.movers.get(cmd.args?.resourceId ?? "");
|
|
1020
|
-
if (!m) return fail("resource
|
|
1011
|
+
if (!m) return fail("resource-not-found");
|
|
1021
1012
|
m.held = cmd.type === CMD.resourceHold;
|
|
1022
1013
|
this.emitMover(m);
|
|
1023
1014
|
return ok();
|
|
@@ -1025,7 +1016,7 @@ var FlowEngine = class {
|
|
|
1025
1016
|
case CMD.resourceDown: {
|
|
1026
1017
|
const a = cmd.args;
|
|
1027
1018
|
const m = this.movers.get(a?.resourceId ?? "");
|
|
1028
|
-
if (!m) return fail("resource
|
|
1019
|
+
if (!m) return fail("resource-not-found");
|
|
1029
1020
|
if (m.status !== "down") {
|
|
1030
1021
|
m.status = "down";
|
|
1031
1022
|
m.repairUntilMs = this.clockMs + (Number(a?.durationMs) || m.mttrMs || 6e4);
|
|
@@ -1035,7 +1026,7 @@ var FlowEngine = class {
|
|
|
1035
1026
|
}
|
|
1036
1027
|
case CMD.resourceRepair: {
|
|
1037
1028
|
const m = this.movers.get(cmd.args?.resourceId ?? "");
|
|
1038
|
-
if (!m) return fail("resource
|
|
1029
|
+
if (!m) return fail("resource-not-found");
|
|
1039
1030
|
if (m.status === "down") {
|
|
1040
1031
|
m.status = m.taskId ? "busy" : "idle";
|
|
1041
1032
|
m.repairUntilMs = void 0;
|
|
@@ -1046,7 +1037,7 @@ var FlowEngine = class {
|
|
|
1046
1037
|
}
|
|
1047
1038
|
case CMD.resourceResetMetrics: {
|
|
1048
1039
|
const m = this.movers.get(cmd.args?.resourceId ?? "");
|
|
1049
|
-
if (!m) return fail("resource
|
|
1040
|
+
if (!m) return fail("resource-not-found");
|
|
1050
1041
|
m.runMs = 0;
|
|
1051
1042
|
m.setupMs = 0;
|
|
1052
1043
|
m.downMs = 0;
|
|
@@ -1059,8 +1050,8 @@ var FlowEngine = class {
|
|
|
1059
1050
|
}
|
|
1060
1051
|
case CMD.resourceAdd: {
|
|
1061
1052
|
const a = cmd.args;
|
|
1062
|
-
if (!a?.kind) return fail("kind
|
|
1063
|
-
if (!a?.homeNode || !this.nodes.has(a.homeNode)) return fail(
|
|
1053
|
+
if (!a?.kind) return fail("kind-required");
|
|
1054
|
+
if (!a?.homeNode || !this.nodes.has(a.homeNode)) return fail("home-node-not-found", { homeNode: a?.homeNode ?? "" });
|
|
1064
1055
|
const count = Math.max(1, Math.min(50, Number(a.count) || 1));
|
|
1065
1056
|
let seq = this.movers.size;
|
|
1066
1057
|
for (let i = 0; i < count; i++) {
|
|
@@ -1078,7 +1069,7 @@ var FlowEngine = class {
|
|
|
1078
1069
|
}
|
|
1079
1070
|
/** 도메인 커맨드 처리(order.release 등). 기본은 거절 — 도메인이 override. */
|
|
1080
1071
|
handleCommand(cmd) {
|
|
1081
|
-
return { commandId: cmd.commandId, accepted: false,
|
|
1072
|
+
return { commandId: cmd.commandId, accepted: false, errorCode: "unknown-command", errorParams: { type: cmd.type }, error: `unknown-command: ${cmd.type}` };
|
|
1082
1073
|
}
|
|
1083
1074
|
scenario = {
|
|
1084
1075
|
load: (def) => {
|
|
@@ -1455,7 +1446,7 @@ var WmsKernel = class extends FlowEngine {
|
|
|
1455
1446
|
if (cmd.type === CMD.orderRelease) {
|
|
1456
1447
|
const a = cmd.args;
|
|
1457
1448
|
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
|
|
1449
|
+
if (lines.length === 0) return { commandId: cmd.commandId, accepted: false, errorCode: "order-release-needs-lines", error: "order.release: gtin or lines required" };
|
|
1459
1450
|
this.createSalesOrder(lines);
|
|
1460
1451
|
return { commandId: cmd.commandId, accepted: true };
|
|
1461
1452
|
}
|
|
@@ -1755,9 +1746,9 @@ var MesKernel = class extends FlowEngine {
|
|
|
1755
1746
|
handleCommand(cmd) {
|
|
1756
1747
|
if (cmd.type === MES_CMD.changeover) {
|
|
1757
1748
|
const a = cmd.args;
|
|
1758
|
-
if (!a?.resourceId || !a?.gtin) return { commandId: cmd.commandId, accepted: false, error: "mes.changeover: resourceId
|
|
1749
|
+
if (!a?.resourceId || !a?.gtin) return { commandId: cmd.commandId, accepted: false, errorCode: "changeover-needs-args", error: "mes.changeover: resourceId and gtin required" };
|
|
1759
1750
|
const m = this.movers.get(a.resourceId);
|
|
1760
|
-
if (!m) return { commandId: cmd.commandId, accepted: false, error: `resource
|
|
1751
|
+
if (!m) return { commandId: cmd.commandId, accepted: false, errorCode: "resource-not-found", errorParams: { resourceId: a.resourceId }, error: `resource-not-found: ${a.resourceId}` };
|
|
1761
1752
|
if (m.lastChangeoverKey !== a.gtin) {
|
|
1762
1753
|
m.setupMs += SETUP_MS;
|
|
1763
1754
|
m.lastChangeoverKey = a.gtin;
|
package/package.json
CHANGED