@operato/twin-kernel 0.0.3 → 0.0.5
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 +79 -1
- package/dist/contract.js +11 -2
- package/dist/domain-catalog.d.ts +33 -0
- package/dist/domain-catalog.js +14 -0
- 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 +270 -27
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -1
- package/dist/mes-kernel.d.ts +48 -4
- package/dist/mes-kernel.js +185 -24
- package/dist/mes-profile.d.ts +5 -0
- package/dist/mes-profile.js +22 -5
- package/dist/state-projector.js +2 -2
- package/dist/wms-profile.d.ts +7 -0
- package/dist/wms-profile.js +14 -5
- package/dist/yms-profile.d.ts +4 -0
- package/dist/yms-profile.js +11 -8
- package/dist-cjs/index.cjs +627 -86
- package/package.json +1 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type CapabilityKey = 'operable' | 'storable' | 'mobile' | 'processable' | 'trackable';
|
|
2
|
+
/** 이동 관측 — Mobile 이 노출. */
|
|
3
|
+
export interface Motion {
|
|
4
|
+
fromNode: string;
|
|
5
|
+
toNode: string;
|
|
6
|
+
startedAtSimMs: number;
|
|
7
|
+
durationMs: number;
|
|
8
|
+
progress: number;
|
|
9
|
+
elapsedMs: number;
|
|
10
|
+
}
|
|
11
|
+
export interface OperableState {
|
|
12
|
+
status: 'idle' | 'busy' | 'down';
|
|
13
|
+
}
|
|
14
|
+
export interface StorableState {
|
|
15
|
+
occupancy: number;
|
|
16
|
+
capacity: number;
|
|
17
|
+
}
|
|
18
|
+
export interface MobileState {
|
|
19
|
+
location: string;
|
|
20
|
+
motion?: Motion;
|
|
21
|
+
}
|
|
22
|
+
export interface ProcessableState {
|
|
23
|
+
output?: {
|
|
24
|
+
good: number;
|
|
25
|
+
scrap: number;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export interface TrackableState {
|
|
29
|
+
lifecycle: string;
|
|
30
|
+
progress?: number;
|
|
31
|
+
held: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** 능력 관측 계약 메타(발견·검증). ops 없음(원칙 ①). */
|
|
34
|
+
export interface CapabilitySpec {
|
|
35
|
+
key: CapabilityKey;
|
|
36
|
+
label: string;
|
|
37
|
+
semantics: string;
|
|
38
|
+
/** 관측 상태 필드(데이터 형식). 직교 — 어느 필드도 두 능력에 중복 없음(원칙 ②). */
|
|
39
|
+
stateFields: string[];
|
|
40
|
+
/** 교환 데이터 모델 이름. */
|
|
41
|
+
models?: string[];
|
|
42
|
+
/** 불변식(모두 준수·신뢰). */
|
|
43
|
+
invariants?: string[];
|
|
44
|
+
/** 실행 결과(이벤트) 형식. */
|
|
45
|
+
results?: string[];
|
|
46
|
+
}
|
|
47
|
+
export declare const CAPABILITIES: Record<CapabilityKey, CapabilitySpec>;
|
|
48
|
+
export declare const CAPABILITY_KEYS: CapabilityKey[];
|
|
49
|
+
/** 능력 집합이 관측 노출하는 상태 필드 합집합(직교이므로 단순 병합). */
|
|
50
|
+
export declare function stateFieldsOf(caps: CapabilityKey[]): string[];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 능력(capability) = 관측 계약(observable contract). 사상 토대: design/plans/capability-contract.md.
|
|
3
|
+
*
|
|
4
|
+
* 원칙 ① 관측만 — state(데이터형식)+models+invariants+results 만 계약. ops/구현은 없음(커널·씬 각 시뮬 자유).
|
|
5
|
+
* "교환되는 것"을 계약하고 "계산하는 법"은 계약하지 않는다(결정성 지옥 회피·공유 가능성 핵심).
|
|
6
|
+
* 원칙 ② 직교·조합 — 교차 관심사 status 는 Operable 하나에만(중복 제거). 엔티티는 능력 *조합*.
|
|
7
|
+
* 원칙 ③ 작은 코어 + 열린 확장(무방언) — 도메인이 능력 선언, 코어는 발명 안 함.
|
|
8
|
+
* 원칙 ④ 능력 ⟂ standardClass — 다른 축, 파생 없음.
|
|
9
|
+
*
|
|
10
|
+
* things-scene 정합: 이건 "의도(intent)" 계층(관측). 씬 기제 믹스(Capacity/Transferable/CarrierLine …)가
|
|
11
|
+
* 이 의도를 *실현*한다 — 공유 계약은 관측(계층 B)뿐, 기제 methods(계층 A)는 공유 안 함.
|
|
12
|
+
* ⚠ Mobile ≠ Transferable: Mobile=자원 자신이 이동, Transferable=노드가 아이템 이동(씬 기제).
|
|
13
|
+
*/
|
|
14
|
+
export const CAPABILITIES = {
|
|
15
|
+
operable: {
|
|
16
|
+
key: 'operable', label: '운영', semantics: '능동 자원의 운영 상태(유휴/가동/고장). status 교차 관심사를 여기 하나로.',
|
|
17
|
+
stateFields: ['status'], results: ['statusChanged']
|
|
18
|
+
},
|
|
19
|
+
storable: {
|
|
20
|
+
key: 'storable', label: '저장', semantics: '아이템을 보유하는 위치 — 점유/용량. (씬 기제: Capacity)',
|
|
21
|
+
stateFields: ['occupancy', 'capacity'], invariants: ['0 <= occupancy <= capacity (capacity>0)'], results: ['occupancyChanged']
|
|
22
|
+
},
|
|
23
|
+
mobile: {
|
|
24
|
+
key: 'mobile', label: '이동', semantics: '자원 자신이 노드 간 이동. Transferable(아이템 이동)과 다름. (씬 기제: CarrierLine)',
|
|
25
|
+
stateFields: ['location', 'motion'], models: ['Motion'], results: ['moved', 'motionTick']
|
|
26
|
+
},
|
|
27
|
+
processable: {
|
|
28
|
+
key: 'processable', label: '가공', semantics: '변환/가공 수행 — 산출(양품/불량). 운영 status 는 Operable 조합. progress 방출은 후속.',
|
|
29
|
+
stateFields: ['output'], results: ['completed']
|
|
30
|
+
},
|
|
31
|
+
trackable: {
|
|
32
|
+
key: 'trackable', label: '추적', semantics: '오더/아이템 생애 추적 — 생애단계(도메인 라벨, 무방언)·진행·보류.',
|
|
33
|
+
stateFields: ['lifecycle', 'progress', 'held'], results: ['lifecycleChanged']
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
export const CAPABILITY_KEYS = ['operable', 'storable', 'mobile', 'processable', 'trackable'];
|
|
37
|
+
/** 능력 집합이 관측 노출하는 상태 필드 합집합(직교이므로 단순 병합). */
|
|
38
|
+
export function stateFieldsOf(caps) {
|
|
39
|
+
const out = new Set();
|
|
40
|
+
for (const c of caps)
|
|
41
|
+
for (const f of CAPABILITIES[c]?.stateFields ?? [])
|
|
42
|
+
out.add(f);
|
|
43
|
+
return [...out];
|
|
44
|
+
}
|
package/dist/contract.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface NodeState {
|
|
|
14
14
|
capacity?: number;
|
|
15
15
|
occupancy: number;
|
|
16
16
|
status?: string;
|
|
17
|
+
parentId?: string;
|
|
17
18
|
}
|
|
18
19
|
export interface ItemState {
|
|
19
20
|
epc: string;
|
|
@@ -64,6 +65,7 @@ export interface MoverState {
|
|
|
64
65
|
taskId?: string;
|
|
65
66
|
motion?: MoverMotion;
|
|
66
67
|
oee?: OeeMetrics;
|
|
68
|
+
held?: boolean;
|
|
67
69
|
}
|
|
68
70
|
export interface TaskState {
|
|
69
71
|
id: string;
|
|
@@ -73,6 +75,7 @@ export interface TaskState {
|
|
|
73
75
|
fromNode?: string;
|
|
74
76
|
toNode?: string;
|
|
75
77
|
resourceRef?: string;
|
|
78
|
+
orderId?: string;
|
|
76
79
|
progress?: number;
|
|
77
80
|
}
|
|
78
81
|
export interface OrderState {
|
|
@@ -82,6 +85,36 @@ export interface OrderState {
|
|
|
82
85
|
progress?: number;
|
|
83
86
|
held?: boolean;
|
|
84
87
|
}
|
|
88
|
+
export type AttentionSeverity = 'low' | 'medium' | 'high' | 'critical';
|
|
89
|
+
export type AttentionState = 'active' | 'acknowledged' | 'cleared';
|
|
90
|
+
export interface Attention {
|
|
91
|
+
id: string;
|
|
92
|
+
kind: string;
|
|
93
|
+
severity: AttentionSeverity;
|
|
94
|
+
state?: AttentionState;
|
|
95
|
+
title: string;
|
|
96
|
+
detail?: string;
|
|
97
|
+
anchor: {
|
|
98
|
+
nodeId?: string;
|
|
99
|
+
moverId?: string;
|
|
100
|
+
orderId?: string;
|
|
101
|
+
};
|
|
102
|
+
since?: number;
|
|
103
|
+
rationale?: string;
|
|
104
|
+
recommendedActions?: RecommendedAction[];
|
|
105
|
+
suggestedAction?: {
|
|
106
|
+
command: string;
|
|
107
|
+
args?: unknown;
|
|
108
|
+
label: string;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** 조치방향 — command 있으면 원클릭 실행, 없으면 권고 텍스트. */
|
|
112
|
+
export interface RecommendedAction {
|
|
113
|
+
label: string;
|
|
114
|
+
command?: string;
|
|
115
|
+
args?: unknown;
|
|
116
|
+
hint?: string;
|
|
117
|
+
}
|
|
85
118
|
export interface StateSnapshot {
|
|
86
119
|
revision: number;
|
|
87
120
|
simClockMs: number;
|
|
@@ -90,6 +123,7 @@ export interface StateSnapshot {
|
|
|
90
123
|
movers: MoverState[];
|
|
91
124
|
tasks: TaskState[];
|
|
92
125
|
orders: OrderState[];
|
|
126
|
+
attentions?: Attention[];
|
|
93
127
|
}
|
|
94
128
|
export interface Command<T = unknown> {
|
|
95
129
|
commandId: string;
|
|
@@ -103,6 +137,16 @@ export interface CommandAck {
|
|
|
103
137
|
accepted: boolean;
|
|
104
138
|
error?: string;
|
|
105
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* 호스트-facing 커맨드 채널(비동기) — 씬 컴포넌트 등 클라이언트가 트윈 호스트에 커맨드를 던지는 계약.
|
|
142
|
+
* 커널 in-process `dispatch`(동기)와 달리 원격 호스트(GraphQL/HTTP/…) 전송을 추상화.
|
|
143
|
+
* 씬 컨트롤 컴포넌트는 이 인터페이스에만 의존(커널 레벨) — concrete 전송은 각 호스트(things-factory 등)가 주입.
|
|
144
|
+
* → 컴포넌트가 특정 호스트 구현(things-factory)에 갇히지 않고, 어디서든 개발/재사용 가능.
|
|
145
|
+
* 컴포넌트는 tenantId 등을 모르는 부분 커맨드를 던지고, 호스트가 보강한다(commandId/type/args 만 제공).
|
|
146
|
+
*/
|
|
147
|
+
export interface TwinCommandChannel {
|
|
148
|
+
dispatch(command: Pick<Command, 'commandId' | 'type' | 'args'>): Promise<CommandAck>;
|
|
149
|
+
}
|
|
106
150
|
export interface RateSpec {
|
|
107
151
|
distribution: 'poisson' | 'uniform' | 'constant' | 'profile';
|
|
108
152
|
meanPerHour: number;
|
|
@@ -123,7 +167,13 @@ export interface ContentSpec {
|
|
|
123
167
|
};
|
|
124
168
|
}
|
|
125
169
|
export interface GeneratorSpec {
|
|
126
|
-
kind
|
|
170
|
+
/** 도메인 소유 라벨 — 무엇을 생성하는 자극인지 도메인이 명명(OrderState.kind 선례, 무방언). 코어는 강제하지 않음. */
|
|
171
|
+
kind: string;
|
|
172
|
+
/**
|
|
173
|
+
* 코어 라우팅 클래스: 공급(arrival, 물건이 들어옴 → onArrival) vs 수요(order, 요청이 들어옴 → onOrder).
|
|
174
|
+
* 미지정 시 레거시 kind('outbound-order'→order, 그 외→arrival)로 추론(하위호환, 카탈로그 nodeTypes 폴백과 동형).
|
|
175
|
+
*/
|
|
176
|
+
stimulus?: 'arrival' | 'order';
|
|
127
177
|
rate: RateSpec;
|
|
128
178
|
content: ContentSpec;
|
|
129
179
|
window?: {
|
|
@@ -148,9 +198,18 @@ export declare const OP_EVENT: {
|
|
|
148
198
|
readonly task: "task.status";
|
|
149
199
|
readonly equipment: "equipment.status";
|
|
150
200
|
readonly order: "order.status";
|
|
201
|
+
readonly quality: "quality.output";
|
|
151
202
|
};
|
|
203
|
+
/** 품질 산출 델타 — recordOutput(양품/불량) 시 방출. goodCount/scrapCount 는 무버 누적값. */
|
|
204
|
+
export interface QualityDelta {
|
|
205
|
+
moverId: string;
|
|
206
|
+
good: boolean;
|
|
207
|
+
goodCount: number;
|
|
208
|
+
scrapCount: number;
|
|
209
|
+
}
|
|
152
210
|
export interface TaskStatusDelta {
|
|
153
211
|
taskId: string;
|
|
212
|
+
orderId?: string;
|
|
154
213
|
kind: string;
|
|
155
214
|
status: TaskStatus;
|
|
156
215
|
fromNode?: string;
|
|
@@ -165,6 +224,12 @@ export interface EquipmentStatusDelta {
|
|
|
165
224
|
location?: string;
|
|
166
225
|
motion?: MoverMotion;
|
|
167
226
|
}
|
|
227
|
+
/** 관측된 오더 라인(SKU 데맨드) — 실 시스템 오더는 품목 라인을 가짐. 이행 예측(남은 데맨드 재계획)에 필요. */
|
|
228
|
+
export interface ObservedOrderLine {
|
|
229
|
+
gtin: string;
|
|
230
|
+
requested: number;
|
|
231
|
+
fulfilled?: number;
|
|
232
|
+
}
|
|
168
233
|
export interface OrderStatusDelta {
|
|
169
234
|
orderId: string;
|
|
170
235
|
kind: string;
|
|
@@ -172,11 +237,23 @@ export interface OrderStatusDelta {
|
|
|
172
237
|
requested: number;
|
|
173
238
|
fulfilled: number;
|
|
174
239
|
held?: boolean;
|
|
240
|
+
/**
|
|
241
|
+
* 오더 라인(SKU+수량) — 선택. 있으면 이행 예측이 "남은 데맨드(라인별 requested-fulfilled)를
|
|
242
|
+
* 현재 재고로 어떻게 채우나"를 재계획할 수 있다. 없으면 top-level 카운트만(이행 예측 불가, 재고 예측만).
|
|
243
|
+
*/
|
|
244
|
+
lines?: ObservedOrderLine[];
|
|
175
245
|
}
|
|
176
246
|
export declare const CMD: {
|
|
177
247
|
readonly orderHold: "order.hold";
|
|
178
248
|
readonly orderResume: "order.resume";
|
|
179
249
|
readonly orderRelease: "order.release";
|
|
250
|
+
readonly attentionAck: "attention.ack";
|
|
251
|
+
readonly resourceHold: "resource.hold";
|
|
252
|
+
readonly resourceResume: "resource.resume";
|
|
253
|
+
readonly resourceDown: "resource.down";
|
|
254
|
+
readonly resourceRepair: "resource.repair";
|
|
255
|
+
readonly resourceResetMetrics: "resource.reset-metrics";
|
|
256
|
+
readonly resourceAdd: "resource.add";
|
|
180
257
|
};
|
|
181
258
|
export type OperationalDelta = TaskStatusDelta | EquipmentStatusDelta | OrderStatusDelta;
|
|
182
259
|
export type EventHandler = (e: CanonicalEnvelope) => void;
|
|
@@ -186,6 +263,7 @@ export interface BoardDef {
|
|
|
186
263
|
id: string;
|
|
187
264
|
type: string;
|
|
188
265
|
capacity: number;
|
|
266
|
+
parentId?: string;
|
|
189
267
|
}[];
|
|
190
268
|
movers: {
|
|
191
269
|
id: string;
|
package/dist/contract.js
CHANGED
|
@@ -9,12 +9,21 @@
|
|
|
9
9
|
export const OP_EVENT = {
|
|
10
10
|
task: 'task.status',
|
|
11
11
|
equipment: 'equipment.status',
|
|
12
|
-
order: 'order.status'
|
|
12
|
+
order: 'order.status',
|
|
13
|
+
quality: 'quality.output' // 품질 산출(양품/불량) — OEE quality 입력. live 누적기가 이걸로 good/scrap 정확 추적.
|
|
13
14
|
};
|
|
14
15
|
// ── Command 채널 어휘 — 트윈의 "행위(act)" 면 (prescriptive/트랜잭션 프론트엔드) ──
|
|
15
16
|
// 코어 공통: order.hold/resume(할당 보류). 도메인: order.release(즉시 투입) 등은 handleCommand 로.
|
|
16
17
|
export const CMD = {
|
|
17
18
|
orderHold: 'order.hold',
|
|
18
19
|
orderResume: 'order.resume',
|
|
19
|
-
orderRelease: 'order.release'
|
|
20
|
+
orderRelease: 'order.release',
|
|
21
|
+
attentionAck: 'attention.ack', // 주목 신호 확인(OPC UA A&C acknowledge) — args:{id}
|
|
22
|
+
// Operable 코어 — 모든 operable 자원(설비·이동무버) 공통. args:{resourceId}. capability-keyed(무방언, equipment.* 아님).
|
|
23
|
+
resourceHold: 'resource.hold', // 계획 정지(정비/오프라인) — 배정 스킵
|
|
24
|
+
resourceResume: 'resource.resume', // 계획 정지 해제
|
|
25
|
+
resourceDown: 'resource.down', // 비계획 고장 주입 — args:{resourceId, durationMs?}
|
|
26
|
+
resourceRepair: 'resource.repair', // 즉시 수리
|
|
27
|
+
resourceResetMetrics: 'resource.reset-metrics', // OEE 계측 창 리셋
|
|
28
|
+
resourceAdd: 'resource.add' // 라이브 자원(무버) 추가 — args:{kind, homeNode, count?}. 런타임 구조 변이(what-if 아닌 실제 act)
|
|
20
29
|
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { CapabilityKey } from './capability.ts';
|
|
2
|
+
export type DomainSystem = 'wms' | 'yms' | 'mes';
|
|
3
|
+
export interface TwinTypeInfo {
|
|
4
|
+
/** 커널 권위 키 (예: 'storage', 'forklift'). */
|
|
5
|
+
key: string;
|
|
6
|
+
/** 로케이션(수동) vs 자원(능동). */
|
|
7
|
+
role: 'node' | 'mover';
|
|
8
|
+
label: string;
|
|
9
|
+
/** ① 표준 온톨로지 투영(03-reference-standards). 열린 문자열 — 하드코딩 enum 금지. */
|
|
10
|
+
standardClass: {
|
|
11
|
+
epcis?: string;
|
|
12
|
+
isa95?: string;
|
|
13
|
+
iso55000?: string;
|
|
14
|
+
};
|
|
15
|
+
/** ④ 식별자 스킴 — id 매칭 UX/검증 힌트(열린 문자열). 예: 'gs1:SGLN' | 'gs1:GIAI' | 'kernel:id'. */
|
|
16
|
+
identity: {
|
|
17
|
+
scheme: string;
|
|
18
|
+
};
|
|
19
|
+
/** ② 능력 프로파일(ADR-0018 정제) — 이 타입이 무엇을 하나. 엔티티→capability→컴포넌트 매핑의 SSOT. */
|
|
20
|
+
capabilities: CapabilityKey[];
|
|
21
|
+
}
|
|
22
|
+
export interface DomainProfileInfo {
|
|
23
|
+
system: DomainSystem;
|
|
24
|
+
label: string;
|
|
25
|
+
/** 노드+무버 트윈 타입 — board·UI 팔레트 소싱 SSOT. */
|
|
26
|
+
types: TwinTypeInfo[];
|
|
27
|
+
/** 로케이션 노드 타입 키(하위호환 파생 뷰 = types 중 role==='node'). 신규 소싱은 types 사용. */
|
|
28
|
+
nodeTypes: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
export declare const DOMAIN_CATALOG: Record<DomainSystem, DomainProfileInfo>;
|
|
31
|
+
export declare const DOMAIN_SYSTEMS: DomainSystem[];
|
|
32
|
+
/** 타입 키 → 능력 프로파일(커널 SSOT). 호스트가 라이브 페이로드에 투영, 컴포넌트가 능력을 렌더. */
|
|
33
|
+
export declare function capabilitiesForType(system: DomainSystem, typeKey: string): CapabilityKey[];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { WMS_TYPES } from "./wms-profile.js";
|
|
2
|
+
import { YMS_TYPES } from "./yms-profile.js";
|
|
3
|
+
import { MES_TYPES } from "./mes-profile.js";
|
|
4
|
+
const nodeKeys = (types) => types.filter(t => t.role === 'node').map(t => t.key);
|
|
5
|
+
export const DOMAIN_CATALOG = {
|
|
6
|
+
wms: { system: 'wms', label: 'WMS (물류창고)', types: WMS_TYPES, nodeTypes: nodeKeys(WMS_TYPES) },
|
|
7
|
+
yms: { system: 'yms', label: 'YMS (야드)', types: YMS_TYPES, nodeTypes: nodeKeys(YMS_TYPES) },
|
|
8
|
+
mes: { system: 'mes', label: 'MES (제조)', types: MES_TYPES, nodeTypes: nodeKeys(MES_TYPES) }
|
|
9
|
+
};
|
|
10
|
+
export const DOMAIN_SYSTEMS = ['wms', 'yms', 'mes'];
|
|
11
|
+
/** 타입 키 → 능력 프로파일(커널 SSOT). 호스트가 라이브 페이로드에 투영, 컴포넌트가 능력을 렌더. */
|
|
12
|
+
export function capabilitiesForType(system, typeKey) {
|
|
13
|
+
return DOMAIN_CATALOG[system]?.types.find(t => t.key === typeKey)?.capabilities ?? [];
|
|
14
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** 표준 온톨로지 투영(열린 문자열 — 하드코딩 enum 금지). */
|
|
2
|
+
export interface StandardClass {
|
|
3
|
+
epcis?: string;
|
|
4
|
+
isa95?: string;
|
|
5
|
+
iso55000?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface Identity {
|
|
8
|
+
/** GS1/커널 식별 스킴 힌트. 예: 'gs1:SGLN' | 'gs1:GIAI' | 'gs1:SSCC'. */
|
|
9
|
+
scheme: string;
|
|
10
|
+
}
|
|
11
|
+
/** 자재/품목 클래스. */
|
|
12
|
+
export interface MaterialDef {
|
|
13
|
+
key: string;
|
|
14
|
+
label: string;
|
|
15
|
+
identity?: Identity;
|
|
16
|
+
}
|
|
17
|
+
/** 로케이션(수동 위치) 타입. */
|
|
18
|
+
export interface NodeTypeDef {
|
|
19
|
+
key: string;
|
|
20
|
+
label: string;
|
|
21
|
+
standardClass?: StandardClass;
|
|
22
|
+
identity?: Identity;
|
|
23
|
+
capabilities?: string[];
|
|
24
|
+
}
|
|
25
|
+
/** 자원(능동 설비·무버) 타입. */
|
|
26
|
+
export interface ResourceTypeDef {
|
|
27
|
+
key: string;
|
|
28
|
+
label: string;
|
|
29
|
+
standardClass?: StandardClass;
|
|
30
|
+
identity?: Identity;
|
|
31
|
+
capabilities?: string[];
|
|
32
|
+
}
|
|
33
|
+
/** 작업 의도 — FlowTask.intent 와 정합(ISA-95 이동 vs 변환). */
|
|
34
|
+
export type OperationIntent = 'transport' | 'process' | 'dwell';
|
|
35
|
+
/** 오퍼레이션 타입(공정 한 작업) — ISA-95 OperationsDefinition. */
|
|
36
|
+
export interface OperationDef {
|
|
37
|
+
key: string;
|
|
38
|
+
label: string;
|
|
39
|
+
intent: OperationIntent;
|
|
40
|
+
/** 오퍼레이션이 수행되는 노드 타입(NodeTypeDef.key). 커널이 nodeByType 로 위치 해소. */
|
|
41
|
+
nodeType?: string;
|
|
42
|
+
/** 요구 자원 종류(ResourceTypeDef.key). transport/process 는 자원 필요, dwell 은 무자원. */
|
|
43
|
+
resourceType?: string;
|
|
44
|
+
/** CBV bizStep URN(방출 이벤트 어휘). */
|
|
45
|
+
bizStep?: string;
|
|
46
|
+
}
|
|
47
|
+
/** 라우트(오퍼레이션 시퀀스) — ISA-95 ProcessSegment 연결. */
|
|
48
|
+
export interface RouteDef {
|
|
49
|
+
key: string;
|
|
50
|
+
label: string;
|
|
51
|
+
/** OperationDef.key 순서. */
|
|
52
|
+
steps: string[];
|
|
53
|
+
}
|
|
54
|
+
export interface RecipePart {
|
|
55
|
+
/** MaterialDef.key. */
|
|
56
|
+
material: string;
|
|
57
|
+
qty: number;
|
|
58
|
+
}
|
|
59
|
+
/** BOM/레시피 — ISA-95 Material Consumed/Produced · EPCIS TransformationEvent(input→output). */
|
|
60
|
+
export interface RecipeDef {
|
|
61
|
+
key: string;
|
|
62
|
+
label: string;
|
|
63
|
+
inputs: RecipePart[];
|
|
64
|
+
outputs: RecipePart[];
|
|
65
|
+
/** RouteDef.key(선택) — 이 산출물을 만드는 공정 경로. */
|
|
66
|
+
route?: string;
|
|
67
|
+
}
|
|
68
|
+
/** CBV 어휘(bizStep·거래유형). */
|
|
69
|
+
export interface DomainVocabulary {
|
|
70
|
+
bizSteps?: Record<string, string>;
|
|
71
|
+
transactionTypes?: Record<string, string>;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 도메인 정의 — 커널이 소비하는 실행 계약. (스토어 메타 version/supplier 는 catalog 래퍼가 얹음.)
|
|
75
|
+
*/
|
|
76
|
+
export interface DomainDefinition {
|
|
77
|
+
id: string;
|
|
78
|
+
label: string;
|
|
79
|
+
vocabulary?: DomainVocabulary;
|
|
80
|
+
materials?: MaterialDef[];
|
|
81
|
+
nodeTypes: NodeTypeDef[];
|
|
82
|
+
resourceTypes: ResourceTypeDef[];
|
|
83
|
+
operations?: OperationDef[];
|
|
84
|
+
routes?: RouteDef[];
|
|
85
|
+
recipes?: RecipeDef[];
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 구조·참조무결성 검증(zero-dep, `validateEpcisEvent` 스타일). 위반 목록 반환(빈 배열 = 유효).
|
|
89
|
+
* 도메인 정의는 데이터 아티팩트라 로드 시점에 런타임 검증한다(컴파일타임 아님).
|
|
90
|
+
*/
|
|
91
|
+
export declare function validateDomainDefinition(def: DomainDefinition): string[];
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 도메인 정의(DomainDefinition) — 커널의 선언적 도메인 계약. `BoardDef`(토폴로지 입력)의 확장 격.
|
|
3
|
+
* 커널이 "무엇이 있고 어떻게 흐르나"(타입 + 공정 route/BOM)를 **데이터로** 받는 형식. zero-dep(자기 타입).
|
|
4
|
+
*
|
|
5
|
+
* 특정 공정이 커널 코드에 하드코딩되던 것을 이 데이터 계약으로 대체한다(design/plans/domain-catalog-layering.md).
|
|
6
|
+
* 스토어 패키지 `@operato/twin-catalog` 가 이 타입을 import 해 스토어 메타(version/supplier)를 얹어 배포한다(catalog → kernel 의존).
|
|
7
|
+
* 표준 앵커: GS1 EPCIS 2.0(bizStep) · ISA-95(WorkCenter·OperationsDefinition·BOM) · ISO 55000(Asset).
|
|
8
|
+
*/
|
|
9
|
+
const INTENTS = ['transport', 'process', 'dwell'];
|
|
10
|
+
function dupes(keys) {
|
|
11
|
+
const seen = new Set();
|
|
12
|
+
const dup = new Set();
|
|
13
|
+
for (const k of keys) {
|
|
14
|
+
if (seen.has(k))
|
|
15
|
+
dup.add(k);
|
|
16
|
+
else
|
|
17
|
+
seen.add(k);
|
|
18
|
+
}
|
|
19
|
+
return [...dup];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 구조·참조무결성 검증(zero-dep, `validateEpcisEvent` 스타일). 위반 목록 반환(빈 배열 = 유효).
|
|
23
|
+
* 도메인 정의는 데이터 아티팩트라 로드 시점에 런타임 검증한다(컴파일타임 아님).
|
|
24
|
+
*/
|
|
25
|
+
export function validateDomainDefinition(def) {
|
|
26
|
+
const v = [];
|
|
27
|
+
if (!def || typeof def !== 'object')
|
|
28
|
+
return ['domain definition 없음/객체 아님'];
|
|
29
|
+
if (typeof def.id !== 'string' || !def.id)
|
|
30
|
+
v.push('id 누락');
|
|
31
|
+
if (typeof def.label !== 'string' || !def.label)
|
|
32
|
+
v.push('label 누락');
|
|
33
|
+
if (!Array.isArray(def.nodeTypes) || def.nodeTypes.length === 0)
|
|
34
|
+
v.push('nodeTypes 비어있음');
|
|
35
|
+
if (!Array.isArray(def.resourceTypes))
|
|
36
|
+
v.push('resourceTypes 배열 아님');
|
|
37
|
+
const nodeKeys = new Set((def.nodeTypes || []).map(n => n.key));
|
|
38
|
+
const resKeys = new Set((def.resourceTypes || []).map(r => r.key));
|
|
39
|
+
const matKeys = new Set((def.materials || []).map(m => m.key));
|
|
40
|
+
const opKeys = new Set((def.operations || []).map(o => o.key));
|
|
41
|
+
const routeKeys = new Set((def.routes || []).map(r => r.key));
|
|
42
|
+
for (const [name, arr] of [['nodeTypes', def.nodeTypes], ['resourceTypes', def.resourceTypes], ['materials', def.materials], ['operations', def.operations], ['routes', def.routes], ['recipes', def.recipes]]) {
|
|
43
|
+
for (const d of dupes((arr || []).map(x => x.key)))
|
|
44
|
+
v.push(`${name} 키 중복: ${d}`);
|
|
45
|
+
}
|
|
46
|
+
for (const o of def.operations || []) {
|
|
47
|
+
if (!INTENTS.includes(o.intent))
|
|
48
|
+
v.push(`operation '${o.key}' intent 부정: ${o.intent}`);
|
|
49
|
+
if (o.nodeType && !nodeKeys.has(o.nodeType))
|
|
50
|
+
v.push(`operation '${o.key}' nodeType '${o.nodeType}' 미정의`);
|
|
51
|
+
if (o.resourceType && !resKeys.has(o.resourceType))
|
|
52
|
+
v.push(`operation '${o.key}' resourceType '${o.resourceType}' 미정의`);
|
|
53
|
+
if (o.intent === 'dwell' && o.resourceType)
|
|
54
|
+
v.push(`operation '${o.key}' dwell 인데 resourceType 지정됨(무자원이어야)`);
|
|
55
|
+
}
|
|
56
|
+
for (const r of def.routes || []) {
|
|
57
|
+
if (!Array.isArray(r.steps) || r.steps.length === 0)
|
|
58
|
+
v.push(`route '${r.key}' steps 비어있음`);
|
|
59
|
+
for (const s of r.steps || [])
|
|
60
|
+
if (!opKeys.has(s))
|
|
61
|
+
v.push(`route '${r.key}' step '${s}' 미정의 operation`);
|
|
62
|
+
}
|
|
63
|
+
for (const rc of def.recipes || []) {
|
|
64
|
+
if (rc.route && !routeKeys.has(rc.route))
|
|
65
|
+
v.push(`recipe '${rc.key}' route '${rc.route}' 미정의`);
|
|
66
|
+
if (!rc.outputs?.length)
|
|
67
|
+
v.push(`recipe '${rc.key}' outputs 비어있음`);
|
|
68
|
+
for (const p of [...(rc.inputs || []), ...(rc.outputs || [])]) {
|
|
69
|
+
if (!matKeys.has(p.material))
|
|
70
|
+
v.push(`recipe '${rc.key}' material '${p.material}' 미정의`);
|
|
71
|
+
if (typeof p.qty !== 'number' || p.qty <= 0)
|
|
72
|
+
v.push(`recipe '${rc.key}' material '${p.material}' qty 부정`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return v;
|
|
76
|
+
}
|
package/dist/flow-engine.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BoardDef, Command, CommandAck, EventHandler, MoverMotion, GeneratorSpec, ScenarioControl, StateSnapshot, TwinKernel, Unsubscribe } from './contract.ts';
|
|
1
|
+
import type { Attention, BoardDef, Command, CommandAck, EventHandler, MoverMotion, OeeMetrics, GeneratorSpec, ScenarioControl, StateSnapshot, TwinKernel, Unsubscribe, NodeState, ItemState, MoverState, OrderStatusDelta } from './contract.ts';
|
|
2
2
|
import type { EpcisEvent, BizTransactionElement } from './epcis.ts';
|
|
3
3
|
import type { AllocationPolicy, SlotView } from './allocation-policy.ts';
|
|
4
4
|
import type { DurationEstimator, DurationContext } from './duration-estimator.ts';
|
|
@@ -8,6 +8,7 @@ export interface FlowNode {
|
|
|
8
8
|
capacity: number;
|
|
9
9
|
occupancy: number;
|
|
10
10
|
status: string;
|
|
11
|
+
parentId?: string;
|
|
11
12
|
}
|
|
12
13
|
export interface FlowItem {
|
|
13
14
|
epc: string;
|
|
@@ -33,6 +34,9 @@ export interface FlowMover {
|
|
|
33
34
|
mttrMs?: number;
|
|
34
35
|
nextFailureMs?: number;
|
|
35
36
|
repairUntilMs?: number;
|
|
37
|
+
held?: boolean;
|
|
38
|
+
holdMs?: number;
|
|
39
|
+
metricsSinceMs?: number;
|
|
36
40
|
}
|
|
37
41
|
export interface FlowTask {
|
|
38
42
|
id: string;
|
|
@@ -82,6 +86,45 @@ interface Rng {
|
|
|
82
86
|
(): number;
|
|
83
87
|
state: number;
|
|
84
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* 주목신호 계산(순수) — State 스냅샷(movers/nodes/orders)에서 attentions 파생.
|
|
91
|
+
* FlowEngine.computeAttentions(sim) 와 live projector 미러가 **공유**하는 계산 층(face2-inbound-live §1.1):
|
|
92
|
+
* sim/live 가 같은 임계값·규칙을 쓴다. good/scrap 미제공(관측 상태에 없음)이면 scrap-high 는 자연 스킵(metric 층 갭).
|
|
93
|
+
*/
|
|
94
|
+
export declare function deriveAttentions(view: {
|
|
95
|
+
movers: {
|
|
96
|
+
id: string;
|
|
97
|
+
status?: string;
|
|
98
|
+
location?: string;
|
|
99
|
+
goodCount?: number;
|
|
100
|
+
scrapCount?: number;
|
|
101
|
+
}[];
|
|
102
|
+
nodes: {
|
|
103
|
+
id: string;
|
|
104
|
+
capacity?: number;
|
|
105
|
+
occupancy?: number;
|
|
106
|
+
}[];
|
|
107
|
+
orders: {
|
|
108
|
+
id: string;
|
|
109
|
+
held?: boolean;
|
|
110
|
+
}[];
|
|
111
|
+
}, acked?: ReadonlySet<string>): Attention[];
|
|
112
|
+
/** OEE 계측 카운터 — sim 은 tick 으로 누적, live 는 실 텔레메트리 또는 이벤트 누적기가 채운다(face2-inbound-live §1.1). */
|
|
113
|
+
export interface OeeCounters {
|
|
114
|
+
runMs: number;
|
|
115
|
+
setupMs: number;
|
|
116
|
+
downMs: number;
|
|
117
|
+
goodCount: number;
|
|
118
|
+
scrapCount: number;
|
|
119
|
+
holdMs?: number;
|
|
120
|
+
metricsSinceMs?: number;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* OEE 공식(순수) — 누적 카운터 + 현재 시각 → OEE. sim(oeeOf)과 live 가 **공유하는 계산 층**(face2-inbound-live §1.1).
|
|
124
|
+
* ⚠ attentions 와 달리: 입력(카운터)이 관측 State·이벤트에 없다 → live 는 카운터를 텔레메트리(정확) 또는
|
|
125
|
+
* 이벤트 누적기(근사)로 별도 공급해야 한다. 공식만 공유되고 입력원은 live 데이터-생산 결정.
|
|
126
|
+
*/
|
|
127
|
+
export declare function computeOee(c: OeeCounters, nowMs: number): OeeMetrics;
|
|
85
128
|
export declare abstract class FlowEngine implements TwinKernel {
|
|
86
129
|
tenantId: string;
|
|
87
130
|
nodes: Map<string, FlowNode>;
|
|
@@ -118,18 +161,51 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
118
161
|
*/
|
|
119
162
|
protected abstract onTaskComplete(task: FlowTask): void;
|
|
120
163
|
loadBoard(def: BoardDef): void;
|
|
164
|
+
/**
|
|
165
|
+
* what-if 구성 변주 — fork(또는 실행 중) 엔진에 무버 추가. loadBoard 무버 삽입과 동일 규약.
|
|
166
|
+
* 기본은 mtbf 미지정(고장 없는 신뢰 자원) → sampleExp(rng) 무소비라 baseline fork 와 깨끗이 비교 가능.
|
|
167
|
+
*/
|
|
168
|
+
addMover(m: {
|
|
169
|
+
id: string;
|
|
170
|
+
kind: string;
|
|
171
|
+
homeNode: string;
|
|
172
|
+
mtbfMs?: number;
|
|
173
|
+
mttrMs?: number;
|
|
174
|
+
}): void;
|
|
175
|
+
/**
|
|
176
|
+
* 관측 상태 주입(라이브 예측용, kernel-unification P1) — 외부 관측 스냅샷(재고·무버·노드)과
|
|
177
|
+
* 저널 오더(원값+라인)로 이 커널의 맵을 채운다. tick 으로 만든 게 아니라 "현재 관측된 현실"을 심어
|
|
178
|
+
* 이후 fork/tick 으로 예측한다. 라이브 런타임은 여전히 projector 미러 — 이 커널은 예측용 임시본.
|
|
179
|
+
* 오더는 남은 데맨드(라인별 requested-fulfilled)를 'created' 로 복원(현재 재고에서 재계획).
|
|
180
|
+
* 진행 중 개별 task 의 내부 상태는 관측만으론 복원 불가 → 재계획에 맡김(정직한 한계).
|
|
181
|
+
*/
|
|
182
|
+
hydrateObserved(snap: {
|
|
183
|
+
nodes: NodeState[];
|
|
184
|
+
items: ItemState[];
|
|
185
|
+
movers: MoverState[];
|
|
186
|
+
}, orders?: OrderStatusDelta[]): void;
|
|
187
|
+
/** what-if 구성 변주 — 노드 용량 변경(fork 대상). 존재하면 true. */
|
|
188
|
+
setNodeCapacity(nodeId: string, capacity: number): boolean;
|
|
189
|
+
/**
|
|
190
|
+
* forecast 몬테카를로 — fork 의 RNG 만 재시드(시나리오·상태·gens·in-flight 는 보존).
|
|
191
|
+
* "현재 조건 지속"을 유지한 채 **미래 확률만** 변주(도착·고장 타이밍 등) → run 마다 다른 표본.
|
|
192
|
+
* (monteCarloForecast 은 scenario.load 로 gens 를 갈아끼우므로 "현재 조건"이 깨진다 — 그 대안.)
|
|
193
|
+
*/
|
|
194
|
+
reseed(seed: number): void;
|
|
121
195
|
onEvent(handler: EventHandler): Unsubscribe;
|
|
122
196
|
/**
|
|
123
197
|
* Command 채널 — 트윈의 "행위(act)" 면. 코어 공통 커맨드(order.hold/resume)는 여기서,
|
|
124
198
|
* 도메인 커맨드(order.release 등)는 handleCommand 로 위임. 커맨드는 sim 상태를 변이하고
|
|
125
199
|
* State 델타를 유발한다(command → 행위 → 관측 폐루프).
|
|
126
200
|
*/
|
|
201
|
+
private _acked;
|
|
127
202
|
dispatch(cmd: Command): CommandAck;
|
|
128
203
|
/** 도메인 커맨드 처리(order.release 등). 기본은 거절 — 도메인이 override. */
|
|
129
204
|
protected handleCommand(cmd: Command): CommandAck;
|
|
130
205
|
readonly scenario: ScenarioControl;
|
|
131
206
|
tick(dtMs: number): void;
|
|
132
207
|
getSnapshot(): StateSnapshot;
|
|
208
|
+
protected computeAttentions(): Attention[];
|
|
133
209
|
/**
|
|
134
210
|
* fork — 현재 상태를 정확히 복제한 새 엔진 (디지털트윈 본연: "현재로부터 예측").
|
|
135
211
|
* 원본(live/sim)은 계속 진행, fork 는 what-if 를 앞으로 굴려 forecast·발산(predicted vs actual) 검사에 쓴다.
|