@operato/twin-kernel 0.7.55 → 0.7.56
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 +16 -3
- package/dist/contract.js +13 -1
- package/dist/epcis.d.ts +61 -0
- package/dist/epcis.js +63 -2
- package/dist/flow-engine.d.ts +37 -1
- package/dist/flow-engine.js +115 -2
- package/dist/observed-reducer.d.ts +8 -1
- package/dist/observed-reducer.js +31 -0
- package/dist-cjs/index.cjs +181 -4
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -439,9 +439,9 @@ export interface TestSpecificationCriterion {
|
|
|
439
439
|
* 이 저장소의 규율이고(§`TestResult.result` 의 좁힘과 같은 자리), 그러면 다음 사람이 이것을
|
|
440
440
|
* 「표준이 준 이름」으로 읽지 않는다.
|
|
441
441
|
*
|
|
442
|
-
* 실 원본이
|
|
443
|
-
*
|
|
444
|
-
*
|
|
442
|
+
* 이 모양으로 한계를 주는 실 원본이 있다는 것도 확인했다. **그것이 이 축을 정한 것은 아니다** —
|
|
443
|
+
* 판정이 필요하다는 당위가 정했고, 원본은 그것을 채울 수 있다는 사실을 확인해 준 것이다.
|
|
444
|
+
* 어느 배포가 무엇을 주는지는 **커널이 알 일이 아니므로 여기 적지 않는다**(연동 쪽 문서의 몫이다).
|
|
445
445
|
*/
|
|
446
446
|
limit?: {
|
|
447
447
|
/** 이 값 미만이면 벗어난다. 없으면 아래쪽 한계가 없다(모르는 것이 아니라 없는 것이다). */
|
|
@@ -2070,6 +2070,18 @@ export declare const OP_EVENT: {
|
|
|
2070
2070
|
* 과거를 다시 계산해도 그때 무엇을 확인했는지 알 수 없다 — 저널이 현실을 불완전하게 담는 자리였다.
|
|
2071
2071
|
*/
|
|
2072
2072
|
readonly attentionAck: "attention.acked";
|
|
2073
|
+
/**
|
|
2074
|
+
* **자리에서 관측된 물리량** — 냉장실 온도·습도, 세척수 유량 같은 것(§`LocationObservation`).
|
|
2075
|
+
*
|
|
2076
|
+
* ── 왜 채널이 필요한가 ────────────────────────────────────────────────────
|
|
2077
|
+
* `LocationState.observations` 를 상태에 두었는데 그것을 낳는 사건이 없었다. 그러면 **상태 ⊆ 이벤트**
|
|
2078
|
+
* 가 깨진다: 채워도 재기동에서 사라지고, 폴드가 되살릴 수 없고, 미러가 이어받을 수도 없다. 상태에만
|
|
2079
|
+
* 있는 축은 조용히 사라지는 축이다.
|
|
2080
|
+
*
|
|
2081
|
+
* 이름을 `energy.measured` 와 같은 결로 둔다 — 그쪽이 에너지 계량의 도착이고 이쪽이 그 일반형이다.
|
|
2082
|
+
* 두 채널을 합치지 않는 이유는 에너지 쪽이 **구간에 누적되는 표본**이라 처리가 다르기 때문이다.
|
|
2083
|
+
*/
|
|
2084
|
+
readonly observation: "location.measured";
|
|
2073
2085
|
};
|
|
2074
2086
|
/**
|
|
2075
2087
|
* 에너지 트윈의 사건 — **물(物)의 계보가 아니라 스칼라의 시계열.**
|
|
@@ -2408,6 +2420,7 @@ export interface TwinModelDef {
|
|
|
2408
2420
|
parallelism?: number;
|
|
2409
2421
|
parentId?: string;
|
|
2410
2422
|
properties?: ResourceProperty[];
|
|
2423
|
+
testSpecificationIds?: TestSpecificationRefs;
|
|
2411
2424
|
}[];
|
|
2412
2425
|
/**
|
|
2413
2426
|
* 설비(설비). mtbfMs/mttrMs 지정 시 확률적 고장 모델 참여(OEE Availability 손실). 미지정=고장 없음.
|
package/dist/contract.js
CHANGED
|
@@ -898,7 +898,19 @@ export const OP_EVENT = {
|
|
|
898
898
|
* "확인해라"(요청)이고 이벤트는 "확인했다"(사실)다. 같은 문자열을 쓰면 저널에서 요청과 사실이
|
|
899
899
|
* 구별되지 않는다.
|
|
900
900
|
*/
|
|
901
|
-
attentionAck: 'attention.acked'
|
|
901
|
+
attentionAck: 'attention.acked',
|
|
902
|
+
/**
|
|
903
|
+
* **자리에서 관측된 물리량** — 냉장실 온도·습도, 세척수 유량 같은 것(§`LocationObservation`).
|
|
904
|
+
*
|
|
905
|
+
* ── 왜 채널이 필요한가 ────────────────────────────────────────────────────
|
|
906
|
+
* `LocationState.observations` 를 상태에 두었는데 그것을 낳는 사건이 없었다. 그러면 **상태 ⊆ 이벤트**
|
|
907
|
+
* 가 깨진다: 채워도 재기동에서 사라지고, 폴드가 되살릴 수 없고, 미러가 이어받을 수도 없다. 상태에만
|
|
908
|
+
* 있는 축은 조용히 사라지는 축이다.
|
|
909
|
+
*
|
|
910
|
+
* 이름을 `energy.measured` 와 같은 결로 둔다 — 그쪽이 에너지 계량의 도착이고 이쪽이 그 일반형이다.
|
|
911
|
+
* 두 채널을 합치지 않는 이유는 에너지 쪽이 **구간에 누적되는 표본**이라 처리가 다르기 때문이다.
|
|
912
|
+
*/
|
|
913
|
+
observation: 'location.measured'
|
|
902
914
|
};
|
|
903
915
|
// ── 에너지(EMS) 사건 — 네 번째 종류의 어휘 ────────────────────────────────
|
|
904
916
|
/**
|
package/dist/epcis.d.ts
CHANGED
|
@@ -8,6 +8,58 @@ export declare const DISP: {
|
|
|
8
8
|
readonly reserved: "urn:epcglobal:cbv:disp:reserved";
|
|
9
9
|
readonly in_transit: "urn:epcglobal:cbv:disp:in_transit";
|
|
10
10
|
readonly non_sellable: "urn:epcglobal:cbv:disp:non_sellable_other";
|
|
11
|
+
/**
|
|
12
|
+
* **기한이 지났다** — CBV `expired`.
|
|
13
|
+
*
|
|
14
|
+
* ── 왜 `non_sellable` 로 접지 않나 (2026-08-24) ─────────────────────────────
|
|
15
|
+
* 커널의 `non_sellable` 은 CBV 의 `non_sellable_other`, 즉 **「그 밖의 이유」**다. 기한 지남을 거기
|
|
16
|
+
* 넣으면 「기한이 지나 못 판다」와 「깨져서 못 판다」가 같은 값이 되고, 화면은 회수·폐기의 사유를
|
|
17
|
+
* 구별할 수 없다. 식품에서 그 둘은 다른 조치다.
|
|
18
|
+
*
|
|
19
|
+
* 그리고 표준에 **정확한 낱말이 있다** — 접는 것은 있는 낱말을 버리는 것이다.
|
|
20
|
+
*
|
|
21
|
+
* ── 기한 날짜와 다른 축이다 ────────────────────────────────────────────────
|
|
22
|
+
* `ItemState.expiry` 는 **날짜**이고 이것은 **상태**다. 날짜가 있으면 「지났나」는 파생이지만, 원본이
|
|
23
|
+
* 「기한 지남」을 상태로 선언하는 시스템이 있다 — 그때 이 값은 관측이다.
|
|
24
|
+
*
|
|
25
|
+
* 둘이 어긋나면(날짜는 남았는데 상태가 지남, 또는 그 반대) **어느 쪽이 맞다고 정하지 않는다** —
|
|
26
|
+
* 아직 그 판정을 세울 근거가 없다. 어긋남의 구분을 없애지 않는 것이 지금의 규율이다.
|
|
27
|
+
*
|
|
28
|
+
* ── 원문으로 확인했다 (2026-08-24) ────────────────────────────────────────
|
|
29
|
+
* 1차 출처: **CBV Standard Release 2.0, Ratified Jun 2022** §7.2.3 처분 값 표(38개). 이 객체의
|
|
30
|
+
* 다른 값들(`in_progress`·`sellable_accessible`·`reserved`·`in_transit`·`non_sellable_other`)도
|
|
31
|
+
* 그 표에 있다.
|
|
32
|
+
*
|
|
33
|
+
* **`non_sellable_expired` 를 쓰지 않는 이유**: 그 값은 CBV 1.0 의 것이고 표준이 **폐기**했다 —
|
|
34
|
+
* 「deprecated in favour of new disposition values expired, damaged, disposed, … introduced in
|
|
35
|
+
* CBV 1.1」. 폐기된 값을 쓰면 새 소비처가 읽지 못한다.
|
|
36
|
+
*
|
|
37
|
+
* 참고: GS1 어휘 등록처(`ref.gs1.org/cbv/…`)로는 확인할 수 없었다 — **없는 값에도 같은 응답**을
|
|
38
|
+
* 준다(지어낸 값의 JSON-LD 가 실재 값과 바이트까지 같았다). 그 경로를 근거로 삼지 말 것.
|
|
39
|
+
*/
|
|
40
|
+
readonly expired: "urn:epcglobal:cbv:disp:expired";
|
|
41
|
+
/**
|
|
42
|
+
* **검사에 합격했다 / 불합격했다** — CBV `conformant` / `non_conformant`.
|
|
43
|
+
*
|
|
44
|
+
* 1차 출처(CBV 2.0 §7.2.3) 정의 그대로다.
|
|
45
|
+
*
|
|
46
|
+
* conformant Outcome of a successful/passed inspection in an inspecting or repairing step
|
|
47
|
+
* non_conformant Outcome of an unsuccessful/failed inspection in an inspecting or repairing step
|
|
48
|
+
*
|
|
49
|
+
* ── 왜 시험 결과 축을 자원에 더하지 않고 이것을 쓰나 (2026-08-24) ────────────
|
|
50
|
+
* 로트의 검사 판정을 담을 자리를 찾다가 `ItemState.testResults` 를 더하려 했다. 그런데 표준은 그
|
|
51
|
+
* 사실을 **이미 처분으로 말한다**: `bizStep: inspecting` 사건에 이 처분이 붙는다.
|
|
52
|
+
*
|
|
53
|
+
* 처분을 쓰면 두 가지가 공짜로 성립한다.
|
|
54
|
+
* ① **상태 ⊆ 이벤트** — 처분은 이미 사건에서 온다. 상태에만 있는 축을 만들지 않는다
|
|
55
|
+
* ② **운영에 곧 닿는다** — 「이 자재를 쓸 수 있나」가 처분으로 답해진다(판정을 따로 읽지 않는다)
|
|
56
|
+
*
|
|
57
|
+
* 시험의 **자세한 내용**(어느 명세로, 무엇을 재어)은 다른 물음이고, 표준은 그것을 `TestResult` 로
|
|
58
|
+
* 두며 결과가 대상을 가리킨다(`TestableObjectID`) — 대상이 결과를 들지 않는다. 그 축이 필요해지면
|
|
59
|
+
* 그때 열되, **판정 자체는 여기서 끝난다.**
|
|
60
|
+
*/
|
|
61
|
+
readonly conformant: "urn:epcglobal:cbv:disp:conformant";
|
|
62
|
+
readonly non_conformant: "urn:epcglobal:cbv:disp:non_conformant";
|
|
11
63
|
};
|
|
12
64
|
/**
|
|
13
65
|
* **자재 소비·산출의 CBV 단계** — 도메인 무관하게 코어가 쓴다.
|
|
@@ -20,6 +72,15 @@ export declare const CBV_BIZSTEP: {
|
|
|
20
72
|
readonly consuming: "urn:epcglobal:cbv:bizstep:consuming";
|
|
21
73
|
/** 새 물품이 생겨 계보가 시작된다 — ISA-95 `MaterialUse: Produced`. */
|
|
22
74
|
readonly commissioning: "urn:epcglobal:cbv:bizstep:commissioning";
|
|
75
|
+
/**
|
|
76
|
+
* **검사** — CBV `inspecting`. 1차 출처(CBV 2.0) 정의: 「Process of reviewing objects to address
|
|
77
|
+
* potential physical or documentation defects」이고, 「표본과 달리 검사된 대상은 그대로 남는다」고
|
|
78
|
+
* 이어진다(즉 검사는 물건을 소비하지 않는다).
|
|
79
|
+
*
|
|
80
|
+
* 이 단계에 `DISP.conformant`/`DISP.non_conformant` 가 붙어 판정이 처분으로 남는다 — 입고검수·
|
|
81
|
+
* 공정 중 검사가 그 모양이다.
|
|
82
|
+
*/
|
|
83
|
+
readonly inspecting: "urn:epcglobal:cbv:bizstep:inspecting";
|
|
23
84
|
};
|
|
24
85
|
export type EpcisEventType = 'ObjectEvent' | 'AggregationEvent' | 'TransactionEvent' | 'TransformationEvent';
|
|
25
86
|
export type EpcisAction = 'ADD' | 'OBSERVE' | 'DELETE';
|
package/dist/epcis.js
CHANGED
|
@@ -16,7 +16,59 @@ export const DISP = {
|
|
|
16
16
|
sellable: 'urn:epcglobal:cbv:disp:sellable_accessible',
|
|
17
17
|
reserved: 'urn:epcglobal:cbv:disp:reserved',
|
|
18
18
|
in_transit: 'urn:epcglobal:cbv:disp:in_transit',
|
|
19
|
-
non_sellable: 'urn:epcglobal:cbv:disp:non_sellable_other' // 불량/scrap
|
|
19
|
+
non_sellable: 'urn:epcglobal:cbv:disp:non_sellable_other', // 불량/scrap
|
|
20
|
+
/**
|
|
21
|
+
* **기한이 지났다** — CBV `expired`.
|
|
22
|
+
*
|
|
23
|
+
* ── 왜 `non_sellable` 로 접지 않나 (2026-08-24) ─────────────────────────────
|
|
24
|
+
* 커널의 `non_sellable` 은 CBV 의 `non_sellable_other`, 즉 **「그 밖의 이유」**다. 기한 지남을 거기
|
|
25
|
+
* 넣으면 「기한이 지나 못 판다」와 「깨져서 못 판다」가 같은 값이 되고, 화면은 회수·폐기의 사유를
|
|
26
|
+
* 구별할 수 없다. 식품에서 그 둘은 다른 조치다.
|
|
27
|
+
*
|
|
28
|
+
* 그리고 표준에 **정확한 낱말이 있다** — 접는 것은 있는 낱말을 버리는 것이다.
|
|
29
|
+
*
|
|
30
|
+
* ── 기한 날짜와 다른 축이다 ────────────────────────────────────────────────
|
|
31
|
+
* `ItemState.expiry` 는 **날짜**이고 이것은 **상태**다. 날짜가 있으면 「지났나」는 파생이지만, 원본이
|
|
32
|
+
* 「기한 지남」을 상태로 선언하는 시스템이 있다 — 그때 이 값은 관측이다.
|
|
33
|
+
*
|
|
34
|
+
* 둘이 어긋나면(날짜는 남았는데 상태가 지남, 또는 그 반대) **어느 쪽이 맞다고 정하지 않는다** —
|
|
35
|
+
* 아직 그 판정을 세울 근거가 없다. 어긋남의 구분을 없애지 않는 것이 지금의 규율이다.
|
|
36
|
+
*
|
|
37
|
+
* ── 원문으로 확인했다 (2026-08-24) ────────────────────────────────────────
|
|
38
|
+
* 1차 출처: **CBV Standard Release 2.0, Ratified Jun 2022** §7.2.3 처분 값 표(38개). 이 객체의
|
|
39
|
+
* 다른 값들(`in_progress`·`sellable_accessible`·`reserved`·`in_transit`·`non_sellable_other`)도
|
|
40
|
+
* 그 표에 있다.
|
|
41
|
+
*
|
|
42
|
+
* **`non_sellable_expired` 를 쓰지 않는 이유**: 그 값은 CBV 1.0 의 것이고 표준이 **폐기**했다 —
|
|
43
|
+
* 「deprecated in favour of new disposition values expired, damaged, disposed, … introduced in
|
|
44
|
+
* CBV 1.1」. 폐기된 값을 쓰면 새 소비처가 읽지 못한다.
|
|
45
|
+
*
|
|
46
|
+
* 참고: GS1 어휘 등록처(`ref.gs1.org/cbv/…`)로는 확인할 수 없었다 — **없는 값에도 같은 응답**을
|
|
47
|
+
* 준다(지어낸 값의 JSON-LD 가 실재 값과 바이트까지 같았다). 그 경로를 근거로 삼지 말 것.
|
|
48
|
+
*/
|
|
49
|
+
expired: 'urn:epcglobal:cbv:disp:expired',
|
|
50
|
+
/**
|
|
51
|
+
* **검사에 합격했다 / 불합격했다** — CBV `conformant` / `non_conformant`.
|
|
52
|
+
*
|
|
53
|
+
* 1차 출처(CBV 2.0 §7.2.3) 정의 그대로다.
|
|
54
|
+
*
|
|
55
|
+
* conformant Outcome of a successful/passed inspection in an inspecting or repairing step
|
|
56
|
+
* non_conformant Outcome of an unsuccessful/failed inspection in an inspecting or repairing step
|
|
57
|
+
*
|
|
58
|
+
* ── 왜 시험 결과 축을 자원에 더하지 않고 이것을 쓰나 (2026-08-24) ────────────
|
|
59
|
+
* 로트의 검사 판정을 담을 자리를 찾다가 `ItemState.testResults` 를 더하려 했다. 그런데 표준은 그
|
|
60
|
+
* 사실을 **이미 처분으로 말한다**: `bizStep: inspecting` 사건에 이 처분이 붙는다.
|
|
61
|
+
*
|
|
62
|
+
* 처분을 쓰면 두 가지가 공짜로 성립한다.
|
|
63
|
+
* ① **상태 ⊆ 이벤트** — 처분은 이미 사건에서 온다. 상태에만 있는 축을 만들지 않는다
|
|
64
|
+
* ② **운영에 곧 닿는다** — 「이 자재를 쓸 수 있나」가 처분으로 답해진다(판정을 따로 읽지 않는다)
|
|
65
|
+
*
|
|
66
|
+
* 시험의 **자세한 내용**(어느 명세로, 무엇을 재어)은 다른 물음이고, 표준은 그것을 `TestResult` 로
|
|
67
|
+
* 두며 결과가 대상을 가리킨다(`TestableObjectID`) — 대상이 결과를 들지 않는다. 그 축이 필요해지면
|
|
68
|
+
* 그때 열되, **판정 자체는 여기서 끝난다.**
|
|
69
|
+
*/
|
|
70
|
+
conformant: 'urn:epcglobal:cbv:disp:conformant',
|
|
71
|
+
non_conformant: 'urn:epcglobal:cbv:disp:non_conformant'
|
|
20
72
|
};
|
|
21
73
|
/**
|
|
22
74
|
* **자재 소비·산출의 CBV 단계** — 도메인 무관하게 코어가 쓴다.
|
|
@@ -28,7 +80,16 @@ export const CBV_BIZSTEP = {
|
|
|
28
80
|
/** 공정에 자재가 들어갔다 — ISA-95 `MaterialUse: Consumed`. */
|
|
29
81
|
consuming: 'urn:epcglobal:cbv:bizstep:consuming',
|
|
30
82
|
/** 새 물품이 생겨 계보가 시작된다 — ISA-95 `MaterialUse: Produced`. */
|
|
31
|
-
commissioning: 'urn:epcglobal:cbv:bizstep:commissioning'
|
|
83
|
+
commissioning: 'urn:epcglobal:cbv:bizstep:commissioning',
|
|
84
|
+
/**
|
|
85
|
+
* **검사** — CBV `inspecting`. 1차 출처(CBV 2.0) 정의: 「Process of reviewing objects to address
|
|
86
|
+
* potential physical or documentation defects」이고, 「표본과 달리 검사된 대상은 그대로 남는다」고
|
|
87
|
+
* 이어진다(즉 검사는 물건을 소비하지 않는다).
|
|
88
|
+
*
|
|
89
|
+
* 이 단계에 `DISP.conformant`/`DISP.non_conformant` 가 붙어 판정이 처분으로 남는다 — 입고검수·
|
|
90
|
+
* 공정 중 검사가 그 모양이다.
|
|
91
|
+
*/
|
|
92
|
+
inspecting: 'urn:epcglobal:cbv:bizstep:inspecting'
|
|
32
93
|
};
|
|
33
94
|
// ── GS1 EPC URI 헬퍼 (표준) ────────────────────────────────────────────────
|
|
34
95
|
/** SSCC (물류단위: 팔레트/화물/트레일러) — 결정적 카운터 기반. */
|
package/dist/flow-engine.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { TestResult, ISOTime, MaterialQuantity, WorkCalendarEntry, EffectivePeriod, Effectivity, OffCalendarReason, ResourceProperty, ResourceClassDef, MaterialDefinition, Attention, TwinModelDef, CanonicalEnvelope, Command, CommandAck, EventHandler, EquipmentMotion, OeeMetrics, AssetState, GeneratorSpec, InterventionOutcome, OrderState, PersonState, ScenarioControl, ScenarioOverride, StateSnapshot, TwinKernel, Unsubscribe, LocationState, ItemState, EquipmentState, OrderStatusDelta, TaskState, TaskStatus, StructureShift, IdentityGroundingView, IdentityDeclaration } from './contract.ts';
|
|
1
|
+
import type { TestResult, ISOTime, MaterialQuantity, WorkCalendarEntry, EffectivePeriod, Effectivity, OffCalendarReason, ResourceProperty, ResourceClassDef, MaterialDefinition, Attention, TwinModelDef, CanonicalEnvelope, Command, CommandAck, EventHandler, EquipmentMotion, OeeMetrics, AssetState, GeneratorSpec, InterventionOutcome, OrderState, PersonState, ScenarioControl, ScenarioOverride, StateSnapshot, TwinKernel, Unsubscribe, LocationState, ItemState, EquipmentState, OrderStatusDelta, TaskState, TaskStatus, StructureShift, IdentityGroundingView, IdentityDeclaration, TestSpecificationCriterion, LocationObservation } from './contract.ts';
|
|
2
|
+
import type { ReducerCheckpoint } from './observed-reducer.ts';
|
|
2
3
|
import type { EpcisEvent, BizTransactionElement } from './epcis.ts';
|
|
3
4
|
import type { AllocationPolicy, SlotView } from './allocation-policy.ts';
|
|
4
5
|
import type { DurationEstimator, DurationContext } from './duration-estimator.ts';
|
|
@@ -369,6 +370,10 @@ export declare function deriveAttentions(view: {
|
|
|
369
370
|
id: string;
|
|
370
371
|
capacity?: number;
|
|
371
372
|
occupancy?: number;
|
|
373
|
+
/** 이 자리에 걸린 판정 기준들 — 없으면 관측을 판정하지 않는다. */
|
|
374
|
+
criteria?: TestSpecificationCriterion[];
|
|
375
|
+
/** 이 자리에서 관측된 물리량들(속성당 최신). */
|
|
376
|
+
observations?: LocationObservation[];
|
|
372
377
|
}[];
|
|
373
378
|
orders: {
|
|
374
379
|
id: string;
|
|
@@ -560,6 +565,37 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
560
565
|
protected localParams: Map<string, Map<string, string>>;
|
|
561
566
|
/** 관측 구동(P0) — 이벤트를 접는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
|
|
562
567
|
private observer?;
|
|
568
|
+
/**
|
|
569
|
+
* **관측 리듀서의 재개점을 꺼낸다** — 호스트가 저장해 다음 기동에서 되돌릴 수 있게.
|
|
570
|
+
*
|
|
571
|
+
* ── 무엇이 문제였나 (2026-08-24 실측) ──────────────────────────────────────
|
|
572
|
+
* 미러는 재기동마다 저널을 **0부터** 다시 집계했다. 실측으로 저널이 2,960만 줄이고, 그 때문에 기동
|
|
573
|
+
* 직후 몇 분간 상태가 비어 있었다. 계측처럼 경계 없이 자라는 흐름이 들어오면 그 몇 분이 몇십 분이
|
|
574
|
+
* 된다 — 불편이 아니라 벽이다.
|
|
575
|
+
*
|
|
576
|
+
* 재개점 자체는 오래전부터 있었다(`serialize`/`restore`). 조회 경로는 그것을 쓰는데
|
|
577
|
+
* (`replayFrom`) **라이브 경로에는 꺼낼 문이 없었다.** 그래서 호스트가 저장할 수 없었다.
|
|
578
|
+
*
|
|
579
|
+
* ── 상태 스냅샷으로는 대신할 수 없다 ──────────────────────────────────────
|
|
580
|
+
* 리듀서는 소비처가 보는 값 말고도 든다: 부모를 기다리는 담김·집계 중인 수량·담을 줄 몰라 세어 둔
|
|
581
|
+
* 사건. 상태만 되돌리고 이어 집계하면 **0부터 집계한 결과와 조용히 달라진다**(§`ReducerCheckpoint`).
|
|
582
|
+
*
|
|
583
|
+
* 관측 구동이 아니면 `undefined` — 시뮬은 리듀서를 갖지 않는다(저장할 것이 없다).
|
|
584
|
+
*/
|
|
585
|
+
observedCheckpoint(): ReducerCheckpoint | undefined;
|
|
586
|
+
/**
|
|
587
|
+
* **재개점에서 관측 리듀서를 되세운다** — 저널을 0부터 다시 집계하지 않게.
|
|
588
|
+
*
|
|
589
|
+
* 리듀서가 아직 없으면 만든다: 미러는 첫 봉투가 올 때 리듀서를 만드는데(§`apply`), 되돌리기는 그보다
|
|
590
|
+
* 먼저 일어나야 한다(그러지 않으면 첫 봉투가 빈 리듀서를 만들고 되돌린 것을 덮는다).
|
|
591
|
+
*
|
|
592
|
+
* 되돌린 뒤 상태로 옮긴다 — 그러지 않으면 첫 스냅샷이 빈 상태를 보인다.
|
|
593
|
+
*
|
|
594
|
+
* **구조가 다르면 되돌리지 않는다**: 재개점은 그 모델 위에서 만들어진 것이고, 다른 공장의 재개점을
|
|
595
|
+
* 얹으면 없는 자리·설비가 생긴다. 판단은 부르는 쪽이 한다(`structureRev` 를 아는 것은 호스트다) —
|
|
596
|
+
* 여기서는 받은 것을 그대로 세운다.
|
|
597
|
+
*/
|
|
598
|
+
restoreObserved(cp: ReducerCheckpoint): void;
|
|
563
599
|
/**
|
|
564
600
|
* 이 커널의 상태가 **관측에서 왔나** — 미러인가.
|
|
565
601
|
*
|
package/dist/flow-engine.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* 통합 타입은 도메인 필드를 옵셔널로 넓혀(FlowItem.gtin?, FlowOrder.shipmentEpc? 등) 두 도메인을 담는다.
|
|
10
10
|
* (roadmap Phase5 발견 → 추출. [[project_flow_single_base_vision]] FlowLocation 단일 base 방향과 정합.)
|
|
11
11
|
*/
|
|
12
|
-
import { OP_EVENT, CMD, locationStatusOf, readBoardEquipment, readBoardLocations, readBoardAssets, classClosure, capabilityOf, requiredTestsFor, priorityRank, dueStatusOf, isOrderTerminal, effectivityAt, offCalendarAt, offCalendarReasonAt, minuteOfDayAt, activeShiftAt, subLotIdOf, itemKeyOf, identityGroundingOf } from "./contract.js";
|
|
12
|
+
import { OP_EVENT, CMD, locationStatusOf, readBoardEquipment, readBoardLocations, readBoardAssets, classClosure, capabilityOf, requiredTestsFor, priorityRank, dueStatusOf, isOrderTerminal, effectivityAt, offCalendarAt, offCalendarReasonAt, minuteOfDayAt, activeShiftAt, subLotIdOf, itemKeyOf, identityGroundingOf, outsideLimit } from "./contract.js";
|
|
13
13
|
import { ObservedReducer } from "./observed-reducer.js";
|
|
14
14
|
import { transformationEvent, aggregationEvent, objectEvent, parseEpc, DISP, ILMD_ATTR, CBV_BIZSTEP, objectUri, bizTransactionUri, gdtiUri } from "./epcis.js";
|
|
15
15
|
import { OP_PARAM } from "./domain-definition.js";
|
|
@@ -108,6 +108,56 @@ thresholds) {
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
for (const n of view.locations) {
|
|
111
|
+
/*
|
|
112
|
+
* **관측이 한계를 벗어났다** — 판정할 수 있을 때만 낸다.
|
|
113
|
+
*
|
|
114
|
+
* ── 왜 이 자리인가 ────────────────────────────────────────────────────────
|
|
115
|
+
* 기준과 측정값을 계약에 넣어 두고 **아무도 부르지 않았다**(`outsideLimit`). 부르지 않는 판정
|
|
116
|
+
* 함수는 시험만 통과하는 코드이고, 「선언만 있는 능력은 사용자에게 거짓말이 된다」와 같은 자리다.
|
|
117
|
+
*
|
|
118
|
+
* ── 커널이 하지 않는 것 ───────────────────────────────────────────────────
|
|
119
|
+
* 표현식을 읽지 않는다. 숫자 한계가 없으면 `outsideLimit` 이 `undefined` 를 주고, 그때는 **신호를
|
|
120
|
+
* 내지 않는다** — 「판정 못 했다」를 「벗어났다」로도 「괜찮다」로도 만들지 않는다.
|
|
121
|
+
*
|
|
122
|
+
* 그 「판정 못 했다」가 조용히 사라지는 것은 다른 축이 답한다(`criterionSaysNothing` ·
|
|
123
|
+
* `testEvidenceGaps`) — 그것은 설정의 흠이고 이것은 운영의 사실이라, 한 신호에 섞지 않는다.
|
|
124
|
+
*
|
|
125
|
+
* ── 심각도를 벗어난 폭으로 가르지 않는다 ──────────────────────────────────
|
|
126
|
+
* 에너지 초과는 5%·15% 로 갈랐다(계량 오차와 구별하기 어려우므로). 여기서는 그러지 않는다:
|
|
127
|
+
* 식품 안전 한계는 **넘으면 넘은 것**이고, 「조금 넘었다」를 낮게 부르면 그 판단을 커널이 대신
|
|
128
|
+
* 하는 것이 된다. 폭은 `params` 로 함께 내고 판단은 현장이 한다.
|
|
129
|
+
*/
|
|
130
|
+
for (const c of n.criteria ?? []) {
|
|
131
|
+
const propertyId = c.evaluatedPropertyId;
|
|
132
|
+
if (!propertyId)
|
|
133
|
+
continue;
|
|
134
|
+
const observed = (n.observations ?? []).find(o => o.propertyId === propertyId);
|
|
135
|
+
if (!observed)
|
|
136
|
+
continue;
|
|
137
|
+
if (outsideLimit(c, observed) !== true)
|
|
138
|
+
continue;
|
|
139
|
+
out.push({
|
|
140
|
+
/* 자리·기준마다 하나 — 같은 방의 온도와 습도가 한 신호로 뭉치지 않는다. */
|
|
141
|
+
id: `observation-out-of-limit:${n.id}:${c.id}`,
|
|
142
|
+
kind: 'observation-out-of-limit',
|
|
143
|
+
severity: 'high',
|
|
144
|
+
anchor: { locationId: n.id },
|
|
145
|
+
/* 언어중립 원시값만 — 문장은 표현계층이 kind 로 골라 렌더한다. */
|
|
146
|
+
params: {
|
|
147
|
+
locationId: n.id,
|
|
148
|
+
criterionId: c.id,
|
|
149
|
+
propertyId,
|
|
150
|
+
/* 값이 없으면 판정이 서지 않으므로 여기 오지 않는다 — 그래도 타입을 좁혀 둔다. */
|
|
151
|
+
value: observed.value ?? '',
|
|
152
|
+
...(observed.uom ? { uom: observed.uom } : {}),
|
|
153
|
+
...(c.limit?.minimum !== undefined ? { minimum: c.limit.minimum } : {}),
|
|
154
|
+
...(c.limit?.maximum !== undefined ? { maximum: c.limit.maximum } : {}),
|
|
155
|
+
/* **언제의 값인가** — 4개월 전 값으로 지금을 판정한 것인지 소비처가 알아야 한다. */
|
|
156
|
+
effectiveTime: observed.effectiveTime,
|
|
157
|
+
...(observed.derived ? { derived: 'true' } : {})
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
111
161
|
if ((n.capacity ?? 0) > 0) {
|
|
112
162
|
const r = (n.occupancy ?? 0) / n.capacity;
|
|
113
163
|
/* 기준은 현장이 정한다 — 없으면 기본값이고, 어느 쪽인지 함께 낸다(조용히 기본값을 진실로 두지 않는다). */
|
|
@@ -573,6 +623,47 @@ export class FlowEngine {
|
|
|
573
623
|
localParams = new Map();
|
|
574
624
|
/** 관측 구동(P0) — 이벤트를 접는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
|
|
575
625
|
observer;
|
|
626
|
+
/**
|
|
627
|
+
* **관측 리듀서의 재개점을 꺼낸다** — 호스트가 저장해 다음 기동에서 되돌릴 수 있게.
|
|
628
|
+
*
|
|
629
|
+
* ── 무엇이 문제였나 (2026-08-24 실측) ──────────────────────────────────────
|
|
630
|
+
* 미러는 재기동마다 저널을 **0부터** 다시 집계했다. 실측으로 저널이 2,960만 줄이고, 그 때문에 기동
|
|
631
|
+
* 직후 몇 분간 상태가 비어 있었다. 계측처럼 경계 없이 자라는 흐름이 들어오면 그 몇 분이 몇십 분이
|
|
632
|
+
* 된다 — 불편이 아니라 벽이다.
|
|
633
|
+
*
|
|
634
|
+
* 재개점 자체는 오래전부터 있었다(`serialize`/`restore`). 조회 경로는 그것을 쓰는데
|
|
635
|
+
* (`replayFrom`) **라이브 경로에는 꺼낼 문이 없었다.** 그래서 호스트가 저장할 수 없었다.
|
|
636
|
+
*
|
|
637
|
+
* ── 상태 스냅샷으로는 대신할 수 없다 ──────────────────────────────────────
|
|
638
|
+
* 리듀서는 소비처가 보는 값 말고도 든다: 부모를 기다리는 담김·집계 중인 수량·담을 줄 몰라 세어 둔
|
|
639
|
+
* 사건. 상태만 되돌리고 이어 집계하면 **0부터 집계한 결과와 조용히 달라진다**(§`ReducerCheckpoint`).
|
|
640
|
+
*
|
|
641
|
+
* 관측 구동이 아니면 `undefined` — 시뮬은 리듀서를 갖지 않는다(저장할 것이 없다).
|
|
642
|
+
*/
|
|
643
|
+
observedCheckpoint() {
|
|
644
|
+
return this.observer?.serialize();
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* **재개점에서 관측 리듀서를 되세운다** — 저널을 0부터 다시 집계하지 않게.
|
|
648
|
+
*
|
|
649
|
+
* 리듀서가 아직 없으면 만든다: 미러는 첫 봉투가 올 때 리듀서를 만드는데(§`apply`), 되돌리기는 그보다
|
|
650
|
+
* 먼저 일어나야 한다(그러지 않으면 첫 봉투가 빈 리듀서를 만들고 되돌린 것을 덮는다).
|
|
651
|
+
*
|
|
652
|
+
* 되돌린 뒤 상태로 옮긴다 — 그러지 않으면 첫 스냅샷이 빈 상태를 보인다.
|
|
653
|
+
*
|
|
654
|
+
* **구조가 다르면 되돌리지 않는다**: 재개점은 그 모델 위에서 만들어진 것이고, 다른 공장의 재개점을
|
|
655
|
+
* 얹으면 없는 자리·설비가 생긴다. 판단은 부르는 쪽이 한다(`structureRev` 를 아는 것은 호스트다) —
|
|
656
|
+
* 여기서는 받은 것을 그대로 세운다.
|
|
657
|
+
*/
|
|
658
|
+
restoreObserved(cp) {
|
|
659
|
+
if (!this.observer) {
|
|
660
|
+
this.observer = new ObservedReducer(this.boardDef ?? { locations: [], equipment: [] });
|
|
661
|
+
this.observeMode = true;
|
|
662
|
+
}
|
|
663
|
+
this.observer.restore(cp);
|
|
664
|
+
this.observedDirty = true;
|
|
665
|
+
this.settleObserved();
|
|
666
|
+
}
|
|
576
667
|
/**
|
|
577
668
|
* 이 커널의 상태가 **관측에서 왔나** — 미러인가.
|
|
578
669
|
*
|
|
@@ -1663,9 +1754,31 @@ export class FlowEngine {
|
|
|
1663
1754
|
collectAttentions() {
|
|
1664
1755
|
// 계산 층은 순수 함수 deriveAttentions 로 위임 — sim(여기)과 live projector 미러가 공유(face2-inbound-live §1.1).
|
|
1665
1756
|
const now = this.now();
|
|
1757
|
+
/*
|
|
1758
|
+
* **자리에 걸린 기준을 여기서 푼다** — 순수 함수는 모델을 읽지 않는다(그 규율은 `attentionThresholds`
|
|
1759
|
+
* 와 같다). 종류 선언(`LocationTypeDef.testSpecificationIds`) → 명세 → 기준으로 풀어 넘긴다.
|
|
1760
|
+
*
|
|
1761
|
+
* 선언이 없으면 빈 배열이고, 그러면 그 자리의 관측은 판정되지 않는다 — 관측은 남고 신호만 서지
|
|
1762
|
+
* 않는다(「선언한 것만 제약이 된다」).
|
|
1763
|
+
*/
|
|
1764
|
+
const specById = new Map((this.boardDef?.testSpecifications ?? []).map(sp => [sp.id, sp]));
|
|
1765
|
+
const criteriaOf = new Map();
|
|
1766
|
+
if (specById.size) {
|
|
1767
|
+
for (const n of readBoardLocations(this.boardDef ?? {})) {
|
|
1768
|
+
const ids = n.testSpecificationIds;
|
|
1769
|
+
if (!ids?.length)
|
|
1770
|
+
continue;
|
|
1771
|
+
const cs = ids.flatMap(id => specById.get(id)?.criteria ?? []);
|
|
1772
|
+
if (cs.length)
|
|
1773
|
+
criteriaOf.set(n.id, cs);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1666
1776
|
const out = deriveAttentions({
|
|
1667
1777
|
equipment: [...this.equipment.values()],
|
|
1668
|
-
locations: [...this.locations.values()]
|
|
1778
|
+
locations: [...this.locations.values()].map(n => {
|
|
1779
|
+
const criteria = criteriaOf.get(n.id);
|
|
1780
|
+
return criteria?.length ? { ...n, criteria } : n;
|
|
1781
|
+
}),
|
|
1669
1782
|
orders: [...this.orders.values()],
|
|
1670
1783
|
tasks: [...this.tasks.values()]
|
|
1671
1784
|
}, this._acked, now, // 지연 판정의 "지금" — 관측 중이면 마지막으로 들은 시각이다(§nowMs)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AssetState, MaterialQuantity, TwinModelDef, CanonicalEnvelope, LocationState, ItemState, EquipmentState, PersonState, TaskState, OrderState, StructureShift } from './contract.ts';
|
|
1
|
+
import type { AssetState, MaterialQuantity, TwinModelDef, CanonicalEnvelope, LocationState, ItemState, LocationObservation, EquipmentState, PersonState, TaskState, OrderState, StructureShift } from './contract.ts';
|
|
2
2
|
interface ProjItem {
|
|
3
3
|
epc: string;
|
|
4
4
|
/** 로트의 부분(표준 MaterialSubLot.ID) — 비직렬 로트가 자리마다 갈릴 때만. */
|
|
@@ -141,10 +141,17 @@ export interface ReducerCheckpoint {
|
|
|
141
141
|
firstAtMs?: number;
|
|
142
142
|
lastAtMs?: number;
|
|
143
143
|
}[];
|
|
144
|
+
/** 자리별 · 속성별 마지막 관측 — 이어 접기가 이 축을 0부터 다시 만들지 않게. */
|
|
145
|
+
observations?: {
|
|
146
|
+
id: string;
|
|
147
|
+
values: LocationObservation[];
|
|
148
|
+
}[];
|
|
144
149
|
}
|
|
145
150
|
export declare class ObservedReducer {
|
|
146
151
|
/** 로케이션 마스터 — 출처를 함께 들고 있다(마스터가 말한 자리 vs 관측으로 알게 된 자리). */
|
|
147
152
|
private master;
|
|
153
|
+
/** 자리별 · 속성별 **마지막 관측** — 이력이 아니다(§`OP_EVENT.observation`). */
|
|
154
|
+
private observations;
|
|
148
155
|
private items;
|
|
149
156
|
private aggregation;
|
|
150
157
|
/** 아직 관측되지 않은 자식의 담김 — 물품을 지어내지 않고 보류했다가 등장할 때 붙인다. */
|
package/dist/observed-reducer.js
CHANGED
|
@@ -41,6 +41,8 @@ function effectiveOf(r) {
|
|
|
41
41
|
export class ObservedReducer {
|
|
42
42
|
/** 로케이션 마스터 — 출처를 함께 들고 있다(마스터가 말한 자리 vs 관측으로 알게 된 자리). */
|
|
43
43
|
master = new Map();
|
|
44
|
+
/** 자리별 · 속성별 **마지막 관측** — 이력이 아니다(§`OP_EVENT.observation`). */
|
|
45
|
+
observations = new Map();
|
|
44
46
|
items = new Map();
|
|
45
47
|
aggregation = new Map();
|
|
46
48
|
/** 아직 관측되지 않은 자식의 담김 — 물품을 지어내지 않고 보류했다가 등장할 때 붙인다. */
|
|
@@ -339,6 +341,27 @@ export class ObservedReducer {
|
|
|
339
341
|
this.touchLocation(d.location);
|
|
340
342
|
break;
|
|
341
343
|
}
|
|
344
|
+
case OP_EVENT.observation: {
|
|
345
|
+
/*
|
|
346
|
+
* **자리의 물리 관측** — 속성마다 마지막 값 하나만 든다(§`LocationState.observations`).
|
|
347
|
+
*
|
|
348
|
+
* 이력을 들지 않는 이유: 상태가 **시간에 비례해 자라지 않게**. 「그때 몇 도였나」는 저널이
|
|
349
|
+
* 답하는 물음이고, 상태가 답하는 것은 「지금 몇 도냐」다.
|
|
350
|
+
*
|
|
351
|
+
* 늦게 온 옛 관측이 최신을 덮지 않게 `stale` 을 지난다 — 대상 키에 속성을 넣는다(온도가 늦게
|
|
352
|
+
* 와도 습도의 최신은 지켜져야 한다).
|
|
353
|
+
*/
|
|
354
|
+
const d = e.data;
|
|
355
|
+
if (!d?.locationId || !d?.propertyId)
|
|
356
|
+
break;
|
|
357
|
+
if (this.stale(`observation:${d.locationId}:${d.propertyId}`, e))
|
|
358
|
+
return;
|
|
359
|
+
this.touchLocation(d.locationId);
|
|
360
|
+
const bin = this.observations.get(d.locationId) ?? new Map();
|
|
361
|
+
bin.set(d.propertyId, { ...d });
|
|
362
|
+
this.observations.set(d.locationId, bin);
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
342
365
|
case OP_EVENT.attentionAck: {
|
|
343
366
|
/* 확인한 사실만 담는다 — 그 신호가 지금도 성립하는지는 상태가 답한다(여기서 판단하지 않는다). */
|
|
344
367
|
const d = e.data;
|
|
@@ -747,6 +770,8 @@ export class ObservedReducer {
|
|
|
747
770
|
*/
|
|
748
771
|
serialize() {
|
|
749
772
|
return {
|
|
773
|
+
/* 관측도 재개점에 든다 — 없으면 이어 접기가 그 축을 0부터 다시 만든다. */
|
|
774
|
+
observations: [...this.observations.entries()].map(([id, bin]) => ({ id, values: [...bin.values()] })),
|
|
750
775
|
revision: this.revision,
|
|
751
776
|
master: [...this.master.values()].map(n => ({ ...n })),
|
|
752
777
|
items: [...this.items.values()].map(i => ({ ...i })),
|
|
@@ -771,6 +796,7 @@ export class ObservedReducer {
|
|
|
771
796
|
* 모델에서 오는 판정 입력(교대·등급·시간대)은 생성자가 이미 세웠으므로 건드리지 않는다.
|
|
772
797
|
*/
|
|
773
798
|
restore(cp) {
|
|
799
|
+
this.observations = new Map((cp?.observations ?? []).map(o => [o.id, new Map((o.values ?? []).map(v => [v.propertyId, { ...v }]))]));
|
|
774
800
|
this.revision = cp?.revision ?? 0;
|
|
775
801
|
this.master = new Map((cp?.master ?? []).map(n => [n.id, { ...n }]));
|
|
776
802
|
this.items = new Map((cp?.items ?? []).map(i => [i.epc, { ...i }]));
|
|
@@ -807,6 +833,11 @@ export class ObservedReducer {
|
|
|
807
833
|
...(n.capacity === undefined ? {} : { capacity: n.capacity }),
|
|
808
834
|
...(n.parallelism === undefined ? {} : { parallelism: n.parallelism }),
|
|
809
835
|
...(n.parentId ? { parentId: n.parentId } : {}),
|
|
836
|
+
/* 관측된 물리량 — 없으면 **키를 만들지 않는다**(빈 배열은 「센서가 없다」와 「못 들었다」를 같게 만든다). */
|
|
837
|
+
...(() => {
|
|
838
|
+
const bin = this.observations.get(n.id);
|
|
839
|
+
return bin?.size ? { observations: [...bin.values()] } : {};
|
|
840
|
+
})(),
|
|
810
841
|
origin: n.origin
|
|
811
842
|
};
|
|
812
843
|
}),
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -599,7 +599,19 @@ var OP_EVENT = {
|
|
|
599
599
|
* "확인해라"(요청)이고 이벤트는 "확인했다"(사실)다. 같은 문자열을 쓰면 저널에서 요청과 사실이
|
|
600
600
|
* 구별되지 않는다.
|
|
601
601
|
*/
|
|
602
|
-
attentionAck: "attention.acked"
|
|
602
|
+
attentionAck: "attention.acked",
|
|
603
|
+
/**
|
|
604
|
+
* **자리에서 관측된 물리량** — 냉장실 온도·습도, 세척수 유량 같은 것(§`LocationObservation`).
|
|
605
|
+
*
|
|
606
|
+
* ── 왜 채널이 필요한가 ────────────────────────────────────────────────────
|
|
607
|
+
* `LocationState.observations` 를 상태에 두었는데 그것을 낳는 사건이 없었다. 그러면 **상태 ⊆ 이벤트**
|
|
608
|
+
* 가 깨진다: 채워도 재기동에서 사라지고, 폴드가 되살릴 수 없고, 미러가 이어받을 수도 없다. 상태에만
|
|
609
|
+
* 있는 축은 조용히 사라지는 축이다.
|
|
610
|
+
*
|
|
611
|
+
* 이름을 `energy.measured` 와 같은 결로 둔다 — 그쪽이 에너지 계량의 도착이고 이쪽이 그 일반형이다.
|
|
612
|
+
* 두 채널을 합치지 않는 이유는 에너지 쪽이 **구간에 누적되는 표본**이라 처리가 다르기 때문이다.
|
|
613
|
+
*/
|
|
614
|
+
observation: "location.measured"
|
|
603
615
|
};
|
|
604
616
|
var ENERGY_EVENT = {
|
|
605
617
|
/** 계량 도착 — 그 시점의 유효전력·누적량. 15분 수요 구간에 누적된다. */
|
|
@@ -971,14 +983,75 @@ var DISP = {
|
|
|
971
983
|
sellable: "urn:epcglobal:cbv:disp:sellable_accessible",
|
|
972
984
|
reserved: "urn:epcglobal:cbv:disp:reserved",
|
|
973
985
|
in_transit: "urn:epcglobal:cbv:disp:in_transit",
|
|
974
|
-
non_sellable: "urn:epcglobal:cbv:disp:non_sellable_other"
|
|
986
|
+
non_sellable: "urn:epcglobal:cbv:disp:non_sellable_other",
|
|
975
987
|
// 불량/scrap
|
|
988
|
+
/**
|
|
989
|
+
* **기한이 지났다** — CBV `expired`.
|
|
990
|
+
*
|
|
991
|
+
* ── 왜 `non_sellable` 로 접지 않나 (2026-08-24) ─────────────────────────────
|
|
992
|
+
* 커널의 `non_sellable` 은 CBV 의 `non_sellable_other`, 즉 **「그 밖의 이유」**다. 기한 지남을 거기
|
|
993
|
+
* 넣으면 「기한이 지나 못 판다」와 「깨져서 못 판다」가 같은 값이 되고, 화면은 회수·폐기의 사유를
|
|
994
|
+
* 구별할 수 없다. 식품에서 그 둘은 다른 조치다.
|
|
995
|
+
*
|
|
996
|
+
* 그리고 표준에 **정확한 낱말이 있다** — 접는 것은 있는 낱말을 버리는 것이다.
|
|
997
|
+
*
|
|
998
|
+
* ── 기한 날짜와 다른 축이다 ────────────────────────────────────────────────
|
|
999
|
+
* `ItemState.expiry` 는 **날짜**이고 이것은 **상태**다. 날짜가 있으면 「지났나」는 파생이지만, 원본이
|
|
1000
|
+
* 「기한 지남」을 상태로 선언하는 시스템이 있다 — 그때 이 값은 관측이다.
|
|
1001
|
+
*
|
|
1002
|
+
* 둘이 어긋나면(날짜는 남았는데 상태가 지남, 또는 그 반대) **어느 쪽이 맞다고 정하지 않는다** —
|
|
1003
|
+
* 아직 그 판정을 세울 근거가 없다. 어긋남의 구분을 없애지 않는 것이 지금의 규율이다.
|
|
1004
|
+
*
|
|
1005
|
+
* ── 원문으로 확인했다 (2026-08-24) ────────────────────────────────────────
|
|
1006
|
+
* 1차 출처: **CBV Standard Release 2.0, Ratified Jun 2022** §7.2.3 처분 값 표(38개). 이 객체의
|
|
1007
|
+
* 다른 값들(`in_progress`·`sellable_accessible`·`reserved`·`in_transit`·`non_sellable_other`)도
|
|
1008
|
+
* 그 표에 있다.
|
|
1009
|
+
*
|
|
1010
|
+
* **`non_sellable_expired` 를 쓰지 않는 이유**: 그 값은 CBV 1.0 의 것이고 표준이 **폐기**했다 —
|
|
1011
|
+
* 「deprecated in favour of new disposition values expired, damaged, disposed, … introduced in
|
|
1012
|
+
* CBV 1.1」. 폐기된 값을 쓰면 새 소비처가 읽지 못한다.
|
|
1013
|
+
*
|
|
1014
|
+
* 참고: GS1 어휘 등록처(`ref.gs1.org/cbv/…`)로는 확인할 수 없었다 — **없는 값에도 같은 응답**을
|
|
1015
|
+
* 준다(지어낸 값의 JSON-LD 가 실재 값과 바이트까지 같았다). 그 경로를 근거로 삼지 말 것.
|
|
1016
|
+
*/
|
|
1017
|
+
expired: "urn:epcglobal:cbv:disp:expired",
|
|
1018
|
+
/**
|
|
1019
|
+
* **검사에 합격했다 / 불합격했다** — CBV `conformant` / `non_conformant`.
|
|
1020
|
+
*
|
|
1021
|
+
* 1차 출처(CBV 2.0 §7.2.3) 정의 그대로다.
|
|
1022
|
+
*
|
|
1023
|
+
* conformant Outcome of a successful/passed inspection in an inspecting or repairing step
|
|
1024
|
+
* non_conformant Outcome of an unsuccessful/failed inspection in an inspecting or repairing step
|
|
1025
|
+
*
|
|
1026
|
+
* ── 왜 시험 결과 축을 자원에 더하지 않고 이것을 쓰나 (2026-08-24) ────────────
|
|
1027
|
+
* 로트의 검사 판정을 담을 자리를 찾다가 `ItemState.testResults` 를 더하려 했다. 그런데 표준은 그
|
|
1028
|
+
* 사실을 **이미 처분으로 말한다**: `bizStep: inspecting` 사건에 이 처분이 붙는다.
|
|
1029
|
+
*
|
|
1030
|
+
* 처분을 쓰면 두 가지가 공짜로 성립한다.
|
|
1031
|
+
* ① **상태 ⊆ 이벤트** — 처분은 이미 사건에서 온다. 상태에만 있는 축을 만들지 않는다
|
|
1032
|
+
* ② **운영에 곧 닿는다** — 「이 자재를 쓸 수 있나」가 처분으로 답해진다(판정을 따로 읽지 않는다)
|
|
1033
|
+
*
|
|
1034
|
+
* 시험의 **자세한 내용**(어느 명세로, 무엇을 재어)은 다른 물음이고, 표준은 그것을 `TestResult` 로
|
|
1035
|
+
* 두며 결과가 대상을 가리킨다(`TestableObjectID`) — 대상이 결과를 들지 않는다. 그 축이 필요해지면
|
|
1036
|
+
* 그때 열되, **판정 자체는 여기서 끝난다.**
|
|
1037
|
+
*/
|
|
1038
|
+
conformant: "urn:epcglobal:cbv:disp:conformant",
|
|
1039
|
+
non_conformant: "urn:epcglobal:cbv:disp:non_conformant"
|
|
976
1040
|
};
|
|
977
1041
|
var CBV_BIZSTEP = {
|
|
978
1042
|
/** 공정에 자재가 들어갔다 — ISA-95 `MaterialUse: Consumed`. */
|
|
979
1043
|
consuming: "urn:epcglobal:cbv:bizstep:consuming",
|
|
980
1044
|
/** 새 물품이 생겨 계보가 시작된다 — ISA-95 `MaterialUse: Produced`. */
|
|
981
|
-
commissioning: "urn:epcglobal:cbv:bizstep:commissioning"
|
|
1045
|
+
commissioning: "urn:epcglobal:cbv:bizstep:commissioning",
|
|
1046
|
+
/**
|
|
1047
|
+
* **검사** — CBV `inspecting`. 1차 출처(CBV 2.0) 정의: 「Process of reviewing objects to address
|
|
1048
|
+
* potential physical or documentation defects」이고, 「표본과 달리 검사된 대상은 그대로 남는다」고
|
|
1049
|
+
* 이어진다(즉 검사는 물건을 소비하지 않는다).
|
|
1050
|
+
*
|
|
1051
|
+
* 이 단계에 `DISP.conformant`/`DISP.non_conformant` 가 붙어 판정이 처분으로 남는다 — 입고검수·
|
|
1052
|
+
* 공정 중 검사가 그 모양이다.
|
|
1053
|
+
*/
|
|
1054
|
+
inspecting: "urn:epcglobal:cbv:bizstep:inspecting"
|
|
982
1055
|
};
|
|
983
1056
|
function ssccUri(companyPrefix, serial) {
|
|
984
1057
|
return `urn:epc:id:sscc:${companyPrefix}.${String(serial).padStart(10, "0")}`;
|
|
@@ -1256,6 +1329,8 @@ function effectiveOf(r) {
|
|
|
1256
1329
|
var ObservedReducer = class {
|
|
1257
1330
|
/** 로케이션 마스터 — 출처를 함께 들고 있다(마스터가 말한 자리 vs 관측으로 알게 된 자리). */
|
|
1258
1331
|
master = /* @__PURE__ */ new Map();
|
|
1332
|
+
/** 자리별 · 속성별 **마지막 관측** — 이력이 아니다(§`OP_EVENT.observation`). */
|
|
1333
|
+
observations = /* @__PURE__ */ new Map();
|
|
1259
1334
|
items = /* @__PURE__ */ new Map();
|
|
1260
1335
|
aggregation = /* @__PURE__ */ new Map();
|
|
1261
1336
|
/** 아직 관측되지 않은 자식의 담김 — 물품을 지어내지 않고 보류했다가 등장할 때 붙인다. */
|
|
@@ -1524,6 +1599,16 @@ var ObservedReducer = class {
|
|
|
1524
1599
|
this.touchLocation(d.location);
|
|
1525
1600
|
break;
|
|
1526
1601
|
}
|
|
1602
|
+
case OP_EVENT.observation: {
|
|
1603
|
+
const d = e.data;
|
|
1604
|
+
if (!d?.locationId || !d?.propertyId) break;
|
|
1605
|
+
if (this.stale(`observation:${d.locationId}:${d.propertyId}`, e)) return;
|
|
1606
|
+
this.touchLocation(d.locationId);
|
|
1607
|
+
const bin = this.observations.get(d.locationId) ?? /* @__PURE__ */ new Map();
|
|
1608
|
+
bin.set(d.propertyId, { ...d });
|
|
1609
|
+
this.observations.set(d.locationId, bin);
|
|
1610
|
+
break;
|
|
1611
|
+
}
|
|
1527
1612
|
case OP_EVENT.attentionAck: {
|
|
1528
1613
|
const d = e.data;
|
|
1529
1614
|
if (d?.id) this.acked.add(d.id);
|
|
@@ -1846,6 +1931,8 @@ var ObservedReducer = class {
|
|
|
1846
1931
|
*/
|
|
1847
1932
|
serialize() {
|
|
1848
1933
|
return {
|
|
1934
|
+
/* 관측도 재개점에 든다 — 없으면 이어 접기가 그 축을 0부터 다시 만든다. */
|
|
1935
|
+
observations: [...this.observations.entries()].map(([id, bin]) => ({ id, values: [...bin.values()] })),
|
|
1849
1936
|
revision: this.revision,
|
|
1850
1937
|
master: [...this.master.values()].map((n) => ({ ...n })),
|
|
1851
1938
|
items: [...this.items.values()].map((i) => ({ ...i })),
|
|
@@ -1870,6 +1957,9 @@ var ObservedReducer = class {
|
|
|
1870
1957
|
* 모델에서 오는 판정 입력(교대·등급·시간대)은 생성자가 이미 세웠으므로 건드리지 않는다.
|
|
1871
1958
|
*/
|
|
1872
1959
|
restore(cp) {
|
|
1960
|
+
this.observations = new Map(
|
|
1961
|
+
(cp?.observations ?? []).map((o) => [o.id, new Map((o.values ?? []).map((v) => [v.propertyId, { ...v }]))])
|
|
1962
|
+
);
|
|
1873
1963
|
this.revision = cp?.revision ?? 0;
|
|
1874
1964
|
this.master = new Map((cp?.master ?? []).map((n) => [n.id, { ...n }]));
|
|
1875
1965
|
this.items = new Map((cp?.items ?? []).map((i) => [i.epc, { ...i }]));
|
|
@@ -1905,6 +1995,11 @@ var ObservedReducer = class {
|
|
|
1905
1995
|
...n.capacity === void 0 ? {} : { capacity: n.capacity },
|
|
1906
1996
|
...n.parallelism === void 0 ? {} : { parallelism: n.parallelism },
|
|
1907
1997
|
...n.parentId ? { parentId: n.parentId } : {},
|
|
1998
|
+
/* 관측된 물리량 — 없으면 **키를 만들지 않는다**(빈 배열은 「센서가 없다」와 「못 들었다」를 같게 만든다). */
|
|
1999
|
+
...(() => {
|
|
2000
|
+
const bin = this.observations.get(n.id);
|
|
2001
|
+
return bin?.size ? { observations: [...bin.values()] } : {};
|
|
2002
|
+
})(),
|
|
1908
2003
|
origin: n.origin
|
|
1909
2004
|
};
|
|
1910
2005
|
}),
|
|
@@ -3421,6 +3516,34 @@ function deriveAttentions(view, acked, nowIso, thresholds) {
|
|
|
3421
3516
|
}
|
|
3422
3517
|
}
|
|
3423
3518
|
for (const n of view.locations) {
|
|
3519
|
+
for (const c of n.criteria ?? []) {
|
|
3520
|
+
const propertyId = c.evaluatedPropertyId;
|
|
3521
|
+
if (!propertyId) continue;
|
|
3522
|
+
const observed = (n.observations ?? []).find((o) => o.propertyId === propertyId);
|
|
3523
|
+
if (!observed) continue;
|
|
3524
|
+
if (outsideLimit(c, observed) !== true) continue;
|
|
3525
|
+
out.push({
|
|
3526
|
+
/* 자리·기준마다 하나 — 같은 방의 온도와 습도가 한 신호로 뭉치지 않는다. */
|
|
3527
|
+
id: `observation-out-of-limit:${n.id}:${c.id}`,
|
|
3528
|
+
kind: "observation-out-of-limit",
|
|
3529
|
+
severity: "high",
|
|
3530
|
+
anchor: { locationId: n.id },
|
|
3531
|
+
/* 언어중립 원시값만 — 문장은 표현계층이 kind 로 골라 렌더한다. */
|
|
3532
|
+
params: {
|
|
3533
|
+
locationId: n.id,
|
|
3534
|
+
criterionId: c.id,
|
|
3535
|
+
propertyId,
|
|
3536
|
+
/* 값이 없으면 판정이 서지 않으므로 여기 오지 않는다 — 그래도 타입을 좁혀 둔다. */
|
|
3537
|
+
value: observed.value ?? "",
|
|
3538
|
+
...observed.uom ? { uom: observed.uom } : {},
|
|
3539
|
+
...c.limit?.minimum !== void 0 ? { minimum: c.limit.minimum } : {},
|
|
3540
|
+
...c.limit?.maximum !== void 0 ? { maximum: c.limit.maximum } : {},
|
|
3541
|
+
/* **언제의 값인가** — 4개월 전 값으로 지금을 판정한 것인지 소비처가 알아야 한다. */
|
|
3542
|
+
effectiveTime: observed.effectiveTime,
|
|
3543
|
+
...observed.derived ? { derived: "true" } : {}
|
|
3544
|
+
}
|
|
3545
|
+
});
|
|
3546
|
+
}
|
|
3424
3547
|
if ((n.capacity ?? 0) > 0) {
|
|
3425
3548
|
const r = (n.occupancy ?? 0) / n.capacity;
|
|
3426
3549
|
const declaredPct = thresholds?.congestionPctOf?.(n.id);
|
|
@@ -3801,6 +3924,47 @@ var FlowEngine = class {
|
|
|
3801
3924
|
localParams = /* @__PURE__ */ new Map();
|
|
3802
3925
|
/** 관측 구동(P0) — 이벤트를 접는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
|
|
3803
3926
|
observer;
|
|
3927
|
+
/**
|
|
3928
|
+
* **관측 리듀서의 재개점을 꺼낸다** — 호스트가 저장해 다음 기동에서 되돌릴 수 있게.
|
|
3929
|
+
*
|
|
3930
|
+
* ── 무엇이 문제였나 (2026-08-24 실측) ──────────────────────────────────────
|
|
3931
|
+
* 미러는 재기동마다 저널을 **0부터** 다시 집계했다. 실측으로 저널이 2,960만 줄이고, 그 때문에 기동
|
|
3932
|
+
* 직후 몇 분간 상태가 비어 있었다. 계측처럼 경계 없이 자라는 흐름이 들어오면 그 몇 분이 몇십 분이
|
|
3933
|
+
* 된다 — 불편이 아니라 벽이다.
|
|
3934
|
+
*
|
|
3935
|
+
* 재개점 자체는 오래전부터 있었다(`serialize`/`restore`). 조회 경로는 그것을 쓰는데
|
|
3936
|
+
* (`replayFrom`) **라이브 경로에는 꺼낼 문이 없었다.** 그래서 호스트가 저장할 수 없었다.
|
|
3937
|
+
*
|
|
3938
|
+
* ── 상태 스냅샷으로는 대신할 수 없다 ──────────────────────────────────────
|
|
3939
|
+
* 리듀서는 소비처가 보는 값 말고도 든다: 부모를 기다리는 담김·집계 중인 수량·담을 줄 몰라 세어 둔
|
|
3940
|
+
* 사건. 상태만 되돌리고 이어 집계하면 **0부터 집계한 결과와 조용히 달라진다**(§`ReducerCheckpoint`).
|
|
3941
|
+
*
|
|
3942
|
+
* 관측 구동이 아니면 `undefined` — 시뮬은 리듀서를 갖지 않는다(저장할 것이 없다).
|
|
3943
|
+
*/
|
|
3944
|
+
observedCheckpoint() {
|
|
3945
|
+
return this.observer?.serialize();
|
|
3946
|
+
}
|
|
3947
|
+
/**
|
|
3948
|
+
* **재개점에서 관측 리듀서를 되세운다** — 저널을 0부터 다시 집계하지 않게.
|
|
3949
|
+
*
|
|
3950
|
+
* 리듀서가 아직 없으면 만든다: 미러는 첫 봉투가 올 때 리듀서를 만드는데(§`apply`), 되돌리기는 그보다
|
|
3951
|
+
* 먼저 일어나야 한다(그러지 않으면 첫 봉투가 빈 리듀서를 만들고 되돌린 것을 덮는다).
|
|
3952
|
+
*
|
|
3953
|
+
* 되돌린 뒤 상태로 옮긴다 — 그러지 않으면 첫 스냅샷이 빈 상태를 보인다.
|
|
3954
|
+
*
|
|
3955
|
+
* **구조가 다르면 되돌리지 않는다**: 재개점은 그 모델 위에서 만들어진 것이고, 다른 공장의 재개점을
|
|
3956
|
+
* 얹으면 없는 자리·설비가 생긴다. 판단은 부르는 쪽이 한다(`structureRev` 를 아는 것은 호스트다) —
|
|
3957
|
+
* 여기서는 받은 것을 그대로 세운다.
|
|
3958
|
+
*/
|
|
3959
|
+
restoreObserved(cp) {
|
|
3960
|
+
if (!this.observer) {
|
|
3961
|
+
this.observer = new ObservedReducer(this.boardDef ?? { locations: [], equipment: [] });
|
|
3962
|
+
this.observeMode = true;
|
|
3963
|
+
}
|
|
3964
|
+
this.observer.restore(cp);
|
|
3965
|
+
this.observedDirty = true;
|
|
3966
|
+
this.settleObserved();
|
|
3967
|
+
}
|
|
3804
3968
|
/**
|
|
3805
3969
|
* 이 커널의 상태가 **관측에서 왔나** — 미러인가.
|
|
3806
3970
|
*
|
|
@@ -4700,10 +4864,23 @@ var FlowEngine = class {
|
|
|
4700
4864
|
*/
|
|
4701
4865
|
collectAttentions() {
|
|
4702
4866
|
const now = this.now();
|
|
4867
|
+
const specById = new Map((this.boardDef?.testSpecifications ?? []).map((sp) => [sp.id, sp]));
|
|
4868
|
+
const criteriaOf = /* @__PURE__ */ new Map();
|
|
4869
|
+
if (specById.size) {
|
|
4870
|
+
for (const n of readBoardLocations(this.boardDef ?? {})) {
|
|
4871
|
+
const ids = n.testSpecificationIds;
|
|
4872
|
+
if (!ids?.length) continue;
|
|
4873
|
+
const cs = ids.flatMap((id) => specById.get(id)?.criteria ?? []);
|
|
4874
|
+
if (cs.length) criteriaOf.set(n.id, cs);
|
|
4875
|
+
}
|
|
4876
|
+
}
|
|
4703
4877
|
const out = deriveAttentions(
|
|
4704
4878
|
{
|
|
4705
4879
|
equipment: [...this.equipment.values()],
|
|
4706
|
-
locations: [...this.locations.values()]
|
|
4880
|
+
locations: [...this.locations.values()].map((n) => {
|
|
4881
|
+
const criteria = criteriaOf.get(n.id);
|
|
4882
|
+
return criteria?.length ? { ...n, criteria } : n;
|
|
4883
|
+
}),
|
|
4707
4884
|
orders: [...this.orders.values()],
|
|
4708
4885
|
tasks: [...this.tasks.values()]
|
|
4709
4886
|
},
|
package/package.json
CHANGED