@operato/twin-kernel 0.7.55 → 0.7.57

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 CHANGED
@@ -1,28 +1,50 @@
1
- # @operato-twin/kernel — 비즈니스레이어 디지털트윈 커널
1
+ # @operato/twin-kernel — 비즈니스레이어 디지털트윈 커널
2
2
 
3
- 물류창고·야드·스마트팩토리의 **비즈니스 실행**을 시뮬레이션·모니터링하는 **헤드리스·프레임워크 무관·zero-dep** TS 커널. UI/3D/DOM 없이 순수 Node 에서 도는 것이 목적(sim=능동 생산, live=수동 미러, 계약 동일).
3
+ 물류창고·야드·스마트팩토리·에너지의 **비즈니스 실행**을 시뮬레이션·모니터링하는 **헤드리스·프레임워크 무관·zero-dep** TS 커널. UI/3D/DOM 없이 순수 Node 에서 동작하는 것이 목적(sim=능동 생산, live=수동 미러, **계약 동일**).
4
4
 
5
- - 실행: `node --test test/*.test.ts` (Node 25 네이티브 TS, **무의존**)
6
- - 상태: **v0.2.3 · 261 tests green**, 3 버티컬(WMS/YMS/MES), ISA-95 4대 자원
5
+ - 실행: `node --test test/*.test.ts` — 네이티브 TS, **런타임 의존성 0개**
6
+ - 상태: **v0.7.56 · 1,085 tests green** · 시험 파일 116개 · 4 버티컬(WMS/YMS/MES/EMS) · ISA-95 4대 자원
7
7
 
8
8
  ## 계층 (아래로만 의존, 무방언)
9
9
 
10
10
  ```
11
- contract.ts 3채널 계약(State/Command/Scenario) + 운영 델타 + TwinKernel ← 도메인/표준 무관
12
- epcis.ts GS1 EPCIS 2.0 이벤트 machinery(타입·빌더·검증기·URI·CBV disp) ← 표준(도메인 무관)
13
- flow-engine.ts FlowEngine base — mechanics(RNG·clock·tick·자원배정·태스크진행·emit·snapshot)
14
- allocation-policy.ts AllocationPolicy 확장 시임(selectPlacement/selectStock)
15
- observed-reducer.ts 관측 이벤트 → State 접기(**두 구동이 쓰는 유일한 규칙**) ← 도메인 무관
16
- domain-definition.ts 공정 명세(ISA-95 OperationsSegment: 소요·변동·수율·자원 요구)
17
- iso-duration.ts xsd:duration 파서(달력 의존 단위는 거절)
18
- runtime.ts TwinRuntime — host-facing facade + 구독 프로토콜 ← 도메인/전송 무관
19
- face2-adapter.ts 레거시 레코드 → 정규 EPCIS(선언적 매핑 + 검증) ← ACL
20
-
21
- {wms,yms,mes}-profile.ts 도메인 어휘(bizStep/btt)만
22
- {wms,yms,mes}-kernel.ts 도메인 flow 동사(4 hook) — FlowEngine 확장
11
+ contract.ts 3채널 계약(State/Command/Scenario) + 운영 델타 + TwinKernel ← 도메인/표준 무관
12
+ epcis.ts GS1 EPCIS 2.0 이벤트 machinery(타입·빌더·검증기·URI·CBV) ← 표준(도메인 무관)
13
+ vocabulary.ts 은퇴 어휘 목록 + 가드 프라그마 — 개명을 기계가 지킨다
14
+ flow-engine.ts FlowEngine base — mechanics(RNG·clock·tick·자원배정·태스크진행·emit·snapshot)
15
+ observed-reducer.ts 관측 이벤트 → State 접기(**두 구동이 쓰는 유일한 규칙**) + 재개점 ← 도메인 무관
16
+ state-projector.ts 위 것의 옛 이름(소비처 호환 별칭)
17
+
18
+ allocation-policy.ts AllocationPolicy 확장 시임(selectPlacement/selectStock)
19
+ capability.ts 자원의 능력 선언 — concrete 결합 대신 능력으로 찾는다
20
+ capacity.ts 용량·점유 계산
21
+ operations-capability.ts 공정이 요구하는 능력 ↔ 자원이 가진 능력의 대조
22
+ domain-definition.ts 공정 명세(ISA-95 OperationsSegment: 소요·변동·수율·자원 요구)
23
+ domain-catalog.ts 도메인 어휘 카탈로그(선언 가능한 것의 목록)
24
+ duration-estimator.ts 실측 추정기 주입 시임
25
+ iso-duration.ts xsd:duration 파서(달력 의존 단위는 거절)
26
+ make-to-order.ts 주문 대응 생산
27
+ task-fold.ts 태스크 접기
28
+ job-response.ts ISA-95 작업 응답
29
+
30
+ event-journal.ts 이벤트 저널 + replay/replayFrom — 트윈의 기억
31
+ forecast.ts 몬테카를로 예측(분포로 답한다)
32
+ divergence.ts 예측 vs 관측 대조 → 드리프트 국소화
33
+ counterfactual.ts 과거 시점 분기 — "그때 X 했다면"
34
+ twin-observer.ts 자동 정합 루프
35
+
36
+ energy-ingest.ts 계측 유입(적산·구간)
37
+ energy-attribution.ts 에너지를 사용처로 귀속
38
+ operational-ingest.ts 운영 사실 유입
39
+ scenario-validate.ts 시나리오 선언 검증
40
+ face2-adapter.ts 레거시 레코드 → 정규 EPCIS(선언적 매핑 + 검증) ← ACL
41
+ runtime.ts TwinRuntime — host-facing facade + 구독 프로토콜 ← 도메인/전송 무관
42
+
43
+ {wms,yms,mes,ems}-profile.ts 도메인 어휘(bizStep/btt/표준 앵커)만
44
+ kernel.ts · {yms,mes,ems}-kernel.ts 도메인 flow 동사(4 hook) — FlowEngine 확장
23
45
  ```
24
46
 
25
- > **무방언 원칙**: 코어(contract/epcis/flow-engine/policy/reducer/runtime)에 특정 도메인 용어를 넣지 않는다. EPCIS 는 표준이라 `epcis.ts`(≠wms). order.kind·task.resourceType 등은 도메인이 소유. 정책 인터페이스는 `selectPlacement`/`selectStock`(≠putaway/pallets).
47
+ > **무방언 원칙**: 코어(contract/epcis/flow-engine/policy/reducer/runtime)에 특정 도메인 용어를 넣지 않는다. EPCIS 는 표준이라 `epcis.ts`(≠wms). `order.kind`·`task.resourceType` 등은 도메인이 소유. 정책 인터페이스는 `selectPlacement`/`selectStock`(≠putaway/pallets).
26
48
 
27
49
  ## FlowEngine — 단일 base, 도메인은 4 hook 만
28
50
 
@@ -35,17 +57,18 @@ face2-adapter.ts 레거시 레코드 → 정규 EPCIS(선언적 매핑 + 검증)
35
57
  | `allocate(order)` | created 오더 할당 → 재고 선택(정책) + 태스크. **시간창 게이트도 여기**(스케줄링) |
36
58
  | `onTaskComplete(task)` | 완료의 의미 — 이동(WMS/YMS) 또는 변환(MES). occupancy·EPCIS·오더 이행 전부 도메인 |
37
59
 
38
- base 는 task **생명주기**(자원 배정·진행·완료·해제·델타)만 소유 → 이동-중립. 새 버티컬 = 프로파일(어휘) + kernel(4 hook), **~100줄**.
60
+ base 는 태스크 **생명주기**(자원 배정·진행·완료·해제·델타)만 소유 → 이동-중립. 새 버티컬 = 프로파일(어휘) + kernel(4 hook).
39
61
 
40
- ## 3 버티컬 — base 가 4 flow 성격을 담음을 실증
62
+ ## 4 버티컬 — base 가 여러 flow 성격을 담음을 실증
41
63
 
42
64
  | 버티컬 | flow 성격 | base 대응 |
43
65
  |---|---|---|
44
66
  | **WMS** | 이동 (putaway/pick/pack/ship) + 백오더 | 기본 |
45
67
  | **YMS** | 이동 (spot/pull) + **시간창 스케줄링**(어포인트먼트↔도크도어 예약) | 스케줄링=도메인 게이팅으로 **base 흡수** |
46
- | **MES** | **변환**(TransformationEvent) + 다단계 라우팅 + **이종 자원**(cutter/welder) | 자원 매칭=`FlowTask.resourceType` **base 확장**(load-bearing 검증) |
68
+ | **MES** | **변환**(TransformationEvent) + 다단계 라우팅 + **이종 자원**(cutter/welder) | 자원 매칭=`FlowTask.resourceType` **base 확장** |
69
+ | **EMS** | 계측의 흐름 — 적산·구간 마감·피크·귀속 | 유입·귀속 모듈로 확장(자리=**전기적 구간**) |
47
70
 
48
- 두 진화 축: 시간창 스케줄링=흡수, 자원-타입 매칭=최소 확장. 둘 다 backward-compatible(기존 도메인 무영향, 예제 바이트 동일로 검증).
71
+ EMS 는 **표준 앵커가 다르다**: 물류·생산은 ISA-95/EPCIS 로 재고 에너지는 **IEC 61850**(설비 데이터 모델)·**ISO 50001**(에너지 경영)로 잰다. 대응할 표준 이름이 없으면 그 칸을 **비운다** — 억지로 가까운 이름을 적으면 적합성 표가 거짓을 말한다.
49
72
 
50
73
  ## 4대 자원 (ISA-95)
51
74
 
@@ -54,7 +77,7 @@ base 는 task **생명주기**(자원 배정·진행·완료·해제·델타)만
54
77
  | ISA-95 | 커널 | 요구 명세 |
55
78
  |---|---|---|
56
79
  | Personnel | `persons` · `PersonState` | `personnelSpecification` = 등급 + 인원 수 |
57
- | Equipment | `movers` · `MoverState` | `equipmentSpecification` = 등급 + 대수 |
80
+ | Equipment | `equipment` · `EquipmentState` | `equipmentSpecification` = 등급 + 대수 |
58
81
  | PhysicalAsset | `assets` · `AssetState` (GS1 **GRAI**) | `physicalAssetSpecification` |
59
82
  | Material | `items` · `ItemState` (EPCIS) | — |
60
83
 
@@ -62,6 +85,24 @@ base 는 task **생명주기**(자원 배정·진행·완료·해제·델타)만
62
85
 
63
86
  > 팔레트를 물품으로 두지 않은 이유: GS1 에서 **SSCC**(물류단위)와 **GRAI**(돌아오는 팔레트 자체)는 다른 것이다. 같은 GRAI 가 오늘은 이 SSCC 를, 내일은 다른 SSCC 를 싣는다. 그래서 **신설하고 연결**한다(`AssetState.carrying` ↔ `ItemState.carriedBy`).
64
87
 
88
+ 자원에는 **자격**이 붙는다. 등급이 시험을 요구하고(`testSpecificationIds`) 개체가 결과를 든다(`testResults`) — 요구된 시험의 결과가 없거나 만료·불합격이면 자격이 성립하지 않는다(`meetsTests`). 결과가 **아예 없는 것**과 **불합격**을 같은 값으로 만들지 않는다.
89
+
90
+ ## 자리와 그 물리 조건
91
+
92
+ `locations` · `LocationState` — 자리는 ISA-95 설비 계층의 노드다(Enterprise→Site→Area→StorageZone→StorageUnit).
93
+
94
+ 자리는 **물리 조건**을 든다: `observations`(속성별 최신 관측 — 온도·습도 등)와 `testSpecificationIds`(그 자리에 걸린 기준). 조건 없이는 물건의 상태를 판정할 수 없고, **이 조인은 트윈만 할 수 있다** — 계측 시스템은 물건의 자리 이력을 모르고, 물류 시스템은 조건 이력을 모른다.
95
+
96
+ | 축 | 표준 | 우리가 더한 것 |
97
+ |---|---|---|
98
+ | 관측 | `OperationsEvent` + `OperationsRecordTemplate`(`EffectiveTimestamp`·`EffectiveEndDate`·`HierarchyScope`) | 값을 `ValueType` 으로 **좁혔다**(단위 없는 물리량은 판정의 재료가 못 된다) |
99
+ | 기준 | `TestSpecificationCriteria`(`Expression` = 자유 문장) | **숫자 한계** `limit: {minimum, maximum, uom}` — 자유 문장으로는 판정할 수 없다 |
100
+ | 판정 | `TestResult.EvaluatedCriterionResult`(TextType) | `pass`/`fail` 로 좁힘 · CBV 처분 `conformant`/`non_conformant` |
101
+
102
+ 판정 함수 `outsideLimit()` 은 **세 갈래로 답한다** — 벗어남(`true`) · 안(`false`) · **판정 못 함(`undefined`)**. 숫자 한계가 없거나, 값이 수가 아니거나, 양쪽이 서로 다른 단위를 말하면 거절한다. 「모름」을 「적합」으로 만들지 않기 위한 것이고, 규제 기록에서 그 구별이 사라지면 결함이 아니라 사고다. 근거의 빈 곳은 `testEvidenceGaps()` 가 따로 센다.
103
+
104
+ 관측은 **사건으로 들어온다**(`OP_EVENT.observation` = `location.measured`) — 상태에만 있는 축은 조용히 사라지는 축이다. 구간 관측(`effectiveEndTime`)은 그 구간 안에서만 참이다: 원본이 「그날 아침」만 말하면 09:00 을 지어내지 않는다.
105
+
65
106
  ## 공정 명세와 소요시간 세 층
66
107
 
67
108
  `OperationDef` 가 ISA-95 `OperationsSegment` 를 담는다 — `duration`(xsd:duration) · `variability`(퍼짐) · `parameters`(수율·준비시간) · 자원 요구 셋. 소요시간은 **강한 근거부터** 찾는다:
@@ -74,48 +115,96 @@ base 는 task **생명주기**(자원 배정·진행·완료·해제·델타)만
74
115
 
75
116
  ## 3채널 계약 (Face 1) + 구독
76
117
 
77
- - **State**: `getSnapshot()`(스냅샷) + 델타 이벤트 스트림. 델타 = EPCIS(재고/위치/조립/변환) + 운영 델타(`task/equipment/order.status`, EPCIS 로 재구성 불가한 절반).
78
- - **Command** (행위/act): `dispatch(cmd)` 가 실제로 sim 을 변이 → State 델타 유발(폐루프). 코어 공통 `order.hold`/`resume`(할당 보류/재개) + 도메인 `order.release`(즉시 투입) 등은 `handleCommand` 시임으로 확장.
79
- - **Scenario**: `scenario.load/start/pause/setSpeed`(시뮬 자극).
118
+ - **State**: `getSnapshot()` + 델타 이벤트 스트림. 델타 = EPCIS(재고/위치/조립/변환) + 운영 델타(`task/equipment/order.status`, EPCIS 로 재구성 불가한 절반) + 관측(`location.measured`).
119
+ - **Command**(행위/act): `dispatch(cmd)` 가 실제로 상태를 변이 → State 델타 유발(폐루프). 공통 `order.hold`/`resume` + 도메인 확장은 `handleCommand` 시임.
120
+ - **Scenario**: `scenario.load/start/pause/setSpeed`(시뮬 자극). 선언은 `scenario-validate.ts` 가 검증한다.
80
121
  - **TwinRuntime**: `subscribe`(snapshot→delta, revision 연속) · `resync` · `tick`. sparse 스트리밍(전이·move-start 만, 매 tick 아님).
81
122
 
123
+ `StateSnapshot` 의 축: `locations` · `items` · `equipment` · `persons` · `assets` · `tasks` · `orders` · `attentions` · `energy` · `conformance` · `nowTime` · `identityGrounding` · `unhandled` · `stepsWithoutMaterial`.
124
+
125
+ **`nowTime` 은 트윈이 직접 말한다.** 소비처가 벽시계로 대신 재면 시뮬 트윈에서 엉뚱한 값이 나오고, 관측 모드에서는 「지금」이 마지막으로 들은 발생 시각이다. 없으면 소비처는 **재지 않는다**.
126
+
82
127
  ## State 이원 모델
128
+
83
129
  - **EPCIS 저널**(이산): 재고·위치·조립·변환. `epcis.ts` 로 정규 방출, `validateEpcisEvent` 검증.
84
- - **운영·키네마틱**(연속): 무버 motion(from/to/progress)·태스크·오더 진척. 운영 델타로 미러.
85
- - **`ObservedReducer`** 가 둘을 접어 재구성 → sim 이벤트든 live 이벤트든 같은 State(데이터원 스왑). `StateProjector` 는 같은 것의 옛 이름(별칭 유지).
130
+ - **운영·키네마틱**(연속): 설비 motion(from/to/progress)·태스크·오더 진척. 운영 델타로 미러.
131
+ - **`ObservedReducer`** 가 둘을 접어 재구성 → sim 이벤트든 live 이벤트든 같은 State(데이터원 스왑).
86
132
 
87
133
  ### 구동은 둘, 규칙은 하나 — 적합성 하네스
88
134
 
89
- 상태를 만드는 구동이 둘이다(시뮬 `tick()` / 관측 `apply()`). 각자 계약으로 옮기는 코드가 두 벌이면 한쪽만 고쳤을 때 **조용히 갈라진다** — 계약 필드가 거의 다 선택이라 "안 채우는 것이 합법"이고, 컴파일러가 잡아 주지 않는다.
135
+ 상태를 만드는 구동이 둘이다(시뮬 `tick()` / 관측 `apply()`). 각자 계약으로 옮기는 코드가 두 벌이면 한쪽만 고쳤을 때 **조용히 어긋난다** — 계약 필드가 거의 다 선택이라 "안 채우는 것이 합법"이고, 컴파일러가 잡아 주지 않는다.
136
+
137
+ > **불변식: 커널 상태의 모든 사실은 이벤트로 나가야 한다 (상태 ⊆ 이벤트).**
90
138
 
91
- > **불변식: 커널 상태의 모든 사실은 이벤트로 나가야 한다.**
139
+ 나가지 않는 사실은 미러가 모르고, 저널로 복원되지도 않고(시간여행), 미러에서 세운 예측 씨앗에도 실리지 않는다. `test/driver-conformance.test.ts` 가 시뮬을 굴려 **그 이벤트를 그대로 미러에 흘리고** 두 스냅샷을 대조한다. 정당한 예외는 주석이 아니라 **상수로** 들고 이유를 적는다(호스트가 적분하는 `oee`, 매 tick 가지 않는 보간값 — 대신 **앵커는 반드시 같다**).
92
140
 
93
- 나가지 않는 사실은 미러가 모르고, 저널로 복원되지도 않고(시간여행), 미러에서 세운 예측 씨앗에도 실리지 않는다. `test/driver-conformance.test.ts` 가 시뮬을 굴려 **그 이벤트를 그대로 미러에 흘리고** 두 스냅샷을 대조한다(WMS·YMS·MES). 정당한 예외는 주석이 아니라 **상수로** 들고 이유를 적는다(호스트가 적분하는 `oee`, 매 tick 가지 않는 보간값 — 대신 **앵커는 반드시 같다**).
141
+ ## 재기동 — 재개점에서 이어 접는다
142
+
143
+ 저널을 처음부터 다시 접지 않기 위한 축이다. 실 저널이 수천만 줄인 현장에서 이것은 성능이 아니라 **가능/불가능**의 문제다.
144
+
145
+ ```ts
146
+ observedCheckpoint(): ReducerCheckpoint | undefined // 재개점을 꺼낸다(관측 구동이 아니면 undefined)
147
+ restoreObserved(cp: ReducerCheckpoint): void // 재개점에서 되세운다
148
+ replayFrom(model, checkpoint, events) // 재개점 + 꼬리만 접는다
149
+ ```
94
150
 
95
- ## 트윈 본연 — 현재로부터 예측 + 정합 (mirror+sim 과 구별짓는 기능)
96
- 관측·예측·행위가 따로 있는 건 mirror+simulator. 트윈의 본질은 그 **커플링**:
97
- - **`kernel.fork()`** — 현재 상태(in-flight 포함)를 정확히 복제한 격리 엔진. 원본(live/sim)은 계속 가고, fork 는 **현재로부터 앞으로 굴려** forecast(완료 시각·처리량)·대안 what-if 탐색.
98
- - **`compareStates(predicted, actual)`** — fork 예측 vs 실제 관측을 같은 시점에 대조 → **드리프트(모델↔현실 이탈) 탐지·국소화**(item/node/order 필드별). 발산 = 이상/개입 신호.
99
- - 루프: 관측(현재) → fork 예측(미래) → 관측(실제) → 정합(발산) → 행위(개입).
100
- - **`EventJournal` + `replay(board, events)`** — 트윈의 **기억**: 이벤트열이 곧 상태(이벤트-소싱). `journal.until(revision)`/`untilSimTime(iso)` 을 projector 로 재생 → **임의 과거 시점 상태 재구성(시간여행)**. 감사·리플레이·발산 비교의 기반. things-factory 에선 append 를 DB 이벤트 테이블로 교체.
151
+ > **상태 스냅샷은 씨앗이 되지 못한다.** 리듀서는 소비처가 보는 값 말고도 든다 — 부모를 기다리는 담김·집계 중인 수량·담을 자리를 몰라 세어 둔 사건. 상태만 되돌리고 뒤를 이어 접으면 **0부터 접은 결과와 조용히 달라진다.**
101
152
 
102
- ### 본질 심화
103
- - **fork = 완전한 결정적 분기** — 상태 + 시나리오(gens) + **RNG state** 까지 복제 → 미래 생성까지 원본과 동일하게 이어가는 진짜 continuation(what-if 는 fork 후 scenario 교체).
104
- - **`TwinObserver`** — 자동 정합 루프: 주기적으로 fork-예측 후 실제가 그 시점 도달 시 대조 → **드리프트를 알림 이벤트로**. 트윈이 자기 모델↔현실 이탈을 능동 감지(방해 없으면 발산 0).
105
- - **`monteCarloForecast`** — 확률적 예측: seed 변주 N개 fork → 지표를 **분포**로(min/mean/p50/p90/max). "언제 끝나?"가 아니라 "P90 완료시각·소진 확률". 원본 무간섭.
106
- - **`TwinHistory` + `counterfactualAt`** — 반사실(기억+분기): 주기 checkpoint(전체 커널 fork 저장) → 과거 시점 T로 되돌아가(사이 시점은 결정적 재구동) **대안 결정을 fork** → 앞으로 굴려 baseline 과 대조 → **"그때 X 했다면?"의 효과**. 사후분석·의사결정 평가.
153
+ 그 동치(`재개점 + 꼬리 == 0부터 접기`)는 `test/observed-checkpoint.test.ts` 가 증명한다. 배경·시행착오·호스트 배선은 [`design/fold-and-resume.md`](../../design/fold-and-resume.md).
154
+
155
+ 원본이 **되풀어 주지 않는 축**은 따로 이어받는다(`hydrateContinuity`) — 열린 수요 구간의 누적·적산 기준점·관측 이후 최대·신호가 언제부터인지. 관측 축(재고·자리·설비)은 그 문으로 심지 않는다.
156
+
157
+ ## 주의 신호 — 커널이 판정하고, 문장은 만들지 않는다
158
+
159
+ `attentions` 는 커널이 스스로 내는 판정이다(`deriveAttentions`/`collectAttentions`). 각 신호는 `kind` · `severity` · `anchor`(어디의 일인가) · `params`(근거가 된 값)를 든다.
160
+
161
+ **문장을 만들지 않는다.** 커널은 키와 값만 내고 표현은 소비처가 한다 — 커널이 한국어 문장을 들면 그 트윈은 한 언어에 묶인다. 그리고 판정에는 **근거를 함께 싣는다**: 한계를 넘은 관측 신호는 값·단위·한계·**언제의 값인지**·잰 것인지 접은 것인지를 모두 낸다. 센서가 멈춘 현장에서 그 시각이 없으면 사람은 방금 벗어난 것으로 읽는다.
162
+
163
+ 판정할 수 없으면 **아무 말도 하지 않는다** — 없는 이탈을 만들지도, 확인되지 않은 합격을 만들지도 않는다.
164
+
165
+ ## 트윈 본연 — 현재로부터 예측 + 정합
166
+
167
+ 관측·예측·행위가 따로 있는 것은 mirror+simulator 다. 트윈의 본질은 그 **커플링**:
168
+
169
+ - **`fork()`** — 현재 상태(in-flight 포함)를 정확히 복제한 격리 엔진. 원본(live/sim)은 계속 진행하고, fork 는 **현재로부터 앞으로** 굴려 완료 시각·처리량을 예측하고 what-if 를 탐색한다. 상태 + 시나리오 + **RNG state** 까지 복제하는 진짜 continuation.
170
+ - **`compareStates(predicted, actual)`** — 같은 시점에 대조해 드리프트(모델↔현실 이탈)를 국소화한다(item/location/order 필드별). 발산 = 이상·개입 신호.
171
+ - **`monteCarloForecast`** — seed 변주 N개 fork → 지표를 **분포**로(min/mean/p50/p90/max). 「언제 끝나나」가 아니라 「P90 완료시각·소진 확률」.
172
+ - **`EventJournal` + `replay`/`replayFrom`** — 트윈의 기억. `until(revision)`/`untilSimTime(iso)` 로 **임의 과거 시점 재구성**(시간여행). 호스트에서는 append 를 DB 이벤트 표로 교체한다.
173
+ - **`TwinHistory` + `counterfactualAt`** — 과거 시점 T 로 돌아가 **대안 결정을 fork** → baseline 과 대조. 「그때 X 했다면」의 효과.
174
+ - **`TwinObserver`** — 주기적으로 fork-예측 후 실제가 그 시점에 도달하면 대조 → 드리프트를 알림 이벤트로.
175
+
176
+ 예측은 **자기 한계를 함께 말한다**: 계보를 잇지 못한 구간(`stepsWithoutMaterial`)과 원본이 개체를 말하지 않아 막힌 오더(`blocked-source-omits-material`)를 결과에 싣는다. 조용히 넘기면 계보가 빈 예측이 정상처럼 보인다.
107
177
 
108
178
  ## 확장 지점
179
+
109
180
  - **버티컬 추가**: `{x}-profile.ts`(어휘) + `{x}-kernel.ts`(4 hook).
181
+ - **능력(capability)으로 찾기**: concrete 타입에 강결합하지 않는다 — 자원이 능력을 선언하고 공정이 능력을 요구한다.
110
182
  - **할당 정책**: `AllocationPolicy` 교체(firstFit/partialFit 내장, FEFO/nearest/zone 추가 가능).
183
+ - **소요시간 추정기**: 실측 기반 추정기를 호스트가 주입.
111
184
  - **Face2 어댑터**: 실 시스템 페이로드 → 선언적 매핑 → 정규 EPCIS.
112
185
  - **host 결합**: `TwinRuntime` 를 GraphQL sub·서비스로 래핑(전송은 얇은 host 계층).
113
186
 
114
187
  ## 미구현(범위 밖)
115
- host 결합(things-factory 전송/퍼시스턴스/커넥터) · 보드 바인딩(컴포넌트↔SGLN) · 도메인 깊이(WMS 멀티라인, YMS 상하차 AggregationEvent, MES BOM).
116
188
 
117
- **모델의 남은 공백**(정직하게): 인원 자격·숙련도(등급만 있고 자격 매칭·숙련도별 소요 차이 없음) · 자산 회수 작업(빈 팔레트가 도착 자리에 남을 뿐) · `movers`/`nodes` 계층 겸직(이동설비↔워크센터, 위치↔워크센터) · next-event 도약(지금은 고정 간격 tick).
189
+ host 결합(전송·퍼시스턴스·커넥터) · 보드 바인딩(컴포넌트↔SGLN) · 도메인 깊이(WMS 멀티라인, YMS 상하차 AggregationEvent, MES BOM).
190
+
191
+ **모델의 남은 공백**(정직하게):
192
+
193
+ - 인원 **숙련도** — 등급과 자격 시험은 있으나 숙련도별 소요 차이는 없다
194
+ - 자산 **회수 작업** — 빈 팔레트가 도착 자리에 남을 뿐
195
+ - `equipment`/`locations` **계층 겸직** — 한 목록이 이동설비와 워크센터를, 다른 목록이 주소 가능한 자리와 작업 자리를 함께 든다(개명으로 이름은 정리됐고 계층은 그대로다)
196
+ - **next-event 도약** — 지금은 고정 간격 tick
197
+ - 라이브 **예측** — 미러 상태에서 fork 는 되지만 라이브 자체의 예측 경로는 통합 보류
198
+ - 구조가 **여러 번 갈린** 저널의 마디별 재개점 — 지금은 최신 하나
199
+
200
+ **1.0 게이트**: `equipment`/`locations` 계층을 정리하거나 가산적으로만 가능하도록 확정한 뒤. 계약 파괴가 예정된 채로 1.0 을 주면 다음 라운드가 곧 2.0 이 된다.
201
+
202
+ ## 설계 SoT
118
203
 
119
- **1.0 게이트**: `movers`/`nodes` 계층을 정리하거나 가산적으로만 가능하도록 확정한 뒤. 계약 파괴가 예정된 채로 1.0 을 주면 다음 라운드가 곧 2.0 이 된다.
204
+ `operato-twin/design/` —
120
205
 
121
- 설계 SoT: `operato-twin/design/plans/` — `simulation-spec.md`(명세·추정기·예측 자격) · `four-resources-and-conformance.md`(4대 자원·불변식) · `kernel-unification-live-observe.md`(구동 통합).
206
+ - [`fold-and-resume.md`](../../design/fold-and-resume.md) — 저널 접기·재개점(시행착오·원칙·현 구현)
207
+ - [`runtime-state-model.md`](../../design/runtime-state-model.md) — 라이브=권위 / 히스토리=파생, 시각축, 정체성
208
+ - [`04-decisions.md`](../../design/04-decisions.md) — ADR
209
+ - `plans/simulation-spec.md`(명세·추정기·예측 자격) · `plans/four-resources-and-conformance.md`(4대 자원·불변식) · `plans/kernel-unification-live-observe.md`(구동 통합)
210
+ - `profiles/ems.md` — 에너지 프로파일의 표준 앵커
@@ -315,8 +315,25 @@ export interface TestResult {
315
315
  /**
316
316
  * 합격 여부 — **우리가 좁힌 것이다.** 표준 `EvaluatedCriterionResult` 는 `TextType` 이고
317
317
  * 판정 어휘를 열거하지 않는다(원문 대조 2026-08-24). 넓힐 때는 이 주석과 함께 움직인다.
318
+ *
319
+ * ── 왜 선택인가 (2026-08-24) ──────────────────────────────────────────────
320
+ * **재기만 하고 판정하지 않는 원천이 정상이다.** 계측기는 값을 내고 판정은 규정이 한다. 그런데 이
321
+ * 필드가 필수이면 그런 원천의 사실을 **아예 담을 수 없다** — 판정을 지어내거나 측정값을 버리거나
322
+ * 둘 중 하나가 되고, 둘 다 이 저장소가 거절하는 것이다(결측을 값으로 바꾸지 않는다).
323
+ *
324
+ * 비어 있는 것은 **「모른다」이며 「합격」이 아니다.** `testPassedAt` 이 그것을 지킨다(거짓을 낸다).
325
+ * 기준이 선언돼 있으면 커널이 채울 수 있다(`judgeAgainstSpec`) — 그때는 `derived` 가 선다.
326
+ */
327
+ result?: 'pass' | 'fail';
328
+ /**
329
+ * 이 판정을 **우리가 냈나** — 원천이 판정하지 않아 선언된 기준으로 커널이 채운 것.
330
+ *
331
+ * 관측의 `derived` 와 같은 낱말·같은 뜻이다(낱말을 둘로 만들지 않는다). 규제 기록에서 「현장이
332
+ * 판정했다」와 「트윈이 계산했다」가 구별되지 않으면, 그 기록은 사고 뒤에 쓸 수 없다.
333
+ *
334
+ * 원천의 판정에는 표식이 없다 — **없음이 「원천이 말했다」다.**
318
335
  */
319
- result: 'pass' | 'fail';
336
+ derived?: boolean;
320
337
  /** 언제 통과·불합격했나(ISO) — 표준 `EvaluationDate`. */
321
338
  at?: ISOTime;
322
339
  /**
@@ -339,6 +356,16 @@ export interface TestResult {
339
356
  */
340
357
  propertyMeasurements?: PropertyMeasurement[];
341
358
  }
359
+ /**
360
+ * `OP_EVENT.test` 의 페이로드 — **결과가 대상을 가리킨다.**
361
+ *
362
+ * 표준(`TestResultType`)의 방향 그대로다. 상태에서는 개체 안에 접어 두지만(소비처가 대상별로 묻는다)
363
+ * 사건에서는 가리킨다 — 그래야 대상이 무엇이든(로트·설비·사람·자리) 한 채널로 들어온다.
364
+ */
365
+ export interface TestResultFact extends TestResult {
366
+ /** 무엇을 시험했나 — 표준 `TestResult.TestableObjectID`. 물품이면 EPC. */
367
+ testableObjectId: string;
368
+ }
342
369
  /**
343
370
  * 이 시험 결과가 **이 시각에 유효한 합격인가.**
344
371
  *
@@ -439,9 +466,9 @@ export interface TestSpecificationCriterion {
439
466
  * 이 저장소의 규율이고(§`TestResult.result` 의 좁힘과 같은 자리), 그러면 다음 사람이 이것을
440
467
  * 「표준이 준 이름」으로 읽지 않는다.
441
468
  *
442
- * 실 원본이 이 모양으로 준다는 것이 확인됐다(첫 실 연동: `criticalLimits.{minimum,maximum}`).
443
- * **그것이 이 축을 정한 것은 아니다** — 판정이 필요하다는 당위가 정했고, 원본은 그것을 채울 수
444
- * 있다는 사실을 확인해 준 것이다.
469
+ * 이 모양으로 한계를 주는 실 원본이 있다는 것도 확인했다. **그것이 이 축을 정한 것은 아니다** —
470
+ * 판정이 필요하다는 당위가 정했고, 원본은 그것을 채울 수 있다는 사실을 확인해 준 것이다.
471
+ * 어느 배포가 무엇을 주는지는 **커널이 알 일이 아니므로 여기 적지 않는다**(연동 쪽 문서의 몫이다).
445
472
  */
446
473
  limit?: {
447
474
  /** 이 값 미만이면 벗어난다. 없으면 아래쪽 한계가 없다(모르는 것이 아니라 없는 것이다). */
@@ -547,6 +574,32 @@ export declare function testEvidenceGaps(spec: Pick<TestSpecification, 'criteria
547
574
  unmeasured: string[];
548
575
  unmatched: string[];
549
576
  };
577
+ /**
578
+ * **선언된 기준으로 판정한다** — 원천이 판정하지 않았을 때 커널이 답을 낼 수 있는가.
579
+ *
580
+ * ── 왜 이 함수가 있나 (당위) ────────────────────────────────────────────────
581
+ * 트윈은 **아무도 판정하지 않을 때 판정해야 한다.** 값이 있고 기준이 있는데 판정이 없으면, 화면은
582
+ * 아무 말도 하지 않고 그것은 「전부 이상 없음」과 **화면상 똑같이 보인다.** 그 침묵이 이 함수가
583
+ * 없앤 것이다.
584
+ *
585
+ * ── 세 갈래로 답한다 (이 함수의 전부) ───────────────────────────────────────
586
+ *
587
+ * 'fail' 기준 하나라도 **분명히** 벗어났다
588
+ * 'pass' **말하는 기준 전부**를 판정했고 전부 안에 있다
589
+ * undefined 판정할 수 없다 — 하나라도 판정 못 한 기준이 있거나, 판정할 기준이 아예 없다
590
+ *
591
+ * `'pass'` 가 「말하는 기준 **전부**」를 요구하는 것이 이 함수의 핵심이다. 하나라도 판정하지 못했는데
592
+ * 합격이라 하면 **확인되지 않은 합격**이 되고, 규제 기록에서 그것은 결함이 아니라 사고다. 그래서
593
+ * 모르는 것이 하나라도 있으면 아무 말도 하지 않는다.
594
+ *
595
+ * 아무 한계도 말하지 않는 기준(`criterionSaysNothing`)은 **셈에서 뺀다** — 자유 문장만 적힌 기준
596
+ * 때문에 판정 가능한 것까지 침묵하면, 이 축이 그 현장에서 영원히 죽는다(고치려던 것의 반대 방향).
597
+ * 그 기준이 비어 있다는 사실은 `testEvidenceGaps` 와 `criterionSaysNothing` 이 따로 낸다 —
598
+ * **설정의 흠과 운영의 사실을 한 답에 섞지 않는다.**
599
+ *
600
+ * 커널은 `Expression` 을 **읽지 않는다**(문법이 정의되지 않은 자유 문자열). 판정은 숫자 한계로만 한다.
601
+ */
602
+ export declare function judgeAgainstSpec(spec: Pick<TestSpecification, 'criteria'> | undefined, result: Pick<TestResult, 'propertyMeasurements'> | undefined): 'pass' | 'fail' | undefined;
550
603
  /**
551
604
  * 유효 기간 — **ISA-95 `EffectiveStartDate` / `EffectiveEndDate`.**
552
605
  *
@@ -1151,6 +1204,39 @@ export interface ItemState {
1151
1204
  lot?: string;
1152
1205
  location: string;
1153
1206
  disposition?: string;
1207
+ /**
1208
+ * 이 로트의 **시험 결과** — 명세당 최신 하나.
1209
+ *
1210
+ * ── 로트에 「어느 기준이 걸리나」는 두지 않았다 ────────────────────────────
1211
+ * 표준 `MaterialLotType` 에는 `TestSpecificationID` 가 있다. 우리는 그것을 **로트에 두지 않는다** —
1212
+ * 이미 `TwinModelDef.materialDefinitions[].testSpecificationIds` 가 「이 품목에 무엇이 걸리나」를
1213
+ * 말하고, 물품은 `gtinKey` 로 그 선언에 닿는다. 로트마다 또 적으면 두 벌이 되고, 두 벌은 언젠가
1214
+ * 어긋난다.
1215
+ *
1216
+ * 그리고 **채우는 곳이 없는 칸은 만들지 않는다.** 로트별로 다른 기준을 말하는 원천을 만나면 그때
1217
+ * 열고, 그때는 채우는 코드와 함께 온다 — 선언만 있는 능력은 소비처에게 거짓말이 된다.
1218
+ *
1219
+ * ── 당위 (2026-08-24) ────────────────────────────────────────────────────
1220
+ * 트윈은 「이 자재를 쓸 수 있나」에 답해야 한다. 그 답의 **근거가 측정값**이다. 판정만 들고 값을
1221
+ * 버리면 「합격이라고 적혀 있다」만 남고 **왜 합격인지 되짚을 수 없다** — 규제 기록은 성질상 사고
1222
+ * 뒤에 읽히므로 그때 아무 일도 하지 못한다. 그리고 이 기록을 이어 갈 수 있는 것은 트윈뿐이다:
1223
+ * 로트는 시스템을 넘어 다니고, 판정한 시스템은 그 뒤를 모른다.
1224
+ *
1225
+ * ── 표준에서 왜 여기 있나 (원문 대조) ─────────────────────────────────────
1226
+ * `MaterialLotType`(B2MML-Material.xsd)이 드는 것은 `TestSpecificationID`(어느 기준)와
1227
+ * `Disposition`(판정)이고, **시험 결과는 로트 안에 없다.** 결과는 `TestResultType` 이라는 별개
1228
+ * 기록이고 `TestableObjectID` 로 **대상을 가리킨다**(B2MML-OperationsTest.xsd).
1229
+ *
1230
+ * **그것이 우리가 좁힌 자리다**: 우리 상태는 대상별 투영이라(소비처가 「이 물품은?」을 묻는다) 가리키는
1231
+ * 기록을 개체 안에 접어 둔다. 같은 사실이고 방향만 다르다 — 사건에서는 표준 그대로 대상을 가리킨다
1232
+ * (§`OP_EVENT.test`). 자원에서 이미 같은 좁힘을 했다(`EquipmentState.testResults`).
1233
+ *
1234
+ * ── 명세당 하나인 이유 ────────────────────────────────────────────────────
1235
+ * 이력을 들면 상태가 **계측 주기로 자란다**(품목 100만 기준에서는 그것이 곧 벽이다). 「그때 무엇을
1236
+ * 쟀나」는 저널이 답하는 물음이고, 상태가 답하는 것은 「지금 이 로트의 자격이 무엇이냐」다.
1237
+ * 자리의 관측을 속성당 하나만 든 것과 같은 규율이다(§`LocationState.observations`).
1238
+ */
1239
+ testResults?: TestResult[];
1154
1240
  /** 소속 물류단위(팔레트 SSCC 등) — AggregationEvent 로 맺어진다. 3D 적재 표현의 재료. */
1155
1241
  parent?: string;
1156
1242
  /**
@@ -2062,6 +2148,19 @@ export declare const OP_EVENT: {
2062
2148
  readonly asset: "asset.status";
2063
2149
  readonly order: "order.status";
2064
2150
  readonly quality: "quality.output";
2151
+ /**
2152
+ * **시험 결과** — 어느 대상을 어느 기준으로 재고 판정했나.
2153
+ *
2154
+ * `quality.output` 과 **다른 사실이다.** 그것은 생산의 양품·불량 수(가동률 입력)이고, 이것은 선언된
2155
+ * 기준에 대한 검사 판정이다. 한 낱말이 두 일을 하면 어느 쪽 어휘도 옳지 않게 된다.
2156
+ *
2157
+ * **대상을 가리킨다**(`testableObjectId`) — 표준 `TestResult.TestableObjectID` 그대로. 로트·설비·
2158
+ * 사람·자리에 두루 쓰이므로 주체를 이름에 넣지 않았다.
2159
+ *
2160
+ * 이 채널이 없으면 시험 결과는 **상태에만 있는 축**이 된다 — 재기동에서 사라지고, 폴드가 되살릴 수
2161
+ * 없고, 미러가 이어받지 못한다(§상태 ⊆ 이벤트).
2162
+ */
2163
+ readonly test: "test.result";
2065
2164
  /**
2066
2165
  * 주목 신호 확인(ack) — **사람이 한 행위**라 파생될 수 없다.
2067
2166
  *
@@ -2070,6 +2169,18 @@ export declare const OP_EVENT: {
2070
2169
  * 과거를 다시 계산해도 그때 무엇을 확인했는지 알 수 없다 — 저널이 현실을 불완전하게 담는 자리였다.
2071
2170
  */
2072
2171
  readonly attentionAck: "attention.acked";
2172
+ /**
2173
+ * **자리에서 관측된 물리량** — 냉장실 온도·습도, 세척수 유량 같은 것(§`LocationObservation`).
2174
+ *
2175
+ * ── 왜 채널이 필요한가 ────────────────────────────────────────────────────
2176
+ * `LocationState.observations` 를 상태에 두었는데 그것을 낳는 사건이 없었다. 그러면 **상태 ⊆ 이벤트**
2177
+ * 가 깨진다: 채워도 재기동에서 사라지고, 폴드가 되살릴 수 없고, 미러가 이어받을 수도 없다. 상태에만
2178
+ * 있는 축은 조용히 사라지는 축이다.
2179
+ *
2180
+ * 이름을 `energy.measured` 와 같은 결로 둔다 — 그쪽이 에너지 계량의 도착이고 이쪽이 그 일반형이다.
2181
+ * 두 채널을 합치지 않는 이유는 에너지 쪽이 **구간에 누적되는 표본**이라 처리가 다르기 때문이다.
2182
+ */
2183
+ readonly observation: "location.measured";
2073
2184
  };
2074
2185
  /**
2075
2186
  * 에너지 트윈의 사건 — **물(物)의 계보가 아니라 스칼라의 시계열.**
@@ -2408,6 +2519,7 @@ export interface TwinModelDef {
2408
2519
  parallelism?: number;
2409
2520
  parentId?: string;
2410
2521
  properties?: ResourceProperty[];
2522
+ testSpecificationIds?: TestSpecificationRefs;
2411
2523
  }[];
2412
2524
  /**
2413
2525
  * 설비(설비). mtbfMs/mttrMs 지정 시 확률적 고장 모델 참여(OEE Availability 손실). 미지정=고장 없음.
package/dist/contract.js CHANGED
@@ -331,6 +331,51 @@ export function testEvidenceGaps(spec, result) {
331
331
  .filter((k) => !!k && !wanted.has(k))
332
332
  };
333
333
  }
334
+ /**
335
+ * **선언된 기준으로 판정한다** — 원천이 판정하지 않았을 때 커널이 답을 낼 수 있는가.
336
+ *
337
+ * ── 왜 이 함수가 있나 (당위) ────────────────────────────────────────────────
338
+ * 트윈은 **아무도 판정하지 않을 때 판정해야 한다.** 값이 있고 기준이 있는데 판정이 없으면, 화면은
339
+ * 아무 말도 하지 않고 그것은 「전부 이상 없음」과 **화면상 똑같이 보인다.** 그 침묵이 이 함수가
340
+ * 없앤 것이다.
341
+ *
342
+ * ── 세 갈래로 답한다 (이 함수의 전부) ───────────────────────────────────────
343
+ *
344
+ * 'fail' 기준 하나라도 **분명히** 벗어났다
345
+ * 'pass' **말하는 기준 전부**를 판정했고 전부 안에 있다
346
+ * undefined 판정할 수 없다 — 하나라도 판정 못 한 기준이 있거나, 판정할 기준이 아예 없다
347
+ *
348
+ * `'pass'` 가 「말하는 기준 **전부**」를 요구하는 것이 이 함수의 핵심이다. 하나라도 판정하지 못했는데
349
+ * 합격이라 하면 **확인되지 않은 합격**이 되고, 규제 기록에서 그것은 결함이 아니라 사고다. 그래서
350
+ * 모르는 것이 하나라도 있으면 아무 말도 하지 않는다.
351
+ *
352
+ * 아무 한계도 말하지 않는 기준(`criterionSaysNothing`)은 **셈에서 뺀다** — 자유 문장만 적힌 기준
353
+ * 때문에 판정 가능한 것까지 침묵하면, 이 축이 그 현장에서 영원히 죽는다(고치려던 것의 반대 방향).
354
+ * 그 기준이 비어 있다는 사실은 `testEvidenceGaps` 와 `criterionSaysNothing` 이 따로 낸다 —
355
+ * **설정의 흠과 운영의 사실을 한 답에 섞지 않는다.**
356
+ *
357
+ * 커널은 `Expression` 을 **읽지 않는다**(문법이 정의되지 않은 자유 문자열). 판정은 숫자 한계로만 한다.
358
+ */
359
+ export function judgeAgainstSpec(spec, result) {
360
+ /* 판정할 기준만 남긴다 — 말하지 않는 기준은 이 답에 관여하지 않는다. */
361
+ const criteria = (spec?.criteria ?? []).filter(c => !criterionSaysNothing(c));
362
+ if (!criteria.length)
363
+ return undefined;
364
+ const measurements = result?.propertyMeasurements ?? [];
365
+ let allJudged = true;
366
+ for (const c of criteria) {
367
+ /* 그 기준이 재는 속성의 측정값 — 속성을 말하지 않은 기준은 짝을 맞출 수 없다. */
368
+ const m = c.evaluatedPropertyId
369
+ ? measurements.find(x => x.testableObjectPropertyId === c.evaluatedPropertyId)
370
+ : undefined;
371
+ const out = outsideLimit(c, m);
372
+ if (out === true)
373
+ return 'fail';
374
+ if (out === undefined)
375
+ allJudged = false;
376
+ }
377
+ return allJudged ? 'pass' : undefined;
378
+ }
334
379
  /**
335
380
  * 이 시각에 유효 기간 밖인가 — **한 규칙**으로 개체·등급·설비↔자산 매핑을 모두 판정한다.
336
381
  *
@@ -886,6 +931,19 @@ export const OP_EVENT = {
886
931
  asset: 'asset.status',
887
932
  order: 'order.status',
888
933
  quality: 'quality.output', // 품질 산출(양품/불량) — OEE quality 입력. live 누적기가 이걸로 good/scrap 정확 추적.
934
+ /**
935
+ * **시험 결과** — 어느 대상을 어느 기준으로 재고 판정했나.
936
+ *
937
+ * `quality.output` 과 **다른 사실이다.** 그것은 생산의 양품·불량 수(가동률 입력)이고, 이것은 선언된
938
+ * 기준에 대한 검사 판정이다. 한 낱말이 두 일을 하면 어느 쪽 어휘도 옳지 않게 된다.
939
+ *
940
+ * **대상을 가리킨다**(`testableObjectId`) — 표준 `TestResult.TestableObjectID` 그대로. 로트·설비·
941
+ * 사람·자리에 두루 쓰이므로 주체를 이름에 넣지 않았다.
942
+ *
943
+ * 이 채널이 없으면 시험 결과는 **상태에만 있는 축**이 된다 — 재기동에서 사라지고, 폴드가 되살릴 수
944
+ * 없고, 미러가 이어받지 못한다(§상태 ⊆ 이벤트).
945
+ */
946
+ test: 'test.result',
889
947
  /**
890
948
  * 주목 신호 확인(ack) — **사람이 한 행위**라 파생될 수 없다.
891
949
  *
@@ -898,7 +956,19 @@ export const OP_EVENT = {
898
956
  * "확인해라"(요청)이고 이벤트는 "확인했다"(사실)다. 같은 문자열을 쓰면 저널에서 요청과 사실이
899
957
  * 구별되지 않는다.
900
958
  */
901
- attentionAck: 'attention.acked'
959
+ attentionAck: 'attention.acked',
960
+ /**
961
+ * **자리에서 관측된 물리량** — 냉장실 온도·습도, 세척수 유량 같은 것(§`LocationObservation`).
962
+ *
963
+ * ── 왜 채널이 필요한가 ────────────────────────────────────────────────────
964
+ * `LocationState.observations` 를 상태에 두었는데 그것을 낳는 사건이 없었다. 그러면 **상태 ⊆ 이벤트**
965
+ * 가 깨진다: 채워도 재기동에서 사라지고, 폴드가 되살릴 수 없고, 미러가 이어받을 수도 없다. 상태에만
966
+ * 있는 축은 조용히 사라지는 축이다.
967
+ *
968
+ * 이름을 `energy.measured` 와 같은 결로 둔다 — 그쪽이 에너지 계량의 도착이고 이쪽이 그 일반형이다.
969
+ * 두 채널을 합치지 않는 이유는 에너지 쪽이 **구간에 누적되는 표본**이라 처리가 다르기 때문이다.
970
+ */
971
+ observation: 'location.measured'
902
972
  };
903
973
  // ── 에너지(EMS) 사건 — 네 번째 종류의 어휘 ────────────────────────────────
904
974
  /**
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';