@operato/twin-kernel 0.7.61 → 0.7.63

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,210 +1,226 @@
1
- # @operato/twin-kernel — 비즈니스레이어 디지털트윈 커널
1
+ # @operato/twin-kernel — a business-layer digital twin kernel
2
2
 
3
- 물류창고·야드·스마트팩토리·에너지의 **비즈니스 실행**을 시뮬레이션·모니터링하는 **헤드리스·프레임워크 무관·zero-dep** TS 커널. UI/3D/DOM 없이 순수 Node 에서 동작하는 것이 목적(sim=능동 생산, live=수동 미러, **계약 동일**).
3
+ A **headless, framework-agnostic, zero-dependency** TypeScript kernel that simulates and mirrors the **business execution** of warehouses, yards, factories and energy systems. It runs in plain Node with no UI, no 3D and no DOM (`sim` = it generates events, `live` = it mirrors them — **same contract either way**).
4
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대 자원
5
+ - Run: `node --test test/*.test.ts` — native TS, **zero runtime dependencies**
6
+ - Status: **v0.7.61 · 1,131 tests green** · 122 test files · 4 verticals (WMS/YMS/MES/EMS) · ISA-95 four resources
7
7
 
8
- ## 계층 (아래로만 의존, 무방언)
8
+ ## Layers (dependencies point down only, no dialect)
9
9
 
10
10
  ```
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 확장
11
+ contract.ts 3-channel contract (State/Command/Scenario) + operational deltas + TwinKernel ← domain/standard agnostic
12
+ epcis.ts GS1 EPCIS 2.0 event machinery (types · builders · validator · URIs · CBV) ← standard, domain agnostic
13
+ vocabulary.ts retired-name list + guard pragma — renames are enforced by machine
14
+ flow-engine.ts FlowEngine base — mechanics (RNG · clock · tick · resource claim · task progress · emit · snapshot)
15
+ observed-reducer.ts events → State fold (**the single rule both drivers use**) + resume points ← domain agnostic
16
+ state-projector.ts the old name for the same thing (kept for consumers)
17
+
18
+ allocation-policy.ts AllocationPolicy seam (selectPlacement/selectStock)
19
+ capability.ts what a resource can do — find by capability, never by concrete type
20
+ capacity.ts capacity and occupancy
21
+ operations-capability.ts what an operation requires ↔ what a resource offers
22
+ domain-definition.ts operation specs (ISA-95 OperationsSegment: duration · variability · yield · resource needs)
23
+ domain-catalog.ts the catalogue of what can be declared, plus the declared relations between axes
24
+ duration-estimator.ts seam for measured estimators injected by the host
25
+ iso-duration.ts xsd:duration parser (calendar-dependent units are rejected)
26
+ make-to-order.ts make-to-order production
27
+ task-fold.ts task folding
28
+ job-response.ts ISA-95 job response
29
+
30
+ event-journal.ts the journal + replay/replayFrom — the twin's memory
31
+ forecast.ts Monte-Carlo forecast (answers with a distribution)
32
+ divergence.ts predicted vs observed → drift, localised
33
+ counterfactual.ts branch from a past instant — "what if we had done X"
34
+ twin-observer.ts the automatic reconciliation loop
35
+
36
+ energy-ingest.ts metering intake (totalisers, demand windows)
37
+ energy-attribution.ts attributing energy to consumers
38
+ operational-ingest.ts operational-fact intake, and the routing that decides which fact a record is
39
+ scenario-validate.ts validating a declared scenario
40
+ face2-adapter.ts legacy records → canonical EPCIS (declarative mapping + validation) ← ACL
41
+ runtime.ts TwinRuntime — host-facing facade + subscription protocol ← domain/transport agnostic
42
+
43
+ {wms,yms,mes,ems}-profile.ts domain vocabulary only (bizStep / btt / standard anchors)
44
+ kernel.ts · {yms,mes,ems}-kernel.ts domain flow verbs (4 hooks) — extend FlowEngine
45
45
  ```
46
46
 
47
- > **무방언 원칙**: 코어(contract/epcis/flow-engine/policy/reducer/runtime)에 특정 도메인 용어를 넣지 않는다. EPCIS 는 표준이라 `epcis.ts`(≠wms). `order.kind`·`task.resourceType` 등은 도메인이 소유. 정책 인터페이스는 `selectPlacement`/`selectStock`(≠putaway/pallets).
47
+ > **No dialect in the core.** `contract` / `epcis` / `flow-engine` / `policy` / `reducer` / `runtime` carry no domain words. EPCIS lives in `epcis.ts` because it is a standard, not because it is WMS. `order.kind` and `task.resourceType` belong to the domain. Policy interfaces are `selectPlacement` / `selectStock`, never `putaway` / `pallets`.
48
48
 
49
- ## FlowEngine — 단일 base, 도메인은 4 hook 만
49
+ ## FlowEngine — one base, four hooks per domain
50
50
 
51
- `abstract FlowEngine implements TwinKernel` 이 모든 mechanics 를 소유. 도메인 kernel 은 **flow 동사**만 구현:
51
+ `abstract FlowEngine implements TwinKernel` owns all the mechanics. A domain kernel implements only the **flow verbs**:
52
52
 
53
- | hook | 역할 |
53
+ | hook | what it means |
54
54
  |---|---|
55
- | `onArrival(spec)` | 입고/도착 자극 → 아이템 생성 + EPCIS + 반입 태스크 |
56
- | `onOrder(spec)` | 오더/작업지시 자극 → 오더 생성 |
57
- | `allocate(order)` | created 오더 할당 → 재고 선택(정책) + 태스크. **시간창 게이트도 여기**(스케줄링) |
58
- | `onTaskComplete(task)` | 완료의 의미 — 이동(WMS/YMS) 또는 변환(MES). occupancy·EPCIS·오더 이행 전부 도메인 |
55
+ | `onArrival(spec)` | inbound stimulus → create items + EPCIS + an inbound task |
56
+ | `onOrder(spec)` | order stimulus → create the order |
57
+ | `allocate(order)` | a `created` order → pick stock (policy) + create tasks. **Time-window gating lives here too** |
58
+ | `onTaskComplete(task)` | what completion *means* — a move (WMS/YMS) or a transformation (MES). Occupancy, EPCIS and order fulfilment are the domain's |
59
59
 
60
- base 는 태스크 **생명주기**(자원 배정·진행·완료·해제·델타)만 소유 → 이동-중립. 새 버티컬 = 프로파일(어휘) + kernel(4 hook).
60
+ The base owns only the task **lifecycle** (claim · progress · complete · release · delta), so it is move-neutral. A new vertical is a profile (vocabulary) plus a kernel (4 hooks).
61
61
 
62
- ## 4 버티컬 — base 가 여러 flow 성격을 담음을 실증
62
+ ## Four verticals — evidence that one base carries several flow shapes
63
63
 
64
- | 버티컬 | flow 성격 | base 대응 |
64
+ | vertical | flow shape | how the base copes |
65
65
  |---|---|---|
66
- | **WMS** | 이동 (putaway/pick/pack/ship) + 백오더 | 기본 |
67
- | **YMS** | 이동 (spot/pull) + **시간창 스케줄링**(어포인트먼트↔도크도어 예약) | 스케줄링=도메인 게이팅으로 **base 흡수** |
68
- | **MES** | **변환**(TransformationEvent) + 다단계 라우팅 + **이종 자원**(cutter/welder) | 자원 매칭=`FlowTask.resourceType` **base 확장** |
69
- | **EMS** | 계측의 흐름 — 적산·구간 마감·피크·귀속 | 유입·귀속 모듈로 확장(자리=**전기적 구간**) |
66
+ | **WMS** | movement (putaway/pick/pack/ship) + backorders | as-is |
67
+ | **YMS** | movement (spot/pull) + **time-window scheduling** (appointment ↔ dock door reservation) | scheduling **absorbed** as domain gating |
68
+ | **MES** | **transformation** (TransformationEvent) + multi-step routing + **heterogeneous resources** (cutter/welder) | resource matching = `FlowTask.resourceType`, a minimal base extension |
69
+ | **EMS** | the flow of measurement — totalising, window closing, peaks, attribution | intake + attribution modules (a "place" here is an **electrical segment**) |
70
70
 
71
- EMS 는 **표준 앵커가 다르다**: 물류·생산은 ISA-95/EPCIS 로 재고 에너지는 **IEC 61850**(설비 데이터 모델)·**ISO 50001**(에너지 경영)로 잰다. 대응할 표준 이름이 없으면 그 칸을 **비운다** — 억지로 가까운 이름을 적으면 적합성 표가 거짓을 말한다.
71
+ EMS **anchors to a different standard**: logistics and production are measured against ISA-95/EPCIS, energy against **IEC 61850** (equipment data model) and **ISO 50001** (energy management). Where the standard has no matching name, that cell is **left empty** — forcing a nearby name would make the conformance table lie.
72
72
 
73
- ## 4대 자원 (ISA-95)
73
+ ## Four resources (ISA-95)
74
74
 
75
- 작업이 요구하는 자원을 표준대로 넷으로 나눠 든다. **한 그릇에 담지 않는 이유**: 사람은 고장 나지 않고 교대로 살고, 설비는 고장 나며 OEE 로 평가받고, 팔레트는 돌아온다. 섞으면 한쪽 어휘가 다른 쪽에 붙어 둘 다 거짓이 된다.
75
+ A task's resource needs are held as the standard's four kinds. **Why not one bucket:** people do not break down and live by shifts, equipment does break down and is judged by OEE, pallets come back. Mix them and one vocabulary attaches to the wrong thing, making both false.
76
76
 
77
- | ISA-95 | 커널 | 요구 명세 |
77
+ | ISA-95 | kernel | requirement spec |
78
78
  |---|---|---|
79
- | Personnel | `persons` · `PersonState` | `personnelSpecification` = 등급 + 인원 수 |
80
- | Equipment | `equipment` · `EquipmentState` | `equipmentSpecification` = 등급 + 대수 |
79
+ | Personnel | `persons` · `PersonState` | `personnelSpecification` = class + headcount |
80
+ | Equipment | `equipment` · `EquipmentState` | `equipmentSpecification` = class + count |
81
81
  | PhysicalAsset | `assets` · `AssetState` (GS1 **GRAI**) | `physicalAssetSpecification` |
82
82
  | Material | `items` · `ItemState` (EPCIS) | — |
83
83
 
84
- 배정 규율: **부분 확보로 시작하지 않는다.** 한 종류라도 모자라면 기다린다(반쯤 잡고 실패하면 자원이 일 없이 묶인다). 확보(`claim*`)와 확정(`assign*`)이 나뉜 이유. 완료 시 함께 해제하되 **자산은 도착 자리에 남는다**(물건이므로 — 회수의 출발점).
84
+ Claim discipline: **never start with a partial claim.** If any kind is short, wait — half-claiming ties resources up with no work to show for it. That is why claiming (`claim*`) and committing (`assign*`) are separate. Everything is released on completion **except the asset, which stays where it arrived** (it is a physical thing — that is where recovery starts).
85
85
 
86
- > 팔레트를 물품으로 두지 않은 이유: GS1 에서 **SSCC**(물류단위)와 **GRAI**(돌아오는 팔레트 자체)는 다른 것이다. 같은 GRAI 가 오늘은 이 SSCC 를, 내일은 다른 SSCC 를 싣는다. 그래서 **신설하고 연결**한다(`AssetState.carrying` ↔ `ItemState.carriedBy`).
86
+ > Why a pallet is not an item: in GS1, an **SSCC** (a logistic unit) and a **GRAI** (the returnable pallet itself) are different things. The same GRAI carries one SSCC today and another tomorrow. So we model them separately and link them (`AssetState.carrying` ↔ `ItemState.carriedBy`).
87
87
 
88
- 자원에는 **자격**이 붙는다. 등급이 시험을 요구하고(`testSpecificationIds`) 개체가 결과를 든다(`testResults`) — 요구된 시험의 결과가 없거나 만료·불합격이면 자격이 성립하지 않는다(`meetsTests`). 결과가 **아예 없는 것**과 **불합격**을 같은 값으로 만들지 않는다.
88
+ Resources carry **qualifications**. A class requires tests (`testSpecificationIds`) and an instance holds results (`testResults`) — a required test that is missing, expired or failed means the qualification does not hold (`meetsTests`). **"No result" and "failed" are never the same value.**
89
89
 
90
- ## 자리와 그 물리 조건
90
+ ## Places and their physical conditions
91
91
 
92
- `locations` · `LocationState` — 자리는 ISA-95 설비 계층의 노드다(Enterprise→Site→Area→StorageZone→StorageUnit).
92
+ `locations` · `LocationState` — a place is a node in the ISA-95 equipment hierarchy (Enterprise→Site→Area→StorageZone→StorageUnit).
93
93
 
94
- 자리는 **물리 조건**을 든다: `observations`(속성별 최신 관측 — 온도·습도 등)와 `testSpecificationIds`(그 자리에 걸린 기준). 조건 없이는 물건의 상태를 판정할 수 없고, **이 조인은 트윈만 할 수 있다** — 계측 시스템은 물건의 자리 이력을 모르고, 물류 시스템은 조건 이력을 모른다.
94
+ A place holds **physical conditions**: `observations` (the latest reading per property — temperature, humidity …) and `testSpecificationIds` (the criteria that apply there). Without conditions you cannot judge the state of the goods, and **only the twin can make this join** — the metering system does not know where the goods were, and the logistics system does not know the conditions.
95
95
 
96
- | 축 | 표준 | 우리가 더한 것 |
96
+ | axis | standard | what we added |
97
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` |
98
+ | observation | `OperationsEvent` + `OperationsRecordTemplate` (`EffectiveTimestamp` · `EffectiveEndDate` · `HierarchyScope`) | we **narrowed** the value to `ValueType` — a physical quantity with no unit cannot be judged |
99
+ | criteria | `TestSpecificationCriteria` (`Expression`, free text) | **numeric limits** `limit: {minimum, maximum, uom}` — free text cannot be evaluated |
100
+ | verdict | `TestResult.EvaluatedCriterionResult` (TextType) | narrowed to `pass`/`fail` · CBV dispositions `conformant`/`non_conformant` |
101
101
 
102
- 판정 함수 `outsideLimit()` 은 **세 갈래로 답한다** — 벗어남(`true`) · 안(`false`) · **판정 못 함(`undefined`)**. 숫자 한계가 없거나, 값이 수가 아니거나, 양쪽이 서로 다른 단위를 말하면 거절한다. 「모름」을 「적합」으로 만들지 않기 위한 것이고, 규제 기록에서 그 구별이 사라지면 결함이 아니라 사고다. 근거의 빈 곳은 `testEvidenceGaps()` 가 따로 센다.
102
+ `outsideLimit()` answers **three ways** — outside (`true`), inside (`false`), **cannot judge (`undefined`)**. It refuses when there is no numeric limit, when the value is not a number, or when the two sides declare different units. This keeps "unknown" from collapsing into "conformant"; in a regulated record, losing that distinction is not a bug but an incident. Missing evidence is counted separately by `testEvidenceGaps()`.
103
103
 
104
- 관측은 **사건으로 들어온다**(`OP_EVENT.observation` = `location.measured`) — 상태에만 있는 축은 조용히 사라지는 축이다. 구간 관측(`effectiveEndTime`)은 그 구간 안에서만 참이다: 원본이 「그날 아침」만 말하면 09:00 을 지어내지 않는다.
104
+ Observations **arrive as events** (`OP_EVENT.observation` = `location.measured`) — an axis that lives only in state is an axis that quietly disappears. An interval observation (`effectiveEndTime`) is true only within its interval: if the source says "that morning", we do not invent 09:00.
105
105
 
106
- ## 공정 명세와 소요시간 세 층
106
+ ## Lots carry their test evidence
107
107
 
108
- `OperationDef` 가 ISA-95 `OperationsSegment` 를 담는다 — `duration`(xsd:duration) · `variability`(퍼짐) · `parameters`(수율·준비시간) · 자원 요구 셋. 소요시간은 **강한 근거부터** 찾는다:
108
+ `ItemState.testResults` — the latest result per specification, each carrying the measurements behind it.
109
109
 
110
- **실측 추정기(호스트 주입) > 선언된 명세 > 커널 상수**
110
+ A record that says only "non-conformant" and one that says "we measured 4.2 °C and it was over the limit" are **different objects**. Regulated records are read **after** an incident, so a verdict without evidence does nothing at the moment it is needed. And the twin is the only thing that can carry this forward: lots cross systems, and the system that judged does not know what happened next.
111
111
 
112
- `specCoverage()` 가 무엇을 썼는지 세어 예측의 **자격**을 낸다 — `relative`(전부 상수: 대안 비교만) · `partial` · `absolute-capable` · `calibrated`. 근거가 없다고 거절하지 않고, **무엇에 근거했는지 함께 말한다.**
112
+ In the standard the verdict is a separate record that **points at** its subject (`TestResult.TestableObjectID`), so events point and state folds it into the object — our narrowing, recorded in the contract. Only the latest result per specification is kept: history is a question for the journal, not for state.
113
113
 
114
- > 표준이 정하지 않은 것: ISA-95 는 자원 구조는 정하지만 파라미터 ID 어휘는 정하지 않는다. 그래서 `OP_PARAM`(yield·setupDuration) **한 곳**에서 정하고, 발명한 자리임을 숨기지 않는다.
114
+ When the source measures but does not judge, the kernel judges from the declared criteria and marks the verdict `derived`. A verdict from the field carries no mark — **absence means the field said it.** The kernel never overrides a verdict the source gave.
115
115
 
116
- ## 3채널 계약 (Face 1) + 구독
116
+ ## Operation specs and three layers of duration
117
117
 
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` 가 검증한다.
121
- - **TwinRuntime**: `subscribe`(snapshot→delta, revision 연속) · `resync` · `tick`. sparse 스트리밍(전이·move-start 만, 매 tick 아님).
118
+ `OperationDef` carries the ISA-95 `OperationsSegment` — `duration` (xsd:duration) · `variability` · `parameters` (yield, setup) · the three resource requirements. Duration is resolved **strongest evidence first**:
122
119
 
123
- `StateSnapshot` 의 축: `locations` · `items` · `equipment` · `persons` · `assets` · `tasks` · `orders` · `attentions` · `energy` · `conformance` · `nowTime` · `identityGrounding` · `unhandled` · `stepsWithoutMaterial`.
120
+ **measured estimator (host-injected) > declared spec > kernel constant**
124
121
 
125
- **`nowTime` 은 트윈이 직접 말한다.** 소비처가 벽시계로 대신 재면 시뮬 트윈에서 엉뚱한 값이 나오고, 관측 모드에서는 「지금」이 마지막으로 들은 발생 시각이다. 없으면 소비처는 **재지 않는다**.
122
+ `specCoverage()` counts which was used and reports the forecast's **standing** — `relative` (all constants: comparison only) · `partial` · `absolute-capable` · `calibrated`. It never refuses for lack of evidence; it **says what the answer rests on.**
126
123
 
127
- ## State 이원 모델
124
+ > What the standard does not fix: ISA-95 defines resource structure but not parameter-ID vocabulary. So `OP_PARAM` (yield, setupDuration) is defined in **one place**, and the fact that we invented it is not hidden.
128
125
 
129
- - **EPCIS 저널**(이산): 재고·위치·조립·변환. `epcis.ts` 로 정규 방출, `validateEpcisEvent` 검증.
130
- - **운영·키네마틱**(연속): 설비 motion(from/to/progress)·태스크·오더 진척. 운영 델타로 미러.
131
- - **`ObservedReducer`** 가 둘을 접어 재구성 → sim 이벤트든 live 이벤트든 같은 State(데이터원 스왑).
126
+ ## The three-channel contract (Face 1) and subscription
132
127
 
133
- ### 구동은 둘, 규칙은 하나 — 적합성 하네스
128
+ - **State**: `getSnapshot()` plus a delta stream. Deltas are EPCIS (stock/place/aggregation/transformation) + operational deltas (`task/equipment/order.status` — the half EPCIS cannot reconstruct) + observations (`location.measured`) + test results (`test.result`).
129
+ - **Command** (act): `dispatch(cmd)` actually mutates state and therefore produces State deltas (closing the loop). `order.hold`/`resume` are common; domains extend through the `handleCommand` seam.
130
+ - **Scenario**: `scenario.load/start/pause/setSpeed`. A declared scenario is validated by `scenario-validate.ts`.
131
+ - **TwinRuntime**: `subscribe` (snapshot→delta, continuous revision) · `resync` · `tick`. Sparse streaming — transitions and move-starts only, not every tick.
134
132
 
135
- 상태를 만드는 구동이 둘이다(시뮬 `tick()` / 관측 `apply()`). 각자 계약으로 옮기는 코드가 두 벌이면 한쪽만 고쳤을 때 **조용히 어긋난다** — 계약 필드가 거의 다 선택이라 "안 채우는 것이 합법"이고, 컴파일러가 잡아 주지 않는다.
133
+ `StateSnapshot` axes: `locations` · `items` · `equipment` · `persons` · `assets` · `tasks` · `orders` · `attentions` · `energy` · `conformance` · `nowTime` · `identityGrounding` · `unhandled` · `stepsWithoutMaterial`.
136
134
 
137
- > **불변식: 커널 상태의 모든 사실은 이벤트로 나가야 한다 (상태 ⊆ 이벤트).**
135
+ **`nowTime` is the twin speaking its own clock.** A consumer that substitutes wall-clock time gets nonsense in a simulated twin, and in observation mode "now" is the last event time we heard. If it is absent, consumers **do not measure**.
138
136
 
139
- 나가지 않는 사실은 미러가 모르고, 저널로 복원되지도 않고(시간여행), 미러에서 세운 예측 씨앗에도 실리지 않는다. `test/driver-conformance.test.ts` 가 시뮬을 굴려 **그 이벤트를 그대로 미러에 흘리고** 두 스냅샷을 대조한다. 정당한 예외는 주석이 아니라 **상수로** 들고 이유를 적는다(호스트가 적분하는 `oee`, 매 tick 가지 않는 보간값 — 대신 **앵커는 반드시 같다**).
137
+ ## The dual state model
140
138
 
141
- ## 재기동 — 재개점에서 이어 접는다
139
+ - **EPCIS journal** (discrete): stock, place, aggregation, transformation. Emitted canonically through `epcis.ts` and checked by `validateEpcisEvent`.
140
+ - **Operational and kinematic** (continuous): equipment motion (from/to/progress), task and order progress. Mirrored as operational deltas.
141
+ - **`ObservedReducer`** folds both into the same State, whether the events came from a simulation or from the field.
142
142
 
143
- 저널을 처음부터 다시 접지 않기 위한 축이다. 실 저널이 수천만 줄인 현장에서 이것은 성능이 아니라 **가능/불가능**의 문제다.
143
+ ### Two drivers, one rule — the conformance harness
144
+
145
+ Two things produce state (simulation `tick()` and observation `apply()`). If each has its own copy of the mapping to the contract, fixing one **silently diverges** from the other — nearly every contract field is optional, so "not filling it" is legal and the compiler says nothing.
146
+
147
+ > **Invariant: every fact in kernel state must go out as an event (state ⊆ events).**
148
+
149
+ A fact that does not go out is invisible to the mirror, cannot be restored from the journal (time travel), and is missing from the seed a forecast forks from. `test/driver-conformance.test.ts` runs a simulation, **feeds its own events into a mirror**, and compares the two snapshots. Legitimate exceptions are held as **constants with a stated reason** — never as prose — for example the host-integrated `oee` and interpolated values that do not travel every tick; **their anchors must still match.**
150
+
151
+ ## Restart — fold from a resume point, not from line one
152
+
153
+ This axis exists so a restart does not re-fold the journal from the beginning. On a real site whose journal runs to tens of millions of rows, that is not a performance question but a question of possible versus impossible.
144
154
 
145
155
  ```ts
146
- observedCheckpoint(): ReducerCheckpoint | undefined // 재개점을 꺼낸다(관측 구동이 아니면 undefined)
147
- restoreObserved(cp: ReducerCheckpoint): void // 재개점에서 되세운다
148
- replayFrom(model, checkpoint, events) // 재개점 + 꼬리만 접는다
156
+ observedCheckpoint(): ReducerCheckpoint | undefined // take the resume point (undefined unless observation-driven)
157
+ restoreObserved(cp: ReducerCheckpoint): void // stand the reducer back up from it
158
+ replayFrom(model, checkpoint, events) // fold the resume point plus the tail only
149
159
  ```
150
160
 
151
- > **상태 스냅샷은 씨앗이 되지 못한다.** 리듀서는 소비처가 보는 값 말고도 든다 — 부모를 기다리는 담김·집계 중인 수량·담을 자리를 몰라 세어 둔 사건. 상태만 되돌리고 뒤를 이어 접으면 **0부터 접은 결과와 조용히 달라진다.**
161
+ > **A state snapshot cannot serve as the seed.** The reducer holds more than what consumers see — aggregations awaiting their parent, quantities mid-tally, events counted because there was nowhere to put them. Restore only the state and fold the tail and you **quietly** get a different answer from folding from zero.
162
+
163
+ That equivalence (`resume point + tail == fold from zero`) is proved by `test/observed-checkpoint.test.ts`. The background, the wrong turns and the host wiring are written up in [`design/fold-and-resume.md`](../../design/fold-and-resume.md).
164
+
165
+ Axes the **source will not restate** are carried separately (`hydrateContinuity`) — the open demand window's accumulation, totaliser baselines, the peak since observation began, and when each attention started. Observation axes (stock, place, equipment) are deliberately **not** seeded that way.
166
+
167
+ ### What happened does not disappear; what is current does
152
168
 
153
- 그 동치(`재개점 + 꼬리 == 0부터 접기`)는 `test/observed-checkpoint.test.ts` 가 증명한다. 배경·시행착오·호스트 배선은 [`design/fold-and-resume.md`](../../design/fold-and-resume.md).
169
+ A later transition that does not mention a field does not erase it — **if that field records something that happened.** A thing that happened cannot become a thing that did not happen, so `materialActual` survives until the source restates it (an explicit empty array clears it).
154
170
 
155
- 원본이 **되풀어 주지 않는 축**은 따로 이어받는다(`hydrateContinuity`) — 열린 수요 구간의 누적·적산 기준점·관측 이후 최대·신호가 언제부터인지. 관측 축(재고·자리·설비)은 그 문으로 심지 않는다.
171
+ The opposite rule holds for current values. `remainingMs`, `progress` and the resource reference are emitted only while a task is running; keeping them would show a finished task still holding time and still gripping a resource it released. **Before widening this list, ask whether the field's absence could itself be a fact.** If it could, it does not belong here.
156
172
 
157
- ## 주의 신호 — 커널이 판정하고, 문장은 만들지 않는다
173
+ ## Attentions — the kernel judges, but writes no sentences
158
174
 
159
- `attentions` 는 커널이 스스로 내는 판정이다(`deriveAttentions`/`collectAttentions`). 각 신호는 `kind` · `severity` · `anchor`(어디의 일인가) · `params`(근거가 된 값)를 든다.
175
+ `attentions` are judgements the kernel makes for itself (`deriveAttentions` / `collectAttentions`). Each carries `kind` · `severity` · `anchor` (whose problem it is) · `params` (the values behind it).
160
176
 
161
- **문장을 만들지 않는다.** 커널은 키와 값만 내고 표현은 소비처가 한다 — 커널이 한국어 문장을 들면 그 트윈은 한 언어에 묶인다. 그리고 판정에는 **근거를 함께 싣는다**: 한계를 넘은 관측 신호는 값·단위·한계·**언제의 값인지**·잰 것인지 접은 것인지를 모두 낸다. 센서가 멈춘 현장에서 그 시각이 없으면 사람은 방금 벗어난 것으로 읽는다.
177
+ **It writes no sentences.** The kernel emits keys and values; wording belongs to the consumer — a kernel that holds Korean sentences is a twin bound to one language. And a judgement always carries its evidence: an out-of-limit observation reports the value, the unit, the limit, **when it was measured**, and whether it was measured or derived. On a site whose sensor has stopped, a signal without that timestamp reads as though it just happened.
162
178
 
163
- 판정할 수 없으면 **아무 말도 하지 않는다** — 없는 이탈을 만들지도, 확인되지 않은 합격을 만들지도 않는다.
179
+ When it cannot judge, it **says nothing** — it neither invents a violation nor manufactures an unverified pass.
164
180
 
165
- ## 트윈 본연 — 현재로부터 예측 + 정합
181
+ ## What makes it a twin — forecasting from now, and reconciliation
166
182
 
167
- 관측·예측·행위가 따로 있는 것은 mirror+simulator 다. 트윈의 본질은 그 **커플링**:
183
+ Observation, prediction and action existing separately is a mirror plus a simulator. A twin is their **coupling**:
168
184
 
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-예측 후 실제가 그 시점에 도달하면 대조 → 드리프트를 알림 이벤트로.
185
+ - **`fork()`** — an isolated engine cloned from the current state, in-flight work included. The original (live or sim) keeps going while the fork rolls **forward from now** to predict completion times and throughput and to explore what-ifs. State, scenario and **RNG state** are all cloned, so it is a true continuation.
186
+ - **`compareStates(predicted, actual)`** — compares at the same instant and localises drift between model and reality (per item / location / order field). Divergence is the signal to look, or to intervene.
187
+ - **`monteCarloForecast`** — N seed-varied forks give the metric as a **distribution** (min/mean/p50/p90/max). Not "when will it finish" but "P90 completion time, probability of running out".
188
+ - **`EventJournal` + `replay`/`replayFrom`** — the twin's memory. `until(revision)` / `untilSimTime(iso)` reconstruct **any past instant** (time travel). In the host, append is swapped for a database event table.
189
+ - **`TwinHistory` + `counterfactualAt`** — return to instant T, **fork an alternative decision**, and compare against the baseline. "What if we had done X."
190
+ - **`TwinObserver`** — forks a prediction periodically and compares once reality reaches that instant, raising drift as an event.
175
191
 
176
- 예측은 **자기 한계를 함께 말한다**: 계보를 잇지 못한 구간(`stepsWithoutMaterial`)과 원본이 개체를 말하지 않아 막힌 오더(`blocked-source-omits-material`)를 결과에 싣는다. 조용히 넘기면 계보가 빈 예측이 정상처럼 보인다.
192
+ A forecast also **states its own limits**: steps taken without being able to link an item (`stepsWithoutMaterial`) and orders blocked because the source never named one (`blocked-source-omits-material`) are reported in the result. Passing over them silently would make a forecast with no lineage look normal.
177
193
 
178
- ## 확장 지점
194
+ ## Extension points
179
195
 
180
- - **버티컬 추가**: `{x}-profile.ts`(어휘) + `{x}-kernel.ts`(4 hook).
181
- - **능력(capability)으로 찾기**: concrete 타입에 강결합하지 않는다 — 자원이 능력을 선언하고 공정이 능력을 요구한다.
182
- - **할당 정책**: `AllocationPolicy` 교체(firstFit/partialFit 내장, FEFO/nearest/zone 추가 가능).
183
- - **소요시간 추정기**: 실측 기반 추정기를 호스트가 주입.
184
- - **Face2 어댑터**: 실 시스템 페이로드 → 선언적 매핑 → 정규 EPCIS.
185
- - **host 결합**: `TwinRuntime` 를 GraphQL sub·서비스로 래핑(전송은 얇은 host 계층).
196
+ - **A new vertical**: `{x}-profile.ts` (vocabulary) + `{x}-kernel.ts` (4 hooks).
197
+ - **Find by capability**: never bind to a concrete type — resources declare capabilities and operations require them.
198
+ - **Allocation policy**: swap `AllocationPolicy` (firstFit/partialFit ship with it; FEFO/nearest/zone can be added).
199
+ - **Duration estimators**: the host injects estimators built from measurements.
200
+ - **Face2 adapter**: a real system's payload → declarative mapping → canonical EPCIS.
201
+ - **Host binding**: wrap `TwinRuntime` in a GraphQL subscription or service (transport stays a thin host layer).
186
202
 
187
- ## 미구현(범위 밖)
203
+ ## Not implemented (out of scope)
188
204
 
189
- host 결합(전송·퍼시스턴스·커넥터) · 보드 바인딩(컴포넌트↔SGLN) · 도메인 깊이(WMS 멀티라인, YMS 상하차 AggregationEvent, MES BOM).
205
+ Host binding (transport, persistence, connectors) · board binding (component ↔ SGLN) · domain depth (WMS multi-line, YMS load/unload AggregationEvent, MES BOM).
190
206
 
191
- **모델의 남은 공백**(정직하게):
207
+ **Gaps in the model, stated plainly:**
192
208
 
193
- - 인원 **숙련도** — 등급과 자격 시험은 있으나 숙련도별 소요 차이는 없다
194
- - 자산 **회수 작업** — 빈 팔레트가 도착 자리에 남을 뿐
195
- - `equipment`/`locations` **계층 겸직** — 한 목록이 이동설비와 워크센터를, 다른 목록이 주소 가능한 자리와 작업 자리를 함께 든다(개명으로 이름은 정리됐고 계층은 그대로다)
196
- - **next-event 도약** — 지금은 고정 간격 tick
197
- - 라이브 **예측** — 미러 상태에서 fork 는 되지만 라이브 자체의 예측 경로는 통합 보류
198
- - 구조가 **여러 번 갈린** 저널의 마디별 재개점 — 지금은 최신 하나
209
+ - Personnel **skill level** — classes and qualification tests exist, but skill does not change duration
210
+ - Asset **recovery work** — an empty pallet simply stays where it arrived
211
+ - `equipment` / `locations` **level conflation** — one list holds both mobile equipment and work centres, the other both addressable places and work positions (the rename cleaned up the names, not the levels)
212
+ - **Next-event jumping** — ticks are still fixed-interval
213
+ - **Live forecasting** — a mirror's state can be forked, but the live path's own forecast is deliberately not unified yet
214
+ - Per-segment resume points for a journal whose **structure changed several times** — only the latest is kept
199
215
 
200
- **1.0 게이트**: `equipment`/`locations` 계층을 정리하거나 가산적으로만 가능하도록 확정한 뒤. 계약 파괴가 예정된 채로 1.0 을 주면 다음 라운드가 곧 2.0 이 된다.
216
+ **1.0 gate**: after the `equipment` / `locations` levels are either resolved or fixed as additive-only. Shipping 1.0 with a contract break already scheduled makes the next round a 2.0.
201
217
 
202
- ## 설계 SoT
218
+ ## Design sources of truth
203
219
 
204
220
  `operato-twin/design/` —
205
221
 
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` — 에너지 프로파일의 표준 앵커
222
+ - [`fold-and-resume.md`](../../design/fold-and-resume.md) — folding and resume points (the wrong turns, the principles, what is implemented)
223
+ - [`runtime-state-model.md`](../../design/runtime-state-model.md) — live = authority / history = derived, the time axis, identity
224
+ - [`04-decisions.md`](../../design/04-decisions.md) — ADRs
225
+ - `plans/simulation-spec.md` (specs, estimators, forecast standing) · `plans/four-resources-and-conformance.md` (four resources, invariants) · `plans/kernel-unification-live-observe.md` (driver unification)
226
+ - `profiles/ems.md` — the energy profile's standard anchors
@@ -366,6 +366,18 @@ export interface TestResultFact extends TestResult {
366
366
  /** 무엇을 시험했나 — 표준 `TestResult.TestableObjectID`. 물품이면 EPC. */
367
367
  testableObjectId: string;
368
368
  }
369
+ /**
370
+ * `OP_EVENT.complete` 의 내용 — 「이 목록이 전부다」.
371
+ *
372
+ * `axis` 를 받아 두는 이유: 지금은 물품만 이 방식이 필요하지만, 다른 목록도 같은 성질을 갖는다
373
+ * (연결된 시스템이 현재 목록을 통째로 말하는 것). 낱말을 닫으면 그때 계약을 또 고친다.
374
+ */
375
+ export interface AxisCompleteFact {
376
+ /** 어느 목록인가 — 지금은 `'items'` 만 다룬다. */
377
+ completeAxis: string;
378
+ /** 그 주기를 **시작한** 시각(ISO). 이보다 오래된 것은 이 주기에 오지 않은 것이다. */
379
+ since: ISOTime;
380
+ }
369
381
  /**
370
382
  * 이 시험 결과가 **이 시각에 유효한 합격인가.**
371
383
  *
@@ -1058,7 +1070,7 @@ export interface LocationState {
1058
1070
  /**
1059
1071
  * 이 자리를 **어떻게 알게 됐는가** — `master`(원 시스템 마스터/저작이 말해 준 자리) ·
1060
1072
  * `observed`(이벤트에 등장해서 알게 된 자리). 소비처가 둘을 구별해야 한다: 관측으로 알게 된 자리는
1061
- * 보드에 좌표가 없고 용량이 비어 있어 **계획에 참여하지 못한다**(그 사실을 감추지 않기 위한 표시).
1073
+ * 트윈 모델에 좌표가 없고 용량이 비어 있어 **계획에 참여하지 못한다**(그 사실을 감추지 않기 위한 표시).
1062
1074
  */
1063
1075
  origin?: 'master' | 'observed';
1064
1076
  /**
@@ -2102,7 +2114,7 @@ export interface ScenarioDef {
2102
2114
  * ── 왜 시나리오에 있나 (2026-08-17) ───────────────────────────────────────
2103
2115
  * 시나리오는 이미 「우리가 가정한 미래」다. 「30분 뒤 이 설비를 세운다」는 정확히 그 미래의 일부이므로
2104
2116
  * 여기 있어야 한다. 호스트가 fork 를 굴리며 밖에서 커맨드를 쏘는 방법도 되지만, 그러면 가정이 fork
2105
- * 밖에 남아 **다른 소비처가 같은 미래를 재생할 수 없다**(예측·백테스트·AI·보드가 각자 타이밍을 다시
2117
+ * 밖에 남아 **다른 소비처가 같은 미래를 재생할 수 없다**(예측·백테스트·AI·트윈 모델이 각자 타이밍을 다시
2106
2118
  * 짜야 한다). fork 가 자기완결이면 같은 선언 하나로 어디서든 같은 미래가 나온다.
2107
2119
  *
2108
2120
  * `atMs` 는 **시나리오를 시작한 시점부터의 경과**다(절대 시각이 아니다) — fork 는 언제 갈라져도
@@ -2180,6 +2192,27 @@ export declare const OP_EVENT: {
2180
2192
  * 없고, 미러가 이어받지 못한다(§상태 ⊆ 이벤트).
2181
2193
  */
2182
2194
  readonly test: "test.result";
2195
+ /**
2196
+ * **이 목록이 전부다** — 연결된 시스템이 현재 목록을 한 바퀴 다 보낸 뒤 그것을 알린다.
2197
+ *
2198
+ * ── 왜 필요한가 (2026-08-25 실측) ────────────────────────────────────────
2199
+ * 연결된 시스템은 매 주기 현재 재고를 전부 보낸다. 그런데 트윈은 그것을 낱낱의 관측으로 받아
2200
+ * **더하기만 했다.** 「그리고 이것 말고는 없다」를 받는 곳이 없어서, 목록에서 빠진 줄은 아무 말도
2201
+ * 오지 않은 것이 되고 트윈에 영원히 남았다.
2202
+ *
2203
+ * 실측: 실제 재고 1,704건인데 트윈이 10,038건을 갖고 있었다.
2204
+ *
2205
+ * ── 왜 「사라진 것을 알려 주기」가 아니라 이 방식인가 ──────────────────────
2206
+ * 연결 쪽이 앞 주기와 비교해 사라진 줄을 찾아 알리는 방법도 있다. 그런데 그것은 **하나 빠뜨리면
2207
+ * 그 줄이 영원히 남는다.** 이 방식은 주기마다 스스로 바로잡는다 — 이미 잘못 쌓인 것도 다음 주기에
2208
+ * 사라진다.
2209
+ *
2210
+ * ── 보내는 쪽이 지킬 것 ─────────────────────────────────────────────────
2211
+ * **끝까지 읽었을 때만 보낸다.** 읽다가 끊긴 주기에 이것을 보내면 **살아 있는 재고를 지운다.**
2212
+ * 그리고 `since` 는 그 주기를 **시작한** 시각이다(끝낸 시각이 아니다) — 주기 도중에 들어온 관측이
2213
+ * 지워지지 않아야 한다.
2214
+ */
2215
+ readonly complete: "axis.complete";
2183
2216
  /**
2184
2217
  * 주목 신호 확인(ack) — **사람이 한 행위**라 파생될 수 없다.
2185
2218
  *
@@ -2491,32 +2524,32 @@ export type OperationalDelta = TaskStatusDelta | EquipmentStatusDelta | PersonSt
2491
2524
  export type EventHandler = (e: CanonicalEnvelope) => void;
2492
2525
  export type Unsubscribe = () => void;
2493
2526
  /**
2494
- * **저장된 보드를 읽는 단 하나의 입구.**
2527
+ * **저장된 트윈 모델을 읽는 단 하나의 입구.**
2495
2528
  *
2496
- * `equipment` 로 개명하기 전에 저장된 보드는 `movers` 키를 갖고 있다(개명 시점 23개 인스턴스). (vocabulary-guard: allow — 읽기 호환 설명)
2529
+ * `equipment` 로 개명하기 전에 저장된 트윈 모델은 `movers` 키를 갖고 있다(개명 시점 23개 인스턴스). (vocabulary-guard: allow — 읽기 호환 설명)
2497
2530
  * 저장물을 다시 쓰지 않고 **읽을 때 흡수**한다 — 마이그레이션은 되돌리기 어렵고, 읽기 호환은 값싸다.
2498
2531
  *
2499
2532
  * 규율 둘:
2500
2533
  * - 이 함수를 **거치지 않고** `def.equipment` 를 직접 읽는 코드를 두지 않는다. 하나라도 남으면
2501
- * 그 경로에서만 옛 보드의 설비가 조용히 사라진다(빈 배열).
2534
+ * 그 경로에서만 옛 트윈 모델의 설비가 조용히 사라진다(빈 배열).
2502
2535
  * - **쓸 때는 새 이름만** 쓴다. 두 이름으로 쓰기 시작하면 저장물에 두 벌이 영구히 섞인다.
2503
2536
  *
2504
- * 제거 시점: 저장된 보드가 모두 `equipment` 키로 바뀐 것이 확인되면(운영 데이터 점검 후) 이 함수는
2537
+ * 제거 시점: 저장된 트윈 모델이 모두 `equipment` 키로 바뀐 것이 확인되면(운영 데이터 점검 후) 이 함수는
2505
2538
  * 사라진다. 그때까지 남겨 두는 이유를 여기 적어 두는 것이 주석의 일이다.
2506
2539
  */
2507
2540
  /**
2508
- * **저장된 보드의 자리를 읽는 단 하나의 입구.** `readBoardEquipment` 와 같은 규율.
2541
+ * **저장된 트윈 모델의 자리를 읽는 단 하나의 입구.** `readBoardEquipment` 와 같은 규율.
2509
2542
  *
2510
- * `nodes` → `locations` 개명(2026-08-01) 전에 저장된 보드는 `nodes` 키를 갖고 있다(개명 시점 23개). (vocabulary-guard: allow — 읽기 호환 설명)
2543
+ * `nodes` → `locations` 개명(2026-08-01) 전에 저장된 트윈 모델은 `nodes` 키를 갖고 있다(개명 시점 23개). (vocabulary-guard: allow — 읽기 호환 설명)
2511
2544
  * 이 함수를 거치지 않고 `def.locations` 를 직접 읽는 코드를 두지 않는다 — 하나라도 남으면 그 경로에서만
2512
- * 옛 보드의 자리가 조용히 사라진다(빈 배열 = 자리 없는 트윈 = 아무 일도 일어나지 않는다).
2545
+ * 옛 트윈 모델의 자리가 조용히 사라진다(빈 배열 = 자리 없는 트윈 = 아무 일도 일어나지 않는다).
2513
2546
  */
2514
2547
  export declare function readBoardLocations(def: TwinModelDef | (Record<string, unknown> & {
2515
2548
  locations?: unknown;
2516
2549
  nodes?: unknown;
2517
2550
  })): TwinModelDef['locations'];
2518
2551
  export declare function readBoardEquipment(def: TwinModelDef | Record<string, unknown>): TwinModelDef['equipment'];
2519
- /** 저장된 보드의 반복사용 자산 — 설비와 같은 정규화를 거친다. */
2552
+ /** 저장된 트윈 모델의 반복사용 자산 — 설비와 같은 정규화를 거친다. */
2520
2553
  export declare function readBoardAssets(def: TwinModelDef | Record<string, unknown>): NonNullable<TwinModelDef['assets']>;
2521
2554
  export interface TwinModelDef {
2522
2555
  /**
@@ -2632,7 +2665,7 @@ export interface TwinModelDef {
2632
2665
  *
2633
2666
  * 그 이름의 유래는 자재가 아니라 **소비자**다: `MesKernel` 의 생성자 인자 이름이
2634
2667
  * `mesSpec: MesDefinitionSpec` 이고(정의-구동 모드를 MES 에만 도입한 커밋 `ce4fbeb`), 호스트가
2635
- * 보드에 얹을 때 그 인자 이름을 그대로 가져왔다. 소비자가 하나일 때는 어색하지 않았다.
2668
+ * 트윈 모델에 얹을 때 그 인자 이름을 그대로 가져왔다. 소비자가 하나일 때는 어색하지 않았다.
2636
2669
  *
2637
2670
  * `materialSpec` 도 답이 아니다. ① 담긴 것이 자재가 아니다 — 타입·오퍼레이션(소요·변동·모수·
2638
2671
  * 인원/설비/자산/자재 명세)·라우트·레시피이고 자재는 그중 한 항목의 한 필드다. ② 그 이름은 이미