@operato/twin-kernel 0.2.2 → 0.3.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/README.md +46 -5
- package/dist/capability.js +2 -2
- package/dist/contract.d.ts +336 -34
- package/dist/contract.js +199 -7
- package/dist/divergence.d.ts +1 -1
- package/dist/divergence.js +3 -3
- package/dist/domain-catalog.d.ts +4 -4
- package/dist/domain-catalog.js +4 -4
- package/dist/domain-definition.d.ts +5 -5
- package/dist/domain-definition.js +6 -6
- package/dist/event-journal.d.ts +1 -1
- package/dist/event-journal.js +1 -1
- package/dist/flow-engine.d.ts +55 -31
- package/dist/flow-engine.js +153 -124
- package/dist/forecast.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/kernel.js +6 -6
- package/dist/mes-kernel.js +25 -25
- package/dist/mes-profile.d.ts +2 -2
- package/dist/mes-profile.js +11 -11
- package/dist/observed-reducer.d.ts +7 -7
- package/dist/observed-reducer.js +40 -30
- package/dist/task-fold.d.ts +2 -2
- package/dist/task-fold.js +4 -4
- package/dist/vocabulary.d.ts +19 -0
- package/dist/vocabulary.js +66 -0
- package/dist/wms-profile.d.ts +2 -2
- package/dist/wms-profile.js +6 -6
- package/dist/yms-kernel.js +10 -10
- package/dist/yms-profile.d.ts +2 -2
- package/dist/yms-profile.js +6 -6
- package/dist-cjs/index.cjs +413 -214
- package/package.json +1 -1
package/dist/contract.js
CHANGED
|
@@ -5,22 +5,168 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* 자리의 상태 — **포화도에서 파생한다.** 저장하는 값이 아니다.
|
|
7
7
|
*
|
|
8
|
-
* 예전에는 시뮬이 `'idle'` 로 두고 한 번도 바꾸지 않았고(변경 지점 0), 미러에는
|
|
8
|
+
* 예전에는 시뮬이 `'idle'` 로 두고 한 번도 바꾸지 않았고(변경 지점 0), 미러에는 자리 상태 채널이
|
|
9
9
|
* 없어 비어 있었다. 화면은 그 값을 그대로 보여 주고 있었다 — **정보처럼 보이는데 정보가 아니었다.**
|
|
10
10
|
*
|
|
11
11
|
* 문턱은 병목 주목(`deriveAttentions`)이 쓰는 것과 **같다**: 90% 이상이면 임박, 100% 이상이면 포화.
|
|
12
12
|
* 규칙이 둘이면 화면과 주목이 다른 말을 한다. 용량을 모르면 상태도 모른다(undefined — 꾸미지 않는다).
|
|
13
13
|
*/
|
|
14
|
-
export const
|
|
15
|
-
export function
|
|
14
|
+
export const LOCATION_SATURATION_NEAR = 0.9;
|
|
15
|
+
export function locationStatusOf(n) {
|
|
16
16
|
const cap = n.capacity;
|
|
17
17
|
if (!(typeof cap === 'number' && cap > 0))
|
|
18
18
|
return undefined;
|
|
19
19
|
const r = (n.occupancy ?? 0) / cap;
|
|
20
|
-
return r >= 1 ? 'full' : r >=
|
|
20
|
+
return r >= 1 ? 'full' : r >= LOCATION_SATURATION_NEAR ? 'near-full' : 'available';
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 설비 계층 단계 — **ISA-95 표준 어휘.** 1차 출처: B2MML `B2MML-Common.xsd` /
|
|
24
|
+
* `EquipmentLevel1Type` 열거값 + `EquipmentLevelType` 주석("role based equipment hierarchy level
|
|
25
|
+
* as defined in ISA 95").
|
|
26
|
+
*
|
|
27
|
+
* 우리가 단의 이름을 발명하지 않는다. "라인" 은 `ProductionLine`, "존" 은 `StorageZone` 으로
|
|
28
|
+
* 표준이 이미 정해 뒀다. 발명하면 그 순간 방언이 되고, 연동 상대와 매핑 표가 필요해진다.
|
|
29
|
+
*
|
|
30
|
+
* **`Other` 는 탈출구다** — 표준도 열거값 밖을 인정한다(`OtherValue` 속성). 억지로 끼워 맞추는 대신
|
|
31
|
+
* `Other` 로 두고 현장의 낱말은 `type` 에 남긴다.
|
|
32
|
+
*
|
|
33
|
+
* `StorageZone`·`StorageUnit` 도 이 계층 안에 있다. 다만 **자리 자체는 다른 축**이다 —
|
|
34
|
+
* ISA-95 는 `OperationalLocation`("자원이 놓이거나 놓일 것으로 예상되는 논리적·물리적 장소",
|
|
35
|
+
* `B2MML-OperationalLocation.xsd`)을 별도 스키마로 두고, `Equipment` 가 자기 위치를 그것으로 가리킨다.
|
|
36
|
+
* 우리 `locations` 가 그 개념이다(2026-08-01 개명 — `plans/isa95-coverage.md` §3-1).
|
|
37
|
+
*/
|
|
38
|
+
export const EQUIPMENT_LEVEL = [
|
|
39
|
+
'Enterprise',
|
|
40
|
+
'Site',
|
|
41
|
+
'Area',
|
|
42
|
+
'ProcessCell',
|
|
43
|
+
'Unit',
|
|
44
|
+
'ProductionLine',
|
|
45
|
+
'WorkCell',
|
|
46
|
+
'ProductionUnit',
|
|
47
|
+
'StorageZone',
|
|
48
|
+
'StorageUnit',
|
|
49
|
+
'WorkCenter',
|
|
50
|
+
'WorkUnit',
|
|
51
|
+
'EquipmentModule',
|
|
52
|
+
'ControlModule',
|
|
53
|
+
'Other'
|
|
54
|
+
];
|
|
55
|
+
/** 표준 열거값인지 — 상류에서 들어온 값을 조용히 통과시키지 않고 확인하는 용도. */
|
|
56
|
+
export function isEquipmentLevel(v) {
|
|
57
|
+
return typeof v === 'string' && EQUIPMENT_LEVEL.includes(v);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 계층 색인을 만든다. **순환은 만들 때 잡고 던진다** — 렌더 도중에 터지는 대신 여기서 한 번에.
|
|
61
|
+
* 순환을 조용히 잘라 내면 롤업이 틀린 값을 내고, 그건 이 함수가 막으려는 바로 그 실패다.
|
|
62
|
+
*/
|
|
63
|
+
export function hierarchyOf(s) {
|
|
64
|
+
const push = (m, k, v) => {
|
|
65
|
+
const cur = m.get(k);
|
|
66
|
+
if (cur)
|
|
67
|
+
cur.push(v);
|
|
68
|
+
else
|
|
69
|
+
m.set(k, [v]);
|
|
70
|
+
};
|
|
71
|
+
const parent = new Map();
|
|
72
|
+
const kids = new Map();
|
|
73
|
+
const known = new Set(s.locations.map(n => n.id));
|
|
74
|
+
for (const n of s.locations) {
|
|
75
|
+
if (!n.parentId || n.parentId === n.id)
|
|
76
|
+
continue; // 자기 부모는 선언 오류 — 사슬에 넣지 않는다
|
|
77
|
+
parent.set(n.id, n.parentId);
|
|
78
|
+
if (known.has(n.parentId))
|
|
79
|
+
push(kids, n.parentId, n.id);
|
|
80
|
+
}
|
|
81
|
+
/* 순환 검출 — 자리마다 사슬을 끝까지 밀어 본다. 방문한 자리를 다시 만나면 그 경로를 그대로 알린다. */
|
|
82
|
+
for (const start of known) {
|
|
83
|
+
const seen = [start];
|
|
84
|
+
for (let at = parent.get(start); at !== undefined; at = parent.get(at)) {
|
|
85
|
+
if (seen.includes(at))
|
|
86
|
+
throw new Error(`location hierarchy has a cycle: ${[...seen, at].join(' → ')}`);
|
|
87
|
+
seen.push(at);
|
|
88
|
+
if (!known.has(at))
|
|
89
|
+
break; // 구역에 닿았다 — 사슬 끝
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const homes = new Map();
|
|
93
|
+
for (const m of s.equipment ?? [])
|
|
94
|
+
if (m.homeLocation)
|
|
95
|
+
push(homes, m.homeLocation, m.id);
|
|
96
|
+
const ancestorsOf = (id) => {
|
|
97
|
+
const out = [];
|
|
98
|
+
for (let at = parent.get(id); at !== undefined; at = parent.get(at)) {
|
|
99
|
+
out.push(at);
|
|
100
|
+
if (!known.has(at))
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
};
|
|
105
|
+
const descendantsOf = (id) => {
|
|
106
|
+
const out = [];
|
|
107
|
+
const stack = [...(kids.get(id) ?? [])];
|
|
108
|
+
while (stack.length) {
|
|
109
|
+
const cur = stack.pop();
|
|
110
|
+
out.push(cur);
|
|
111
|
+
stack.push(...(kids.get(cur) ?? []));
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
};
|
|
115
|
+
const typeOf = new Map(s.locations.map(n => [n.id, n.type]));
|
|
116
|
+
const levelOf = new Map(s.locations.map(n => [n.id, n.level]));
|
|
117
|
+
return {
|
|
118
|
+
childrenOf: id => [...(kids.get(id) ?? [])],
|
|
119
|
+
ancestorsOf,
|
|
120
|
+
descendantsOf,
|
|
121
|
+
rollupOf: id => ancestorsOf(id).find(a => !known.has(a)),
|
|
122
|
+
ancestorOfLevel: (id, level) => ancestorsOf(id).find(a => known.has(a) && levelOf.get(a) === level),
|
|
123
|
+
ancestorOfType: (id, type) => ancestorsOf(id).find(a => known.has(a) && typeOf.get(a) === type),
|
|
124
|
+
equipmentOf: (id, opts) => {
|
|
125
|
+
const scope = opts?.deep ? [id, ...descendantsOf(id)] : [id];
|
|
126
|
+
return scope.flatMap(n => homes.get(n) ?? []);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* 등급 소속을 **상속을 타고 닫는다** — "이 개체가 이 등급으로 통하는가".
|
|
132
|
+
*
|
|
133
|
+
* 순환은 방문 집합으로 끊는다(잘못된 마스터가 무한 루프를 만들지 않게). 등급 정의가 없으면 소속
|
|
134
|
+
* 그대로만 본다 — 정의를 요구하지 않는다(정의를 싣지 않은 트윈이 그대로 돌아야 한다).
|
|
135
|
+
*
|
|
136
|
+
* `at` 를 주면 **유효 기간 밖의 등급은 제외**한다. 안 주면 기간을 보지 않는다(모르면 판단하지 않는다).
|
|
137
|
+
*/
|
|
138
|
+
export function classClosure(directIds, defs, at) {
|
|
139
|
+
const byId = new Map((defs ?? []).map(d => [d.id, d]));
|
|
140
|
+
const atMs = at ? Date.parse(at) : NaN;
|
|
141
|
+
const inWindow = (d) => {
|
|
142
|
+
if (!d || !Number.isFinite(atMs))
|
|
143
|
+
return true;
|
|
144
|
+
const from = d.effectiveStart ? Date.parse(d.effectiveStart) : NaN;
|
|
145
|
+
const to = d.effectiveEnd ? Date.parse(d.effectiveEnd) : NaN;
|
|
146
|
+
if (Number.isFinite(from) && atMs < from)
|
|
147
|
+
return false;
|
|
148
|
+
if (Number.isFinite(to) && atMs > to)
|
|
149
|
+
return false;
|
|
150
|
+
return true;
|
|
151
|
+
};
|
|
152
|
+
const out = new Set();
|
|
153
|
+
const stack = [...(directIds ?? [])];
|
|
154
|
+
while (stack.length) {
|
|
155
|
+
const id = stack.pop();
|
|
156
|
+
if (out.has(id))
|
|
157
|
+
continue;
|
|
158
|
+
const def = byId.get(id);
|
|
159
|
+
if (!inWindow(def))
|
|
160
|
+
continue; // 만료된 등급은 자기도, 그 상위도 타지 않는다
|
|
161
|
+
out.add(id);
|
|
162
|
+
for (const b of def?.baseIds ?? [])
|
|
163
|
+
if (!out.has(b))
|
|
164
|
+
stack.push(b);
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
21
167
|
}
|
|
22
168
|
// ── 운영 델타(비-EPCIS) — State 채널의 나머지 절반 ──────────────────────────
|
|
23
|
-
// EPCIS 이벤트는 재고/위치만 재구성 가능. tasks·
|
|
169
|
+
// EPCIS 이벤트는 재고/위치만 재구성 가능. tasks·equipment·orders 의 운영 상태는
|
|
24
170
|
// 이 델타로 미러한다. envelope.eventType = 'task.status' | 'equipment.status' | 'order.status'.
|
|
25
171
|
// (execution-model.md §5, roadmap 발견 gap: 운영 델타 이벤트화)
|
|
26
172
|
export const OP_EVENT = {
|
|
@@ -40,11 +186,57 @@ export const CMD = {
|
|
|
40
186
|
orderResume: 'order.resume',
|
|
41
187
|
orderRelease: 'order.release',
|
|
42
188
|
attentionAck: 'attention.ack', // 주목 신호 확인(OPC UA A&C acknowledge) — args:{id}
|
|
43
|
-
// Operable 코어 — 모든 operable 자원(
|
|
189
|
+
// Operable 코어 — 모든 operable 자원(설비·이동설비) 공통. args:{resourceId}. capability-keyed(무방언, equipment.* 아님).
|
|
44
190
|
resourceHold: 'resource.hold', // 계획 정지(정비/오프라인) — 배정 스킵
|
|
45
191
|
resourceResume: 'resource.resume', // 계획 정지 해제
|
|
46
192
|
resourceDown: 'resource.down', // 비계획 고장 주입 — args:{resourceId, durationMs?}
|
|
47
193
|
resourceRepair: 'resource.repair', // 즉시 수리
|
|
48
194
|
resourceResetMetrics: 'resource.reset-metrics', // OEE 계측 창 리셋
|
|
49
|
-
resourceAdd: 'resource.add' // 라이브 자원(
|
|
195
|
+
resourceAdd: 'resource.add' // 라이브 자원(설비) 추가 — args:{kind, homeLocation, count?}. 런타임 구조 변이(what-if 아닌 실제 act)
|
|
50
196
|
};
|
|
197
|
+
// ── 보드 청사진 바인딩 (최소) ──────────────────────────────────────────────
|
|
198
|
+
/**
|
|
199
|
+
* **저장된 보드를 읽는 단 하나의 입구.**
|
|
200
|
+
*
|
|
201
|
+
* `equipment` 로 개명하기 전에 저장된 보드는 `movers` 키를 갖고 있다(개명 시점 23개 인스턴스). (vocabulary-guard: allow — 읽기 호환 설명)
|
|
202
|
+
* 저장물을 다시 쓰지 않고 **읽을 때 흡수**한다 — 마이그레이션은 되돌리기 어렵고, 읽기 호환은 값싸다.
|
|
203
|
+
*
|
|
204
|
+
* 규율 둘:
|
|
205
|
+
* - 이 함수를 **거치지 않고** `def.equipment` 를 직접 읽는 코드를 두지 않는다. 하나라도 남으면
|
|
206
|
+
* 그 경로에서만 옛 보드의 설비가 조용히 사라진다(빈 배열).
|
|
207
|
+
* - **쓸 때는 새 이름만** 쓴다. 두 이름으로 쓰기 시작하면 저장물에 두 벌이 영구히 섞인다.
|
|
208
|
+
*
|
|
209
|
+
* 제거 시점: 저장된 보드가 모두 `equipment` 키로 바뀐 것이 확인되면(운영 데이터 점검 후) 이 함수는
|
|
210
|
+
* 사라진다. 그때까지 남겨 두는 이유를 여기 적어 두는 것이 주석의 일이다.
|
|
211
|
+
*/
|
|
212
|
+
/**
|
|
213
|
+
* **저장된 보드의 자리를 읽는 단 하나의 입구.** `readBoardEquipment` 와 같은 규율.
|
|
214
|
+
*
|
|
215
|
+
* `nodes` → `locations` 개명(2026-08-01) 전에 저장된 보드는 `nodes` 키를 갖고 있다(개명 시점 23개). (vocabulary-guard: allow — 읽기 호환 설명)
|
|
216
|
+
* 이 함수를 거치지 않고 `def.locations` 를 직접 읽는 코드를 두지 않는다 — 하나라도 남으면 그 경로에서만
|
|
217
|
+
* 옛 보드의 자리가 조용히 사라진다(빈 배열 = 자리 없는 트윈 = 아무 일도 일어나지 않는다).
|
|
218
|
+
*/
|
|
219
|
+
export function readBoardLocations(def) {
|
|
220
|
+
const d = def; // vocabulary-guard: allow — 옛 키 캐스트
|
|
221
|
+
return d.locations ?? d.nodes ?? []; // vocabulary-guard: allow — 옛 키 흡수
|
|
222
|
+
}
|
|
223
|
+
export function readBoardEquipment(def) {
|
|
224
|
+
const d = def; // vocabulary-guard: allow — 옛 키를 읽어야 하는 자리
|
|
225
|
+
const list = d.equipment ?? d.equipmentList ?? []; // vocabulary-guard: allow — 옛 키 흡수
|
|
226
|
+
/* 소속 자리 키도 함께 정규화한다 — `homeNode → homeLocation` 개명 전 보드가 23개 있다. (vocabulary-guard: allow — 옛 키 정규화 설명)
|
|
227
|
+
배열만 흡수하고 안쪽 키를 놓치면 설비는 나타나지만 **소속이 전부 비어** 롤업이 통째로 사라진다. */
|
|
228
|
+
return list.map(e => normalizeHomeLocation(e));
|
|
229
|
+
}
|
|
230
|
+
/** 소속 자리 키 정규화 — 설비·자산이 같은 규칙을 쓴다(둘 다 옛 이름이 저장돼 있다). */
|
|
231
|
+
function normalizeHomeLocation(entry) {
|
|
232
|
+
const legacy = entry.homeNode; // vocabulary-guard: allow — 옛 키를 읽어야 하는 자리
|
|
233
|
+
if (legacy === undefined || entry.homeLocation !== undefined)
|
|
234
|
+
return entry;
|
|
235
|
+
const { homeNode: _drop, ...rest } = entry; // vocabulary-guard: allow — 옛 키 제거
|
|
236
|
+
return { ...rest, homeLocation: legacy };
|
|
237
|
+
}
|
|
238
|
+
/** 저장된 보드의 반복사용 자산 — 설비와 같은 정규화를 거친다. */
|
|
239
|
+
export function readBoardAssets(def) {
|
|
240
|
+
const list = (def.assets ?? []);
|
|
241
|
+
return list.map(a => normalizeHomeLocation(a));
|
|
242
|
+
}
|
package/dist/divergence.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export interface StateDivergence {
|
|
|
8
8
|
hasDrift: boolean;
|
|
9
9
|
itemLocation: FieldDiff<string>[];
|
|
10
10
|
itemDisposition: FieldDiff<string>[];
|
|
11
|
-
|
|
11
|
+
locationOccupancy: FieldDiff<number>[];
|
|
12
12
|
orderStatus: FieldDiff<string>[];
|
|
13
13
|
}
|
|
14
14
|
/** predicted(fork forecast)와 actual(관측) 스냅샷을 대조. 같은 sim 시점에 호출하는 것이 의미 있음. */
|
package/dist/divergence.js
CHANGED
|
@@ -21,8 +21,8 @@ function diffBy(predicted, actual, idOf, valOf) {
|
|
|
21
21
|
export function compareStates(predicted, actual) {
|
|
22
22
|
const itemLocation = diffBy(predicted.items, actual.items, i => i.epc, i => i.location);
|
|
23
23
|
const itemDisposition = diffBy(predicted.items, actual.items, i => i.epc, i => i.disposition);
|
|
24
|
-
const
|
|
24
|
+
const locationOccupancy = diffBy(predicted.locations, actual.locations, n => n.id, n => n.occupancy);
|
|
25
25
|
const orderStatus = diffBy(predicted.orders, actual.orders, o => o.id, o => o.status);
|
|
26
|
-
const hasDrift = itemLocation.length > 0 || itemDisposition.length > 0 ||
|
|
27
|
-
return { hasDrift, itemLocation, itemDisposition,
|
|
26
|
+
const hasDrift = itemLocation.length > 0 || itemDisposition.length > 0 || locationOccupancy.length > 0 || orderStatus.length > 0;
|
|
27
|
+
return { hasDrift, itemLocation, itemDisposition, locationOccupancy, orderStatus };
|
|
28
28
|
}
|
package/dist/domain-catalog.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export interface TwinTypeInfo {
|
|
|
4
4
|
/** 커널 권위 키 (예: 'storage', 'forklift'). */
|
|
5
5
|
key: string;
|
|
6
6
|
/** 로케이션(수동) vs 자원(능동). */
|
|
7
|
-
role: '
|
|
7
|
+
role: 'location' | 'equipment';
|
|
8
8
|
label: string;
|
|
9
9
|
/** ① 표준 온톨로지 투영(03-reference-standards). 열린 문자열 — 하드코딩 enum 금지. */
|
|
10
10
|
standardClass: {
|
|
@@ -22,10 +22,10 @@ export interface TwinTypeInfo {
|
|
|
22
22
|
export interface DomainProfileInfo {
|
|
23
23
|
system: DomainSystem;
|
|
24
24
|
label: string;
|
|
25
|
-
/**
|
|
25
|
+
/** 자리+설비 트윈 타입 — board·UI 팔레트 소싱 SSOT. */
|
|
26
26
|
types: TwinTypeInfo[];
|
|
27
|
-
/** 로케이션
|
|
28
|
-
|
|
27
|
+
/** 로케이션 자리 타입 키(하위호환 파생 뷰 = types 중 role==='location'). 신규 소싱은 types 사용. */
|
|
28
|
+
locationTypes: readonly string[];
|
|
29
29
|
}
|
|
30
30
|
export declare const DOMAIN_CATALOG: Record<DomainSystem, DomainProfileInfo>;
|
|
31
31
|
export declare const DOMAIN_SYSTEMS: DomainSystem[];
|
package/dist/domain-catalog.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { WMS_TYPES } from "./wms-profile.js";
|
|
2
2
|
import { YMS_TYPES } from "./yms-profile.js";
|
|
3
3
|
import { MES_TYPES } from "./mes-profile.js";
|
|
4
|
-
const
|
|
4
|
+
const locationKeys = (types) => types.filter(t => t.role === 'location').map(t => t.key);
|
|
5
5
|
export const DOMAIN_CATALOG = {
|
|
6
6
|
// label 은 언어 중립 i18n 키(twin.system.<code>) — 사람 언어는 표현계층이 렌더(L2).
|
|
7
|
-
wms: { system: 'wms', label: 'twin.system.wms', types: WMS_TYPES,
|
|
8
|
-
yms: { system: 'yms', label: 'twin.system.yms', types: YMS_TYPES,
|
|
9
|
-
mes: { system: 'mes', label: 'twin.system.mes', types: MES_TYPES,
|
|
7
|
+
wms: { system: 'wms', label: 'twin.system.wms', types: WMS_TYPES, locationTypes: locationKeys(WMS_TYPES) },
|
|
8
|
+
yms: { system: 'yms', label: 'twin.system.yms', types: YMS_TYPES, locationTypes: locationKeys(YMS_TYPES) },
|
|
9
|
+
mes: { system: 'mes', label: 'twin.system.mes', types: MES_TYPES, locationTypes: locationKeys(MES_TYPES) }
|
|
10
10
|
};
|
|
11
11
|
export const DOMAIN_SYSTEMS = ['wms', 'yms', 'mes'];
|
|
12
12
|
/** 타입 키 → 능력 프로파일(커널 SSOT). 호스트가 라이브 페이로드에 투영, 컴포넌트가 능력을 렌더. */
|
|
@@ -15,14 +15,14 @@ export interface MaterialDef {
|
|
|
15
15
|
identity?: Identity;
|
|
16
16
|
}
|
|
17
17
|
/** 로케이션(수동 위치) 타입. */
|
|
18
|
-
export interface
|
|
18
|
+
export interface LocationTypeDef {
|
|
19
19
|
key: string;
|
|
20
20
|
label: string;
|
|
21
21
|
standardClass?: StandardClass;
|
|
22
22
|
identity?: Identity;
|
|
23
23
|
capabilities?: string[];
|
|
24
24
|
}
|
|
25
|
-
/** 자원(능동
|
|
25
|
+
/** 자원(능동 설비·설비) 타입. */
|
|
26
26
|
export interface ResourceTypeDef {
|
|
27
27
|
key: string;
|
|
28
28
|
label: string;
|
|
@@ -91,8 +91,8 @@ export interface OperationDef {
|
|
|
91
91
|
key: string;
|
|
92
92
|
label: string;
|
|
93
93
|
intent: OperationIntent;
|
|
94
|
-
/** 오퍼레이션이 수행되는
|
|
95
|
-
|
|
94
|
+
/** 오퍼레이션이 수행되는 자리 타입(LocationTypeDef.key). 커널이 locationByType 로 위치 해소. */
|
|
95
|
+
locationType?: string;
|
|
96
96
|
/** 요구 자원 종류(ResourceTypeDef.key). transport/process 는 자원 필요, dwell 은 무자원. */
|
|
97
97
|
resourceType?: string;
|
|
98
98
|
/** CBV bizStep URN(방출 이벤트 어휘). */
|
|
@@ -174,7 +174,7 @@ export interface DomainDefinition {
|
|
|
174
174
|
label: string;
|
|
175
175
|
vocabulary?: DomainVocabulary;
|
|
176
176
|
materials?: MaterialDef[];
|
|
177
|
-
|
|
177
|
+
locationTypes: LocationTypeDef[];
|
|
178
178
|
resourceTypes: ResourceTypeDef[];
|
|
179
179
|
operations?: OperationDef[];
|
|
180
180
|
routes?: RouteDef[];
|
|
@@ -41,24 +41,24 @@ export function validateDomainDefinition(def) {
|
|
|
41
41
|
v.push('id 누락');
|
|
42
42
|
if (typeof def.label !== 'string' || !def.label)
|
|
43
43
|
v.push('label 누락');
|
|
44
|
-
if (!Array.isArray(def.
|
|
45
|
-
v.push('
|
|
44
|
+
if (!Array.isArray(def.locationTypes) || def.locationTypes.length === 0)
|
|
45
|
+
v.push('locationTypes 비어있음');
|
|
46
46
|
if (!Array.isArray(def.resourceTypes))
|
|
47
47
|
v.push('resourceTypes 배열 아님');
|
|
48
|
-
const
|
|
48
|
+
const locationKeys = new Set((def.locationTypes || []).map(n => n.key));
|
|
49
49
|
const resKeys = new Set((def.resourceTypes || []).map(r => r.key));
|
|
50
50
|
const matKeys = new Set((def.materials || []).map(m => m.key));
|
|
51
51
|
const opKeys = new Set((def.operations || []).map(o => o.key));
|
|
52
52
|
const routeKeys = new Set((def.routes || []).map(r => r.key));
|
|
53
|
-
for (const [name, arr] of [['
|
|
53
|
+
for (const [name, arr] of [['locationTypes', def.locationTypes], ['resourceTypes', def.resourceTypes], ['materials', def.materials], ['operations', def.operations], ['routes', def.routes], ['recipes', def.recipes]]) {
|
|
54
54
|
for (const d of dupes((arr || []).map(x => x.key)))
|
|
55
55
|
v.push(`${name} 키 중복: ${d}`);
|
|
56
56
|
}
|
|
57
57
|
for (const o of def.operations || []) {
|
|
58
58
|
if (!INTENTS.includes(o.intent))
|
|
59
59
|
v.push(`operation '${o.key}' intent 부정: ${o.intent}`);
|
|
60
|
-
if (o.
|
|
61
|
-
v.push(`operation '${o.key}'
|
|
60
|
+
if (o.locationType && !locationKeys.has(o.locationType))
|
|
61
|
+
v.push(`operation '${o.key}' locationType '${o.locationType}' 미정의`);
|
|
62
62
|
if (o.resourceType && !resKeys.has(o.resourceType))
|
|
63
63
|
v.push(`operation '${o.key}' resourceType '${o.resourceType}' 미정의`);
|
|
64
64
|
if (o.intent === 'dwell' && o.resourceType)
|
package/dist/event-journal.d.ts
CHANGED
|
@@ -13,5 +13,5 @@ export declare class EventJournal {
|
|
|
13
13
|
/** sim 시각(ISO eventTime) 이하 이벤트. */
|
|
14
14
|
untilSimTime(iso: string): CanonicalEnvelope[];
|
|
15
15
|
}
|
|
16
|
-
/** 이벤트열 → 상태 재구성(시간여행). board = 마스터(
|
|
16
|
+
/** 이벤트열 → 상태 재구성(시간여행). board = 마스터(토폴로지·설비). */
|
|
17
17
|
export declare function replay(board: BoardDef, events: readonly CanonicalEnvelope[]): ProjectedState;
|
package/dist/event-journal.js
CHANGED
|
@@ -32,7 +32,7 @@ export class EventJournal {
|
|
|
32
32
|
return this.events.filter(e => e.eventTime <= iso);
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
/** 이벤트열 → 상태 재구성(시간여행). board = 마스터(
|
|
35
|
+
/** 이벤트열 → 상태 재구성(시간여행). board = 마스터(토폴로지·설비). */
|
|
36
36
|
export function replay(board, events) {
|
|
37
37
|
const proj = new StateProjector(board);
|
|
38
38
|
for (const e of events)
|
package/dist/flow-engine.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { Attention, BoardDef, CanonicalEnvelope, Command, CommandAck, EventHandler,
|
|
1
|
+
import type { ResourceProperty, ResourceClassDef, Attention, BoardDef, CanonicalEnvelope, Command, CommandAck, EventHandler, EquipmentMotion, OeeMetrics, AssetState, GeneratorSpec, OrderState, PersonState, ScenarioControl, StateSnapshot, TwinKernel, Unsubscribe, LocationState, ItemState, EquipmentState, OrderStatusDelta, TaskState } 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';
|
|
5
5
|
import type { OperationDef } from './domain-definition.ts';
|
|
6
|
-
export interface
|
|
6
|
+
export interface FlowLocation {
|
|
7
7
|
id: string;
|
|
8
8
|
type: string;
|
|
9
9
|
capacity: number;
|
|
@@ -39,29 +39,47 @@ export interface FlowItem {
|
|
|
39
39
|
/** 물리 자산 — 반복사용(팔레트·랙·용기). 설비도 물품도 아니다(GRAI vs SSCC 구분은 계약 주석 참조). */
|
|
40
40
|
export interface FlowAsset {
|
|
41
41
|
id: string;
|
|
42
|
-
|
|
42
|
+
/** 속한 등급들 — 표준 `PhysicalAssetClassID`(복수). */
|
|
43
|
+
assetClassIds?: string[];
|
|
43
44
|
location?: string;
|
|
44
45
|
status: string;
|
|
45
46
|
taskId: string | null;
|
|
46
47
|
carrying?: string;
|
|
48
|
+
/** 자원 속성 — 표준 `PhysicalAssetProperty`. */
|
|
49
|
+
properties?: ResourceProperty[];
|
|
50
|
+
/** 적격을 검증한 시험 명세들 — 표준 `TestSpecificationID`. */
|
|
51
|
+
testSpecificationIds?: string[];
|
|
47
52
|
}
|
|
48
53
|
/** 사람 — 설비와 별개 자원(고장·OEE 가 아니라 등급·교대로 산다). */
|
|
49
54
|
export interface FlowPerson {
|
|
50
55
|
id: string;
|
|
51
|
-
|
|
56
|
+
/** 속한 등급들 — 표준 `PersonnelClassID`(복수). 자격이 여럿인 사람을 표현한다. */
|
|
57
|
+
personnelClassIds?: string[];
|
|
52
58
|
status: string;
|
|
53
59
|
taskId: string | null;
|
|
54
60
|
window?: {
|
|
55
61
|
startHour: number;
|
|
56
62
|
endHour: number;
|
|
57
63
|
};
|
|
64
|
+
/** 지금 어디에 있나 — 표준 `Person.OperationalLocation`. 커널은 사람을 움직이지 않는다(마스터·관측이 말한다). */
|
|
65
|
+
location?: string;
|
|
66
|
+
/** 자원 속성 — 표준 `PersonProperty`. */
|
|
67
|
+
properties?: ResourceProperty[];
|
|
68
|
+
/** 자격을 검증한 시험 명세들 — 표준 `TestSpecificationID`. */
|
|
69
|
+
testSpecificationIds?: string[];
|
|
58
70
|
}
|
|
59
|
-
export interface
|
|
71
|
+
export interface FlowEquipment {
|
|
60
72
|
id: string;
|
|
61
73
|
kind: string;
|
|
62
74
|
location: string;
|
|
63
75
|
status: string;
|
|
64
76
|
taskId: string | null;
|
|
77
|
+
/** 붙박인 자리 — 소속. `location` 은 지금 위치. 고정 설비는 둘이 항상 같다(EquipmentState.homeLocation 참조). */
|
|
78
|
+
homeLocation?: string;
|
|
79
|
+
/** 자원 속성 — 표준 `EquipmentProperty`(§ResourceProperty). */
|
|
80
|
+
properties?: ResourceProperty[];
|
|
81
|
+
/** 적격을 검증한 시험 명세들 — 표준 `TestSpecificationID`. */
|
|
82
|
+
testSpecificationIds?: string[];
|
|
65
83
|
/** 교대(가동시간) — 지정 시 이 시간대에만 배정된다. 미지정=24시간 가용. */
|
|
66
84
|
window?: {
|
|
67
85
|
startHour: number;
|
|
@@ -106,9 +124,9 @@ export interface FlowTask {
|
|
|
106
124
|
appliedSetupMs?: number;
|
|
107
125
|
/**
|
|
108
126
|
* 작업 의도(씬 3 직교의도 정렬 · ISA-95 이동 vs 변환). 기본 'transport'.
|
|
109
|
-
* transport:
|
|
110
|
-
* process:
|
|
111
|
-
* dwell:
|
|
127
|
+
* transport: eq 가 item 을 fromNode→toNode 운반(완료 시 자원 위치 이동 + 이동 모션).
|
|
128
|
+
* process: 자리에서 item 변환(자원=정지 설비 — 위치 불변, 모션 없음). 가공을 거리-0 운반으로 위장 안 함.
|
|
129
|
+
* dwell: 자리에서 시간만 소비(무자원 체류·큐어링 — 즉시 진행, 자원 불요).
|
|
112
130
|
*/
|
|
113
131
|
intent?: 'transport' | 'process' | 'dwell';
|
|
114
132
|
}
|
|
@@ -138,19 +156,19 @@ interface Rng {
|
|
|
138
156
|
state: number;
|
|
139
157
|
}
|
|
140
158
|
/**
|
|
141
|
-
* 주목신호 계산(순수) — State 스냅샷(
|
|
159
|
+
* 주목신호 계산(순수) — State 스냅샷(equipment/locations/orders)에서 attentions 파생.
|
|
142
160
|
* FlowEngine.computeAttentions(sim) 와 live projector 미러가 **공유**하는 계산 층(face2-inbound-live §1.1):
|
|
143
161
|
* sim/live 가 같은 임계값·규칙을 쓴다. good/scrap 미제공(관측 상태에 없음)이면 scrap-high 는 자연 스킵(metric 층 갭).
|
|
144
162
|
*/
|
|
145
163
|
export declare function deriveAttentions(view: {
|
|
146
|
-
|
|
164
|
+
equipment: {
|
|
147
165
|
id: string;
|
|
148
166
|
status?: string;
|
|
149
167
|
location?: string;
|
|
150
168
|
goodCount?: number;
|
|
151
169
|
scrapCount?: number;
|
|
152
170
|
}[];
|
|
153
|
-
|
|
171
|
+
locations: {
|
|
154
172
|
id: string;
|
|
155
173
|
capacity?: number;
|
|
156
174
|
occupancy?: number;
|
|
@@ -178,9 +196,15 @@ export interface OeeCounters {
|
|
|
178
196
|
export declare function computeOee(c: OeeCounters, nowMs: number): OeeMetrics;
|
|
179
197
|
export declare abstract class FlowEngine implements TwinKernel {
|
|
180
198
|
tenantId: string;
|
|
181
|
-
|
|
199
|
+
locations: Map<string, FlowLocation>;
|
|
182
200
|
items: Map<string, FlowItem>;
|
|
183
|
-
|
|
201
|
+
equipment: Map<string, FlowEquipment>;
|
|
202
|
+
/** 등급 정의(표준 `<X>Class`) — 상속·유효기간 판정의 재료. 선언 안 하면 비어 있고, 소속 그대로 판정한다. */
|
|
203
|
+
protected classDefs: {
|
|
204
|
+
personnel?: ResourceClassDef[];
|
|
205
|
+
equipment?: ResourceClassDef[];
|
|
206
|
+
asset?: ResourceClassDef[];
|
|
207
|
+
};
|
|
184
208
|
/** 사람 — 선언하지 않으면 빈 맵(인원 제약 없는 트윈, 기존 거동). */
|
|
185
209
|
persons: Map<string, FlowPerson>;
|
|
186
210
|
/** 물리 자산 — 선언하지 않으면 빈 맵(자산 제약 없는 트윈, 기존 거동). */
|
|
@@ -233,34 +257,34 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
233
257
|
protected abstract onTaskComplete(task: FlowTask): void;
|
|
234
258
|
loadBoard(def: BoardDef): void;
|
|
235
259
|
/**
|
|
236
|
-
* what-if 구성 변주 — fork(또는 실행 중) 엔진에
|
|
260
|
+
* what-if 구성 변주 — fork(또는 실행 중) 엔진에 설비 추가. loadBoard 설비 삽입과 동일 규약.
|
|
237
261
|
* 기본은 mtbf 미지정(고장 없는 신뢰 자원) → sampleExp(rng) 무소비라 baseline fork 와 깨끗이 비교 가능.
|
|
238
262
|
*/
|
|
239
|
-
|
|
263
|
+
addEquipment(m: {
|
|
240
264
|
id: string;
|
|
241
265
|
kind: string;
|
|
242
|
-
|
|
266
|
+
homeLocation: string;
|
|
243
267
|
mtbfMs?: number;
|
|
244
268
|
mttrMs?: number;
|
|
245
269
|
}): void;
|
|
246
270
|
/**
|
|
247
|
-
* 관측 상태 주입(라이브 예측용, kernel-unification P1) — 외부 관측 스냅샷(
|
|
271
|
+
* 관측 상태 주입(라이브 예측용, kernel-unification P1) — 외부 관측 스냅샷(재고·설비·자리)과
|
|
248
272
|
* 저널 오더(원값+라인)로 이 커널의 맵을 채운다. tick 으로 만든 게 아니라 "현재 관측된 현실"을 심어
|
|
249
273
|
* 이후 fork/tick 으로 예측한다. 라이브 런타임은 여전히 projector 미러 — 이 커널은 예측용 임시본.
|
|
250
274
|
* 오더는 남은 데맨드(라인별 requested-fulfilled)를 'created' 로 복원(현재 재고에서 재계획).
|
|
251
275
|
* 진행 중 개별 task 의 내부 상태는 관측만으론 복원 불가 → 재계획에 맡김(정직한 한계).
|
|
252
276
|
*/
|
|
253
277
|
hydrateObserved(snap: {
|
|
254
|
-
|
|
278
|
+
locations: LocationState[];
|
|
255
279
|
items: ItemState[];
|
|
256
|
-
|
|
280
|
+
equipment: EquipmentState[];
|
|
257
281
|
persons?: PersonState[];
|
|
258
282
|
assets?: AssetState[];
|
|
259
283
|
tasks?: TaskState[];
|
|
260
284
|
orders?: OrderState[];
|
|
261
285
|
}, orders?: OrderStatusDelta[]): void;
|
|
262
|
-
/** what-if 구성 변주 —
|
|
263
|
-
|
|
286
|
+
/** what-if 구성 변주 — 자리 용량 변경(fork 대상). 존재하면 true. */
|
|
287
|
+
setLocationCapacity(locationId: string, capacity: number): boolean;
|
|
264
288
|
/**
|
|
265
289
|
* forecast 몬테카를로 — fork 의 RNG 만 재시드(시나리오·상태·gens·in-flight 는 보존).
|
|
266
290
|
* "현재 조건 지속"을 유지한 채 **미래 확률만** 변주(도착·고장 타이밍 등) → run 마다 다른 표본.
|
|
@@ -366,12 +390,12 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
366
390
|
gtin: string;
|
|
367
391
|
weight: number;
|
|
368
392
|
}[]): string;
|
|
369
|
-
protected
|
|
393
|
+
protected locationByType(type: string): FlowLocation | undefined;
|
|
370
394
|
/**
|
|
371
395
|
* process 변화 대수(EPCIS TransformationEvent · ISA-95 Material Consumed/Produced · 씬 Processable.transform).
|
|
372
396
|
* inputs 소비 → outputs 생산. 입출력 arity 가 곧 대수:
|
|
373
397
|
* merge N→1(조립) · split 1→N(분해) · transform 1→1(타입변경) · loss N→0(소실) · gain 0→N(부산물·생성).
|
|
374
|
-
* 아이템 상태(소비/생산)와
|
|
398
|
+
* 아이템 상태(소비/생산)와 자리 점유를 갱신하고 계보(TransformationEvent)를 방출한다.
|
|
375
399
|
* 도메인은 이 원시만 호출 — onTaskComplete 의 소비/생산/EPCIS 손코딩을 대체.
|
|
376
400
|
*/
|
|
377
401
|
protected transform(inputs: string[], outputs: {
|
|
@@ -429,7 +453,7 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
429
453
|
}): void;
|
|
430
454
|
/**
|
|
431
455
|
* containment 분해(EPCIS AggregationEvent DELETE) — 부모(용기)에서 자식들을 풀어냄.
|
|
432
|
-
* materialize 지정 시 자식이 독립 아이템으로 등장(materialize:
|
|
456
|
+
* materialize 지정 시 자식이 독립 아이템으로 등장(materialize: 자리 배치 + occupancy + ObjectEvent ADD).
|
|
433
457
|
* 미지정 시 이벤트만.
|
|
434
458
|
*/
|
|
435
459
|
protected disaggregate(parent: string, children: string[], opts: {
|
|
@@ -442,10 +466,10 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
442
466
|
}): void;
|
|
443
467
|
/** 품질 보고(OEE Quality) — 도메인이 완료 시 자원별 양품/불량 1건 계상(예: 용접 수율). */
|
|
444
468
|
protected recordOutput(moverId: string | null, good: boolean): void;
|
|
445
|
-
/**
|
|
469
|
+
/** 설비 OEE(스냅샷 파생) — Availability×Performance×Quality. planned = 설비 존재 sim 시간(clockMs). */
|
|
446
470
|
private oeeOf;
|
|
447
|
-
/** 정책에 넘길 특정 타입
|
|
448
|
-
protected slotViews(
|
|
471
|
+
/** 정책에 넘길 특정 타입 자리의 관측 뷰 — 예약(그 자리로 향하는 in-flight task) 포함. */
|
|
472
|
+
protected slotViews(locationType: string): SlotView[];
|
|
449
473
|
protected emit(event: EpcisEvent): void;
|
|
450
474
|
protected emitOp(eventType: string, data: unknown): void;
|
|
451
475
|
/**
|
|
@@ -461,7 +485,7 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
461
485
|
protected emitPerson(p: FlowPerson): void;
|
|
462
486
|
/** 설비 상태 전이 — `taskId` 를 함께 싣는다(사람·자산 델타와 같은 규칙). 없으면 미러가 "이 설비가
|
|
463
487
|
* 무슨 일을 하는 중인가" 를 알 수 없어 작업↔자원 연결이 한쪽에서만 성립한다. */
|
|
464
|
-
protected
|
|
488
|
+
protected emitEquipment(m: FlowEquipment, motion?: EquipmentMotion): void;
|
|
465
489
|
/** 오더 델타 — **라인까지 싣는다.** 라인이 빠지면 미러가 남은 데맨드를 라인별로 재계획할 수 없다. */
|
|
466
490
|
protected emitOrder(o: FlowOrder): void;
|
|
467
491
|
/**
|
|
@@ -529,7 +553,7 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
529
553
|
*/
|
|
530
554
|
private stationFull;
|
|
531
555
|
/** 교대 밖인가 — 자원이 지금 일하지 않는 이유 중 고장·계획정지와 구별되는 세 번째. */
|
|
532
|
-
protected offShift(m:
|
|
556
|
+
protected offShift(m: FlowEquipment): boolean;
|
|
533
557
|
/** 시뮬 시각의 시(0..23) — 운영시간·시간대 배율의 기준. */
|
|
534
558
|
protected hourOfDay(): number;
|
|
535
559
|
/**
|
|
@@ -541,9 +565,9 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
541
565
|
/** 지수분포 표본(고장/수리 간격) — mean 을 평균으로 하는 무기억 프로세스. */
|
|
542
566
|
private sampleExp;
|
|
543
567
|
/**
|
|
544
|
-
* 확률적 설비 고장(MTBF/MTTR) — mtbf 지정
|
|
568
|
+
* 확률적 설비 고장(MTBF/MTTR) — mtbf 지정 설비만 참여(미지정=고장 없음, rng 무소비 → byte-identical).
|
|
545
569
|
* up: nextFailure 도래 시 down(수리까지 repairUntil). down: downMs 누적, repair 도래 시 up(다음 고장 예약).
|
|
546
|
-
* down 중
|
|
570
|
+
* down 중 설비는 배정 불가 + 진행중 task 동결(processTasks 가 skip) → OEE Availability 손실.
|
|
547
571
|
*/
|
|
548
572
|
private processFailures;
|
|
549
573
|
/**
|