@operato/twin-kernel 0.7.64 → 0.7.66
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 +2 -0
- package/dist/capability.js +9 -1
- package/dist/contract.d.ts +81 -7
- package/dist/contract.js +1 -1
- package/dist/divergence.js +1 -1
- package/dist/domain-catalog.js +1 -1
- package/dist/ems-kernel.d.ts +31 -4
- package/dist/ems-kernel.js +114 -7
- package/dist/ems-profile.d.ts +19 -0
- package/dist/ems-profile.js +42 -0
- package/dist/energy-ingest.d.ts +19 -2
- package/dist/energy-ingest.js +58 -2
- package/dist/epcis.d.ts +2 -2
- package/dist/epcis.js +2 -2
- package/dist/event-journal.d.ts +8 -8
- package/dist/event-journal.js +8 -8
- package/dist/face2-adapter.d.ts +34 -1
- package/dist/face2-adapter.js +40 -0
- package/dist/flow-engine.d.ts +16 -7
- package/dist/flow-engine.js +91 -12
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/job-response.d.ts +1 -1
- package/dist/job-response.js +1 -1
- package/dist/kernel.js +1 -1
- package/dist/mes-kernel.d.ts +1 -1
- package/dist/mes-kernel.js +1 -1
- package/dist/observed-reducer.d.ts +57 -11
- package/dist/observed-reducer.js +76 -13
- package/dist/operational-ingest.d.ts +1 -1
- package/dist/operational-ingest.js +6 -6
- package/dist/state-projector.js +1 -1
- package/dist/task-fold.d.ts +2 -2
- package/dist/task-fold.js +1 -1
- package/dist/yms-kernel.js +1 -1
- package/dist-cjs/index.cjs +297 -30
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -29,8 +29,8 @@ export { WmsKernel } from "./kernel.js";
|
|
|
29
29
|
export { YmsKernel } from "./yms-kernel.js";
|
|
30
30
|
export { MesKernel } from "./mes-kernel.js";
|
|
31
31
|
export { EmsKernel, DEMAND_WINDOW_MS, demandWindowStart } from "./ems-kernel.js";
|
|
32
|
-
export { ingestEnergyRecords, isEnergyRecord, ingestEnergyEquipmentRecords, isEnergyEquipmentRecord } from "./energy-ingest.js";
|
|
33
|
-
/* 운영 사실의 문 — 리듀서가
|
|
32
|
+
export { ingestEnergyRecords, isEnergyRecord, ingestEnergyEquipmentRecords, isEnergyEquipmentRecord, ingestEnergyGenerationRecords, isEnergyGenerationRecord } from "./energy-ingest.js";
|
|
33
|
+
/* 운영 사실의 문 — 리듀서가 다루는 여섯이 들어오는 자리(미러가 시뮬보다 가난하지 않게). */
|
|
34
34
|
export { ingestOperationalRecords, isOperationalRecord, operationalKindOf } from "./operational-ingest.js";
|
|
35
35
|
export { attributeEnergy, electricityCost, energyIntensity, energyOfWindows } from "./energy-attribution.js";
|
|
36
36
|
/* 에너지 상태 타입은 **계약**에 있다(상태의 모양은 계약이다) — contract 의 `export *` 가 이미 낸다. */
|
package/dist/job-response.d.ts
CHANGED
|
@@ -48,7 +48,7 @@ export interface JobResponse {
|
|
|
48
48
|
/** 저널 한 줄에서 자원·자재 실적을 읽기 위한 최소 모양(작업 폴드가 보는 것과 같은 줄). */
|
|
49
49
|
type Row = TaskDeltaRow;
|
|
50
50
|
/**
|
|
51
|
-
* 저널을
|
|
51
|
+
* 저널을 계산해 **작업별 실적 회신**을 만든다 — 표준 이름으로.
|
|
52
52
|
*
|
|
53
53
|
* 짝맞춤(어느 전이가 한 작업인가)은 **커널 규칙**(`foldTaskRecords`)을 그대로 쓴다. 여기서 다시 짝을
|
|
54
54
|
* 맞추면 같은 저널이 지표에서는 3건, 실적에서는 4건이 되는 일이 생긴다 — 실제로 겪은 부류다.
|
package/dist/job-response.js
CHANGED
|
@@ -5,7 +5,7 @@ const dataOf = (r) => {
|
|
|
5
5
|
return r.data ?? p?.data ?? p ?? {};
|
|
6
6
|
};
|
|
7
7
|
/**
|
|
8
|
-
* 저널을
|
|
8
|
+
* 저널을 계산해 **작업별 실적 회신**을 만든다 — 표준 이름으로.
|
|
9
9
|
*
|
|
10
10
|
* 짝맞춤(어느 전이가 한 작업인가)은 **커널 규칙**(`foldTaskRecords`)을 그대로 쓴다. 여기서 다시 짝을
|
|
11
11
|
* 맞추면 같은 저널이 지표에서는 3건, 실적에서는 4건이 되는 일이 생긴다 — 실제로 겪은 부류다.
|
package/dist/kernel.js
CHANGED
|
@@ -196,7 +196,7 @@ export class WmsKernel extends FlowEngine {
|
|
|
196
196
|
if (t.intent === 'process')
|
|
197
197
|
return this.onProcessComplete(t);
|
|
198
198
|
const item = this.itemByRef(t.itemEpc);
|
|
199
|
-
/* 여기 도달했다면 코어가 이미 물품을 확인했다(주체가 사라진 작업은 완료 전에
|
|
199
|
+
/* 여기 도달했다면 코어가 이미 물품을 확인했다(주체가 사라진 작업은 완료 전에 제외된다).
|
|
200
200
|
그래도 단정(`!`)은 쓰지 않는다 — 계약이 바뀌면 조용히 틀리는 대신 분명히 멈춘다. */
|
|
201
201
|
if (!item)
|
|
202
202
|
throw new Error(`task ${t.id}: item "${t.itemEpc}" vanished between the core check and the domain hook`);
|
package/dist/mes-kernel.d.ts
CHANGED
|
@@ -121,7 +121,7 @@ export declare class MesKernel extends FlowEngine {
|
|
|
121
121
|
*
|
|
122
122
|
* 씨앗이 이미 이행된 오더를 주입하지 않으므로(남은 수량 0) 그 오더에 딸린 작업만 남을 수 있고,
|
|
123
123
|
* 관측이 오더 연결을 담지 못한 작업도 있다. 그때 예전에는 `order.gtin` 에서 오류가 났고 **그 예외
|
|
124
|
-
* 하나가 정확도 추세 전체를 죽였다.** 이제 코어가 이 답을 보고 그 작업만
|
|
124
|
+
* 하나가 정확도 추세 전체를 죽였다.** 이제 코어가 이 답을 보고 그 작업만 제외한다.
|
|
125
125
|
*/
|
|
126
126
|
protected canComplete(t: FlowTask): boolean;
|
|
127
127
|
protected onTaskComplete(t: FlowTask): void;
|
package/dist/mes-kernel.js
CHANGED
|
@@ -356,7 +356,7 @@ export class MesKernel extends FlowEngine {
|
|
|
356
356
|
*
|
|
357
357
|
* 씨앗이 이미 이행된 오더를 주입하지 않으므로(남은 수량 0) 그 오더에 딸린 작업만 남을 수 있고,
|
|
358
358
|
* 관측이 오더 연결을 담지 못한 작업도 있다. 그때 예전에는 `order.gtin` 에서 오류가 났고 **그 예외
|
|
359
|
-
* 하나가 정확도 추세 전체를 죽였다.** 이제 코어가 이 답을 보고 그 작업만
|
|
359
|
+
* 하나가 정확도 추세 전체를 죽였다.** 이제 코어가 이 답을 보고 그 작업만 제외한다.
|
|
360
360
|
*/
|
|
361
361
|
canComplete(t) {
|
|
362
362
|
return !!(t.orderId && this.orders.get(t.orderId));
|
|
@@ -92,10 +92,10 @@ export interface ProjectedState {
|
|
|
92
92
|
acked: string[];
|
|
93
93
|
}
|
|
94
94
|
/**
|
|
95
|
-
* 리듀서의 **완전한 재개점** — 이어
|
|
95
|
+
* 리듀서의 **완전한 재개점** — 이어 계산의 씨앗.
|
|
96
96
|
*
|
|
97
97
|
* `ProjectedState`(보기)와 구별한다: 여기에는 소비처가 보지 않는 것도 들어간다(보류된 담김·집합·
|
|
98
|
-
* 반영 못 한 사건 집계). 그것이 빠지면 「이어
|
|
98
|
+
* 반영 못 한 사건 집계). 그것이 빠지면 「이어 계산한 결과」가 「0부터 계산한 결과」와 조용히 달라진다.
|
|
99
99
|
*/
|
|
100
100
|
export interface ReducerCheckpoint {
|
|
101
101
|
revision: number;
|
|
@@ -151,17 +151,40 @@ export interface ReducerCheckpoint {
|
|
|
151
151
|
firstAtMs?: number;
|
|
152
152
|
lastAtMs?: number;
|
|
153
153
|
}[];
|
|
154
|
-
/** 자리별 · 속성별 마지막 관측 — 이어
|
|
154
|
+
/** 자리별 · 속성별 마지막 관측 — 이어 계산가 이 축을 0부터 다시 만들지 않게. */
|
|
155
155
|
observations?: {
|
|
156
156
|
id: string;
|
|
157
157
|
values: LocationObservation[];
|
|
158
158
|
}[];
|
|
159
|
+
/**
|
|
160
|
+
* 설비마다 두 값 — 마지막으로 값을 받은 시각, 그리고 그때까지 값이 가장 오래 끊겼던 길이.
|
|
161
|
+
* §`equipmentSilence` 가 이 값으로 판단한다.
|
|
162
|
+
*
|
|
163
|
+
* 저장해 두어야 한다. 저장하지 않으면 재기동한 뒤에 값이 얼마나 끊겼는지 계산할 수 없다.
|
|
164
|
+
* 그러면 화면은 어제 받은 상태를 오늘의 상태로 그린다.
|
|
165
|
+
*/
|
|
166
|
+
heard?: {
|
|
167
|
+
id: string;
|
|
168
|
+
atMs: number;
|
|
169
|
+
longestGapMs?: number;
|
|
170
|
+
}[];
|
|
159
171
|
}
|
|
160
172
|
export declare class ObservedReducer {
|
|
161
173
|
/** 로케이션 마스터 — 출처를 함께 들고 있다(마스터가 말한 자리 vs 관측으로 알게 된 자리). */
|
|
162
174
|
private master;
|
|
163
175
|
/** 자리별 · 속성별 **마지막 관측** — 이력이 아니다(§`OP_EVENT.observation`). */
|
|
164
176
|
private observations;
|
|
177
|
+
/**
|
|
178
|
+
* 설비별 **마지막으로 들은 시각** — 그 설비의 상태가 아니라 **우리 지식의 상태**다.
|
|
179
|
+
*
|
|
180
|
+
* ── 왜 상태에 넣지 않나 ─────────────────────────────────────────────────────
|
|
181
|
+
* 「가동 중」은 설비에 대한 사실이고 「그 말을 언제 들었나」는 우리에 대한 사실이다. 상태에 섞으면
|
|
182
|
+
* 두 종류가 한 통에 담기고, 자체 구동(시뮬)에는 없는 값이라 두 구동이 어긋난다 — 물품의
|
|
183
|
+
* `seenAtMs` 가 같은 이유로 상태에서 빠졌다(§`snapshot`).
|
|
184
|
+
*
|
|
185
|
+
* 그래서 여기 안에 두고, 밖으로는 **주의 신호**로만 낸다(§`equipmentSilence`).
|
|
186
|
+
*/
|
|
187
|
+
private heard;
|
|
165
188
|
private items;
|
|
166
189
|
private aggregation;
|
|
167
190
|
/** 아직 관측되지 않은 자식의 담김 — 물품을 지어내지 않고 보류했다가 등장할 때 붙인다. */
|
|
@@ -209,12 +232,12 @@ export declare class ObservedReducer {
|
|
|
209
232
|
/**
|
|
210
233
|
* **구조를 전환한다** — 관측된 사실은 지키고 토폴로지만 새 선언으로 바꾼다.
|
|
211
234
|
*
|
|
212
|
-
* 공장은 바뀐다. 도장 부스를 넷 더 놓고,
|
|
235
|
+
* 공장은 바뀐다. 도장 부스를 넷 더 놓고, 라인 하나를 닫는다. 그런데 지금까지는 구조가 바뀌면
|
|
213
236
|
* **그 트윈의 저널을 통째로 지우는 것**이 유일한 길이었다 — 안 지우면 옛 이벤트를 새 공장에 대고
|
|
214
|
-
*
|
|
237
|
+
* 계산하게 되어 이력이 거짓말을 한다. 역사를 잃거나 거짓말을 하거나, 둘뿐이었다.
|
|
215
238
|
*
|
|
216
239
|
* 셋째 길이 이것이다: 이벤트가 **자기 구조를 달고** 다니고, 재생은 구조가 바뀌는 지점에서 여기를
|
|
217
|
-
* 불러 전환한 뒤
|
|
240
|
+
* 불러 전환한 뒤 이어서 계산한다. 그러면 "그때 그 공장의 사실" 로 계속 읽힌다.
|
|
218
241
|
*
|
|
219
242
|
* ── 무엇을 지키고 무엇을 버리는가 ───────────────────────────────────────
|
|
220
243
|
* **관측은 지킨다** — 물품·오더·작업·집합은 구조와 무관한 사실이다(팔레트는 부스를 늘려도 그대로다).
|
|
@@ -250,6 +273,29 @@ export declare class ObservedReducer {
|
|
|
250
273
|
* 겹칠 때의 보조 기준이다. 시각이 없으면 판정하지 않는다(있는 것만 가지고 판단한다).
|
|
251
274
|
*/
|
|
252
275
|
private stale;
|
|
276
|
+
/**
|
|
277
|
+
* 이 설비의 값을 받은 시각을 적고, 지난번 값과의 간격 중 가장 긴 것을 함께 기록한다.
|
|
278
|
+
*
|
|
279
|
+
* 간격을 기록하는 이유는 판단 기준을 우리가 정하지 않기 위해서다. 「몇 분 넘게 소식이 없으면
|
|
280
|
+
* 이상하다」는 현장마다 다르다 — 1초마다 값을 보내는 계측기와 하루 한 번 올라오는 점검 기록이
|
|
281
|
+
* 같은 표에 있다. 기준을 하나로 정하면 앞쪽에는 신호가 너무 많이 서고 뒤쪽에는 서지 않는다.
|
|
282
|
+
* 그래서 그 설비가 지금까지 실제로 보여 준 간격을 기준으로 쓴다.
|
|
283
|
+
*/
|
|
284
|
+
private markHeard;
|
|
285
|
+
/**
|
|
286
|
+
* 값이 지난 간격보다 오래 끊긴 설비의 목록. 밖으로 내보내는 값은 이것 하나다.
|
|
287
|
+
*
|
|
288
|
+
* 끊긴 시간은 이 트윈이 마지막으로 어떤 값이든 받은 시각을 기준으로 잰다. 연결이 통째로 멈추면 그
|
|
289
|
+
* 기준도 멈추므로 아무 신호도 서지 않는다. 그때 알려야 할 사실은 설비마다의 상태가 아니라 「연결이
|
|
290
|
+
* 끊겼다」 하나이고, 그것은 유입 장부가 알린다.
|
|
291
|
+
*
|
|
292
|
+
* 지난 간격을 아직 모르는 설비는 목록에 넣지 않는다. 값을 한 번만 받았다면 이 설비가 원래 얼마나
|
|
293
|
+
* 뜸하게 보내는지 알 수 없다.
|
|
294
|
+
*/
|
|
295
|
+
equipmentSilence(): Map<string, {
|
|
296
|
+
silentMs: number;
|
|
297
|
+
longestBeforeMs: number;
|
|
298
|
+
}>;
|
|
253
299
|
/** 대상별 마지막 반영 시각 — 순서 판정용(대상=EPC·작업·설비·오더 id). */
|
|
254
300
|
private lastAt;
|
|
255
301
|
/**
|
|
@@ -311,7 +357,7 @@ export declare class ObservedReducer {
|
|
|
311
357
|
* 시뮬 기준시각(BASE_EPOCH, 2026-01-01)에 얼어 있었다.
|
|
312
358
|
*
|
|
313
359
|
* 결과는 조용했다: 구간 마감·피크 같은 **커널이 낸 사실이 8개월 전으로 기록**되어, 저널을 기간으로
|
|
314
|
-
*
|
|
360
|
+
* 계산하는 소비처(성과·요금·피더 배분)가 그 트윈에서 영원히 아무것도 찾지 못했다. 계측 자체는 원천 시각을
|
|
315
361
|
* 달고 있었으므로 화면의 순간값만 정상으로 보였다 — 그래서 눈에 띄지 않았다.
|
|
316
362
|
*
|
|
317
363
|
* 시각을 되돌리지는 않는다: 늦게 도착한 옛 표본이 "지금" 을 과거로 끌면 그 뒤 판정이 전부 흔들린다.
|
|
@@ -338,14 +384,14 @@ export declare class ObservedReducer {
|
|
|
338
384
|
/**
|
|
339
385
|
* **재개점(checkpoint)** — 스냅샷과 다르다.
|
|
340
386
|
*
|
|
341
|
-
* ── 왜 스냅샷으로는 이어
|
|
387
|
+
* ── 왜 스냅샷으로는 이어 계산할 수 없나 (2026-08-18) ─────────────────────────
|
|
342
388
|
* `snapshot()` 은 **소비처가 보는 값**이다(파생된 판정·정리된 목록). 그것으로 리듀서를 되세우면
|
|
343
389
|
* 보이지 않는 것들이 사라진다 — 아직 등장하지 않은 자식의 담김(`pendingParent`), 집합 관계
|
|
344
390
|
* (`aggregation`), 반영 못 한 사건 집계(`unhandled`), 정정 선언(`corrections`). 그 상태에서 뒤 이벤트를
|
|
345
|
-
*
|
|
391
|
+
* 계산하면 **0부터 계산한 결과와 달라진다.** 다르면 그 차이는 조용하다(오류가 없다).
|
|
346
392
|
*
|
|
347
|
-
* 그래서 재개점은 **내부 상태 전부**다. 「이어
|
|
348
|
-
* 0부터
|
|
393
|
+
* 그래서 재개점은 **내부 상태 전부**다. 「이어 계산」의 정합성은 시험이 증명한다:
|
|
394
|
+
* 0부터 계산한 결과 == 앞부분 재개점 + 뒷부분 계산.
|
|
349
395
|
*
|
|
350
396
|
* 모델에서 오는 것(교대·등급·시간대)은 담지 않는다 — 되세울 때 같은 모델을 받기 때문이다.
|
|
351
397
|
*/
|
package/dist/observed-reducer.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Observed Reducer — **이벤트를
|
|
2
|
+
* Observed Reducer — **이벤트를 계산해 상태를 만드는 단 하나의 규칙.**
|
|
3
3
|
*
|
|
4
4
|
* 이 파일은 원래 미러 전용(`StateProjector`)이었다. 그런데 커널도 이벤트로 굴러갈 수 있게 되면서
|
|
5
5
|
* (`FlowEngine.apply`, 통합 P0) **두 구동이 같은 규칙을 써야** 한다 — 규칙이 두 벌이면 반드시
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* live 모드: 외부(실 WMS)에서 이벤트가 도착 → 이 projector 가 State 를 재구성.
|
|
15
15
|
* → "계약 동일, 데이터원만 스왑"(execution-model.md §5, ADR-0010). 같은 이벤트면 같은 State.
|
|
16
16
|
*
|
|
17
|
-
* 두 갈래 이벤트를 함께
|
|
17
|
+
* 두 갈래 이벤트를 함께 계산한다:
|
|
18
18
|
* - EPCIS(epcis.*) → 재고/위치/조립 (What/Where)
|
|
19
19
|
* - 운영 델타(task/equipment/order.status) → tasks·equipment·orders (EPCIS 로 재구성 불가한 절반)
|
|
20
20
|
* 마스터(로케이션)는 board 초기화 + applyMaster 로 갱신(마스터 동기).
|
|
@@ -43,6 +43,17 @@ export class ObservedReducer {
|
|
|
43
43
|
master = new Map();
|
|
44
44
|
/** 자리별 · 속성별 **마지막 관측** — 이력이 아니다(§`OP_EVENT.observation`). */
|
|
45
45
|
observations = new Map();
|
|
46
|
+
/**
|
|
47
|
+
* 설비별 **마지막으로 들은 시각** — 그 설비의 상태가 아니라 **우리 지식의 상태**다.
|
|
48
|
+
*
|
|
49
|
+
* ── 왜 상태에 넣지 않나 ─────────────────────────────────────────────────────
|
|
50
|
+
* 「가동 중」은 설비에 대한 사실이고 「그 말을 언제 들었나」는 우리에 대한 사실이다. 상태에 섞으면
|
|
51
|
+
* 두 종류가 한 통에 담기고, 자체 구동(시뮬)에는 없는 값이라 두 구동이 어긋난다 — 물품의
|
|
52
|
+
* `seenAtMs` 가 같은 이유로 상태에서 빠졌다(§`snapshot`).
|
|
53
|
+
*
|
|
54
|
+
* 그래서 여기 안에 두고, 밖으로는 **주의 신호**로만 낸다(§`equipmentSilence`).
|
|
55
|
+
*/
|
|
56
|
+
heard = new Map();
|
|
46
57
|
items = new Map();
|
|
47
58
|
aggregation = new Map();
|
|
48
59
|
/** 아직 관측되지 않은 자식의 담김 — 물품을 지어내지 않고 보류했다가 등장할 때 붙인다. */
|
|
@@ -112,12 +123,12 @@ export class ObservedReducer {
|
|
|
112
123
|
/**
|
|
113
124
|
* **구조를 전환한다** — 관측된 사실은 지키고 토폴로지만 새 선언으로 바꾼다.
|
|
114
125
|
*
|
|
115
|
-
* 공장은 바뀐다. 도장 부스를 넷 더 놓고,
|
|
126
|
+
* 공장은 바뀐다. 도장 부스를 넷 더 놓고, 라인 하나를 닫는다. 그런데 지금까지는 구조가 바뀌면
|
|
116
127
|
* **그 트윈의 저널을 통째로 지우는 것**이 유일한 길이었다 — 안 지우면 옛 이벤트를 새 공장에 대고
|
|
117
|
-
*
|
|
128
|
+
* 계산하게 되어 이력이 거짓말을 한다. 역사를 잃거나 거짓말을 하거나, 둘뿐이었다.
|
|
118
129
|
*
|
|
119
130
|
* 셋째 길이 이것이다: 이벤트가 **자기 구조를 달고** 다니고, 재생은 구조가 바뀌는 지점에서 여기를
|
|
120
|
-
* 불러 전환한 뒤
|
|
131
|
+
* 불러 전환한 뒤 이어서 계산한다. 그러면 "그때 그 공장의 사실" 로 계속 읽힌다.
|
|
121
132
|
*
|
|
122
133
|
* ── 무엇을 지키고 무엇을 버리는가 ───────────────────────────────────────
|
|
123
134
|
* **관측은 지킨다** — 물품·오더·작업·집합은 구조와 무관한 사실이다(팔레트는 부스를 늘려도 그대로다).
|
|
@@ -245,6 +256,54 @@ export class ObservedReducer {
|
|
|
245
256
|
this.lastAt.set(key, { at, recorded: Number.isFinite(recorded) ? recorded : seen.recorded });
|
|
246
257
|
return false;
|
|
247
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* 이 설비의 값을 받은 시각을 적고, 지난번 값과의 간격 중 가장 긴 것을 함께 기록한다.
|
|
261
|
+
*
|
|
262
|
+
* 간격을 기록하는 이유는 판단 기준을 우리가 정하지 않기 위해서다. 「몇 분 넘게 소식이 없으면
|
|
263
|
+
* 이상하다」는 현장마다 다르다 — 1초마다 값을 보내는 계측기와 하루 한 번 올라오는 점검 기록이
|
|
264
|
+
* 같은 표에 있다. 기준을 하나로 정하면 앞쪽에는 신호가 너무 많이 서고 뒤쪽에는 서지 않는다.
|
|
265
|
+
* 그래서 그 설비가 지금까지 실제로 보여 준 간격을 기준으로 쓴다.
|
|
266
|
+
*/
|
|
267
|
+
markHeard(id, e) {
|
|
268
|
+
const at = Date.parse(String(e.eventTime ?? ''));
|
|
269
|
+
if (!Number.isFinite(at))
|
|
270
|
+
return;
|
|
271
|
+
const prev = this.heard.get(id);
|
|
272
|
+
if (!prev) {
|
|
273
|
+
this.heard.set(id, { atMs: at });
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
/* 이미 지난 시각이 담긴 사건은 간격 계산에서 뺀다. 도착이 늦은 사건이고, 값이 끊긴 것이 아니다. */
|
|
277
|
+
if (at <= prev.atMs)
|
|
278
|
+
return;
|
|
279
|
+
const gap = at - prev.atMs;
|
|
280
|
+
this.heard.set(id, { atMs: at, longestGapMs: Math.max(prev.longestGapMs ?? 0, gap) });
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* 값이 지난 간격보다 오래 끊긴 설비의 목록. 밖으로 내보내는 값은 이것 하나다.
|
|
284
|
+
*
|
|
285
|
+
* 끊긴 시간은 이 트윈이 마지막으로 어떤 값이든 받은 시각을 기준으로 잰다. 연결이 통째로 멈추면 그
|
|
286
|
+
* 기준도 멈추므로 아무 신호도 서지 않는다. 그때 알려야 할 사실은 설비마다의 상태가 아니라 「연결이
|
|
287
|
+
* 끊겼다」 하나이고, 그것은 유입 장부가 알린다.
|
|
288
|
+
*
|
|
289
|
+
* 지난 간격을 아직 모르는 설비는 목록에 넣지 않는다. 값을 한 번만 받았다면 이 설비가 원래 얼마나
|
|
290
|
+
* 뜸하게 보내는지 알 수 없다.
|
|
291
|
+
*/
|
|
292
|
+
equipmentSilence() {
|
|
293
|
+
const out = new Map();
|
|
294
|
+
const now = this.observedAtMs;
|
|
295
|
+
if (now === undefined)
|
|
296
|
+
return out;
|
|
297
|
+
for (const [id, h] of this.heard) {
|
|
298
|
+
if (h.longestGapMs === undefined)
|
|
299
|
+
continue;
|
|
300
|
+
const silentMs = now - h.atMs;
|
|
301
|
+
if (silentMs <= 0)
|
|
302
|
+
continue;
|
|
303
|
+
out.set(id, { silentMs, longestBeforeMs: h.longestGapMs });
|
|
304
|
+
}
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
248
307
|
/** 대상별 마지막 반영 시각 — 순서 판정용(대상=EPC·작업·설비·오더 id). */
|
|
249
308
|
lastAt = new Map();
|
|
250
309
|
/**
|
|
@@ -333,6 +392,7 @@ export class ObservedReducer {
|
|
|
333
392
|
/* 유효 기간은 **마스터 사실**이므로 침묵을 "없어졌다" 로 읽지 않고 아는 값을 지킨다.
|
|
334
393
|
판정은 여기서 하지 않는다 — `snapshot()` 이 관측 시각으로 `effectivityAt` 을 부른다. */
|
|
335
394
|
const eqPeriod = d.effectiveStart || d.effectiveEnd ? effectiveOf(d) : effectiveOf(known);
|
|
395
|
+
this.markHeard(d.moverId, e);
|
|
336
396
|
this.equipment.set(d.moverId, { id: d.moverId, kind: d.kind, status: d.status, location: d.location, ...(homeLocation ? { homeLocation } : {}), taskId: d.taskId, motion: d.motion, origin: known?.origin ?? 'observed', ...eqPeriod, ...(d.held ? { held: true } : {}) });
|
|
337
397
|
break;
|
|
338
398
|
}
|
|
@@ -400,7 +460,7 @@ export class ObservedReducer {
|
|
|
400
460
|
}
|
|
401
461
|
case OP_EVENT.test: {
|
|
402
462
|
/*
|
|
403
|
-
* **시험 결과** — 대상을 가리켜 들어오고(표준 방향), 상태에서는 그 개체 안에
|
|
463
|
+
* **시험 결과** — 대상을 가리켜 들어오고(표준 방향), 상태에서는 그 개체 안에 계산된다.
|
|
404
464
|
*
|
|
405
465
|
* 명세당 마지막 하나만 든다: 이력을 들면 상태가 계측 주기로 자란다(품목 100만 기준에서 벽이다).
|
|
406
466
|
* 늦게 온 옛 결과가 최신을 덮지 않게 `stale` 을 지나며, **대상 키에 명세를 넣는다** — 온도
|
|
@@ -870,7 +930,7 @@ export class ObservedReducer {
|
|
|
870
930
|
* 시뮬 기준시각(BASE_EPOCH, 2026-01-01)에 얼어 있었다.
|
|
871
931
|
*
|
|
872
932
|
* 결과는 조용했다: 구간 마감·피크 같은 **커널이 낸 사실이 8개월 전으로 기록**되어, 저널을 기간으로
|
|
873
|
-
*
|
|
933
|
+
* 계산하는 소비처(성과·요금·피더 배분)가 그 트윈에서 영원히 아무것도 찾지 못했다. 계측 자체는 원천 시각을
|
|
874
934
|
* 달고 있었으므로 화면의 순간값만 정상으로 보였다 — 그래서 눈에 띄지 않았다.
|
|
875
935
|
*
|
|
876
936
|
* 시각을 되돌리지는 않는다: 늦게 도착한 옛 표본이 "지금" 을 과거로 끌면 그 뒤 판정이 전부 흔들린다.
|
|
@@ -933,20 +993,20 @@ export class ObservedReducer {
|
|
|
933
993
|
/**
|
|
934
994
|
* **재개점(checkpoint)** — 스냅샷과 다르다.
|
|
935
995
|
*
|
|
936
|
-
* ── 왜 스냅샷으로는 이어
|
|
996
|
+
* ── 왜 스냅샷으로는 이어 계산할 수 없나 (2026-08-18) ─────────────────────────
|
|
937
997
|
* `snapshot()` 은 **소비처가 보는 값**이다(파생된 판정·정리된 목록). 그것으로 리듀서를 되세우면
|
|
938
998
|
* 보이지 않는 것들이 사라진다 — 아직 등장하지 않은 자식의 담김(`pendingParent`), 집합 관계
|
|
939
999
|
* (`aggregation`), 반영 못 한 사건 집계(`unhandled`), 정정 선언(`corrections`). 그 상태에서 뒤 이벤트를
|
|
940
|
-
*
|
|
1000
|
+
* 계산하면 **0부터 계산한 결과와 달라진다.** 다르면 그 차이는 조용하다(오류가 없다).
|
|
941
1001
|
*
|
|
942
|
-
* 그래서 재개점은 **내부 상태 전부**다. 「이어
|
|
943
|
-
* 0부터
|
|
1002
|
+
* 그래서 재개점은 **내부 상태 전부**다. 「이어 계산」의 정합성은 시험이 증명한다:
|
|
1003
|
+
* 0부터 계산한 결과 == 앞부분 재개점 + 뒷부분 계산.
|
|
944
1004
|
*
|
|
945
1005
|
* 모델에서 오는 것(교대·등급·시간대)은 담지 않는다 — 되세울 때 같은 모델을 받기 때문이다.
|
|
946
1006
|
*/
|
|
947
1007
|
serialize() {
|
|
948
1008
|
return {
|
|
949
|
-
/* 관측도 재개점에 든다 — 없으면 이어
|
|
1009
|
+
/* 관측도 재개점에 든다 — 없으면 이어 계산가 그 축을 0부터 다시 만든다. */
|
|
950
1010
|
observations: [...this.observations.entries()].map(([id, bin]) => ({ id, values: [...bin.values()] })),
|
|
951
1011
|
revision: this.revision,
|
|
952
1012
|
master: [...this.master.values()].map(n => ({ ...n })),
|
|
@@ -963,7 +1023,9 @@ export class ObservedReducer {
|
|
|
963
1023
|
orders: [...this.orders.values()].map(o => ({ ...o })),
|
|
964
1024
|
acked: [...this.acked],
|
|
965
1025
|
corrections: this.corrections.map(c => ({ ...c })),
|
|
966
|
-
unhandled: [...this.unhandled.entries()].map(([eventType, v]) => ({ eventType, ...v }))
|
|
1026
|
+
unhandled: [...this.unhandled.entries()].map(([eventType, v]) => ({ eventType, ...v })),
|
|
1027
|
+
/* 마지막으로 들은 시각 — 이어받지 않으면 재기동 뒤에 침묵의 길이를 말할 수 없다. */
|
|
1028
|
+
heard: [...this.heard.entries()].map(([id, v]) => ({ id, ...v }))
|
|
967
1029
|
};
|
|
968
1030
|
}
|
|
969
1031
|
/**
|
|
@@ -975,6 +1037,7 @@ export class ObservedReducer {
|
|
|
975
1037
|
this.observations = new Map((cp?.observations ?? []).map(o => [o.id, new Map((o.values ?? []).map(v => [v.propertyId, { ...v }]))]));
|
|
976
1038
|
this.revision = cp?.revision ?? 0;
|
|
977
1039
|
this.master = new Map((cp?.master ?? []).map(n => [n.id, { ...n }]));
|
|
1040
|
+
this.heard = new Map((cp?.heard ?? []).map(h => [h.id, { atMs: h.atMs, ...(h.longestGapMs !== undefined ? { longestGapMs: h.longestGapMs } : {}) }]));
|
|
978
1041
|
this.items = new Map((cp?.items ?? []).map(i => [i.epc, { ...i }]));
|
|
979
1042
|
this.aggregation = new Map((cp?.aggregation ?? []).map(a => [a.parent, [...a.children]]));
|
|
980
1043
|
this.pendingParent = new Map((cp?.pendingParent ?? []).map(x => [x.child, x.parent]));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IngestResult } from './face2-adapter.ts';
|
|
2
|
-
/** 이 문이 받는 여섯 가지 — 리듀서가
|
|
2
|
+
/** 이 문이 받는 여섯 가지 — 리듀서가 다루는 것과 같은 목록(주목 확인은 우리 안의 행위라 제외). */
|
|
3
3
|
export type OperationalKind = 'task' | 'equipment' | 'person' | 'asset' | 'order' | 'quality' | 'test' | 'observation' | 'complete';
|
|
4
4
|
/**
|
|
5
5
|
* 정규 운영 레코드 — **델타의 필드 이름 + 시각(`at`)**.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* 운영 사실 인제스트 — **작업·설비·사람·자산·오더·품질이 들어오는 문.** (ADR-0029 어휘 넓히기)
|
|
3
3
|
*
|
|
4
4
|
* ── 무엇이 없었나 (2026-08-19) ──────────────────────────────────────────────
|
|
5
|
-
* 관측 리듀서는 이 여섯을 **이미
|
|
5
|
+
* 관측 리듀서는 이 여섯을 **이미 다룬다**(`observed-reducer.ts` 의 `OP_EVENT.*` 분기). 그런데 라이브
|
|
6
6
|
* 인제스트 문은 어휘를 셋만 알았다: EPCIS 품목 사실, 에너지 계량, 설비 에너지 상태. 그래서 원본이
|
|
7
7
|
* 「이 작업이 끝났다」·「이 설비가 고장이다」를 말할 **길이 없었다** — 넣으면 `epc` 가 없어 EPCIS
|
|
8
8
|
* 검증에서 거부됐다.
|
|
@@ -17,14 +17,14 @@
|
|
|
17
17
|
* 규율을 먼저 세웠다: "필드 이름이 계약이다").
|
|
18
18
|
*
|
|
19
19
|
* ── 무엇을 거부하나 ─────────────────────────────────────────────────────────
|
|
20
|
-
* 지어낼 수 없는 것이 빠지면 거부한다 — 정체(누구의 상태인가)와 상태다. 그리고
|
|
20
|
+
* 지어낼 수 없는 것이 빠지면 거부한다 — 정체(누구의 상태인가)와 상태다. 그리고 **계산할 수 없는 낱말**도
|
|
21
21
|
* 거부한다: 설비 상태를 `'RUNNING'` 으로 받으면 아무 오류 없이 가동률이 0% 가 되고(누적기는 `busy`·
|
|
22
22
|
* `down` 만 센다), 사람이 `'available'` 이면 배정에서 조용히 사라진다. 그 실패는 화면에서 「일이 없는
|
|
23
|
-
* 공장」으로 보이고 원인을 되짚을 수 없다. 그래서 커널이
|
|
23
|
+
* 공장」으로 보이고 원인을 되짚을 수 없다. 그래서 커널이 다룰 수 있는 낱말만 받고, **받는 낱말을 이유에
|
|
24
24
|
* 적어** 커넥터가 매핑을 고칠 수 있게 한다(매핑=밖, 검증=커널).
|
|
25
25
|
*
|
|
26
26
|
* 오더의 상태·종류는 **열려 있다** — 도메인이 소유한다(`picking`·`packed`·`shipped`…). 커널이 그 낱말로
|
|
27
|
-
* 무엇을
|
|
27
|
+
* 무엇을 계산하지 않으므로 닫을 근거가 없다.
|
|
28
28
|
*
|
|
29
29
|
* ── 파생은 받아도 커널이 다시 계산한다 ──────────────────────────────────────
|
|
30
30
|
* 작업의 진척(`progress`)은 계약에 있어 받지만, 상태에 앉는 값은 커널이 **소요·남은 시간에서 다시
|
|
@@ -49,7 +49,7 @@ const SPECS = {
|
|
|
49
49
|
task: {
|
|
50
50
|
eventType: OP_EVENT.task,
|
|
51
51
|
identity: 'taskId',
|
|
52
|
-
/* 종류가 없으면 성과를 종류별로
|
|
52
|
+
/* 종류가 없으면 성과를 종류별로 모을 수 없고(선언된 시간·수율이 종류로 붙는다) 지어낼 수도 없다. */
|
|
53
53
|
required: ['taskId', 'kind', 'status'],
|
|
54
54
|
fields: {
|
|
55
55
|
taskId: 'string', kind: 'string', status: 'string', fromNode: 'string', toNode: 'string',
|
|
@@ -296,7 +296,7 @@ export function ingestOperationalRecords(records, opts) {
|
|
|
296
296
|
}
|
|
297
297
|
const allowed = spec.enums?.[name];
|
|
298
298
|
if (allowed && !allowed.includes(v)) {
|
|
299
|
-
errors.push(`${kind}.${name} 이 커널이
|
|
299
|
+
errors.push(`${kind}.${name} 이 커널이 다루는 낱말이 아니다: ${JSON.stringify(v)} — 받는 값은 ${allowed.join(' · ')}`);
|
|
300
300
|
break;
|
|
301
301
|
}
|
|
302
302
|
data[name] = v;
|
package/dist/state-projector.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* `StateProjector` — **옛 이름.** 규칙 자체는 `observed-reducer.ts` 로 옮겼다.
|
|
3
3
|
*
|
|
4
|
-
* 이름을 옮긴 이유: 이 규칙은 이제 미러 전용이 아니다. 커널도 같은 규칙으로 이벤트를
|
|
4
|
+
* 이름을 옮긴 이유: 이 규칙은 이제 미러 전용이 아니다. 커널도 같은 규칙으로 이벤트를 계산한다
|
|
5
5
|
* (`FlowEngine.apply`). "투영기" 라는 이름은 미러만의 것처럼 읽혀 두 구동이 규칙을 나눠 갖는 구조를
|
|
6
6
|
* 가린다. 소비처(호스트·테스트)가 많아 이름은 그대로 쓸 수 있게 남긴다.
|
|
7
7
|
*/
|
package/dist/task-fold.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export interface TaskFacets {
|
|
|
28
28
|
*
|
|
29
29
|
* 이 축이 없으면 「이 공정의 양품률이 얼마였나」를 저널에서 되짚을 수 없다: 양품/불량은 설비 누적
|
|
30
30
|
* 카운터(종류 모름)와 EPCIS disposition(종류 모름)에만 남아 있었다. 판정은 작업 완료에서 일어나므로
|
|
31
|
-
* 그 자리에서 적고, 여기서 그대로
|
|
31
|
+
* 그 자리에서 적고, 여기서 그대로 처리한다.
|
|
32
32
|
*
|
|
33
33
|
* **없음은 「양품」이 아니라 「판정하지 않았다」**다(이동·체류에는 품질 판정이 없다).
|
|
34
34
|
*/
|
|
@@ -54,7 +54,7 @@ export interface TaskFoldResult {
|
|
|
54
54
|
latestMs: number;
|
|
55
55
|
}
|
|
56
56
|
/**
|
|
57
|
-
* `task.status` 델타를 작업 단위 기록으로
|
|
57
|
+
* `task.status` 델타를 작업 단위 기록으로 처리한다.
|
|
58
58
|
*
|
|
59
59
|
* 입력은 **시간순일 필요가 없다**(revision 순·역순 모두 허용) — 이정표는 min/max 로 고르고 축의 값은
|
|
60
60
|
* 처음 본 것을 남기므로 순서에 의존하지 않는다. 단 `createdMs` 는 "마지막 생성 전이" 이므로 같은 작업의
|
package/dist/task-fold.js
CHANGED
|
@@ -10,7 +10,7 @@ function deltaOf(row) {
|
|
|
10
10
|
return (row.data ?? p?.data ?? p ?? {});
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
|
-
* `task.status` 델타를 작업 단위 기록으로
|
|
13
|
+
* `task.status` 델타를 작업 단위 기록으로 처리한다.
|
|
14
14
|
*
|
|
15
15
|
* 입력은 **시간순일 필요가 없다**(revision 순·역순 모두 허용) — 이정표는 min/max 로 고르고 축의 값은
|
|
16
16
|
* 처음 본 것을 남기므로 순서에 의존하지 않는다. 단 `createdMs` 는 "마지막 생성 전이" 이므로 같은 작업의
|
package/dist/yms-kernel.js
CHANGED
|
@@ -161,7 +161,7 @@ export class YmsKernel extends FlowEngine {
|
|
|
161
161
|
/** 태스크 완료 — 목적지 타입으로 분기: yard-slot=주차, dock-door=상/하차+depart, gate=출차. */
|
|
162
162
|
onTaskComplete(t) {
|
|
163
163
|
const trailer = this.itemByRef(t.itemEpc);
|
|
164
|
-
/* 여기 도달했다면 코어가 이미 물품을 확인했다(주체가 사라진 작업은 완료 전에
|
|
164
|
+
/* 여기 도달했다면 코어가 이미 물품을 확인했다(주체가 사라진 작업은 완료 전에 제외된다).
|
|
165
165
|
그래도 단정(`!`)은 쓰지 않는다 — 계약이 바뀌면 조용히 틀리는 대신 분명히 멈춘다. */
|
|
166
166
|
if (!trailer)
|
|
167
167
|
throw new Error(`task ${t.id}: item "${t.itemEpc}" vanished between the core check and the domain hook`);
|