@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 +152 -136
- package/dist/contract.d.ts +44 -11
- package/dist/contract.js +31 -10
- package/dist/domain-catalog.d.ts +1 -1
- package/dist/event-journal.d.ts +1 -1
- package/dist/flow-engine.d.ts +2 -2
- package/dist/flow-engine.js +4 -4
- package/dist/observed-reducer.d.ts +11 -3
- package/dist/observed-reducer.js +129 -9
- package/dist/operational-ingest.d.ts +1 -1
- package/dist/operational-ingest.js +51 -0
- package/dist/vocabulary.js +3 -3
- package/dist-cjs/index.cjs +122 -16
- package/package.json +1 -1
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
|
-
|
|
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
|
-
-
|
|
6
|
-
-
|
|
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
|
|
12
|
-
epcis.ts GS1 EPCIS 2.0
|
|
13
|
-
vocabulary.ts
|
|
14
|
-
flow-engine.ts FlowEngine base — mechanics(RNG·clock·tick
|
|
15
|
-
observed-reducer.ts
|
|
16
|
-
state-projector.ts
|
|
17
|
-
|
|
18
|
-
allocation-policy.ts
|
|
19
|
-
capability.ts
|
|
20
|
-
capacity.ts
|
|
21
|
-
operations-capability.ts
|
|
22
|
-
domain-definition.ts
|
|
23
|
-
domain-catalog.ts
|
|
24
|
-
duration-estimator.ts
|
|
25
|
-
iso-duration.ts
|
|
26
|
-
make-to-order.ts
|
|
27
|
-
task-fold.ts
|
|
28
|
-
job-response.ts
|
|
29
|
-
|
|
30
|
-
event-journal.ts
|
|
31
|
-
forecast.ts
|
|
32
|
-
divergence.ts
|
|
33
|
-
counterfactual.ts
|
|
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
|
|
41
|
-
runtime.ts TwinRuntime — host-facing facade +
|
|
42
|
-
|
|
43
|
-
{wms,yms,mes,ems}-profile.ts
|
|
44
|
-
kernel.ts · {yms,mes,ems}-kernel.ts
|
|
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
|
-
>
|
|
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 —
|
|
49
|
+
## FlowEngine — one base, four hooks per domain
|
|
50
50
|
|
|
51
|
-
`abstract FlowEngine implements TwinKernel`
|
|
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)` |
|
|
56
|
-
| `onOrder(spec)` |
|
|
57
|
-
| `allocate(order)` | created
|
|
58
|
-
| `onTaskComplete(task)` |
|
|
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
|
|
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
|
-
##
|
|
62
|
+
## Four verticals — evidence that one base carries several flow shapes
|
|
63
63
|
|
|
64
|
-
|
|
|
64
|
+
| vertical | flow shape | how the base copes |
|
|
65
65
|
|---|---|---|
|
|
66
|
-
| **WMS** |
|
|
67
|
-
| **YMS** |
|
|
68
|
-
| **MES** |
|
|
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
|
|
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
|
-
##
|
|
73
|
+
## Four resources (ISA-95)
|
|
74
74
|
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
>
|
|
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
|
-
|
|
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` —
|
|
92
|
+
`locations` · `LocationState` — a place is a node in the ISA-95 equipment hierarchy (Enterprise→Site→Area→StorageZone→StorageUnit).
|
|
93
93
|
|
|
94
|
-
|
|
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
|
-
|
|
|
99
|
-
|
|
|
100
|
-
|
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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
|
-
|
|
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
|
-
##
|
|
116
|
+
## Operation specs and three layers of duration
|
|
117
117
|
|
|
118
|
-
-
|
|
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
|
-
|
|
120
|
+
**measured estimator (host-injected) > declared spec > kernel constant**
|
|
124
121
|
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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 //
|
|
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
|
-
>
|
|
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
|
-
|
|
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
|
-
|
|
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`
|
|
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
|
-
|
|
183
|
+
Observation, prediction and action existing separately is a mirror plus a simulator. A twin is their **coupling**:
|
|
168
184
|
|
|
169
|
-
- **`fork()`** —
|
|
170
|
-
- **`compareStates(predicted, actual)`** —
|
|
171
|
-
- **`monteCarloForecast`** — seed
|
|
172
|
-
- **`EventJournal` + `replay`/`replayFrom`** —
|
|
173
|
-
- **`TwinHistory` + `counterfactualAt`** —
|
|
174
|
-
- **`TwinObserver`** —
|
|
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
|
-
|
|
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
|
-
-
|
|
181
|
-
-
|
|
182
|
-
-
|
|
183
|
-
-
|
|
184
|
-
- **Face2
|
|
185
|
-
- **
|
|
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
|
-
|
|
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
|
|
196
|
-
- **
|
|
197
|
-
-
|
|
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
|
|
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
|
-
##
|
|
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) —
|
|
209
|
-
- `plans/simulation-spec.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
|
package/dist/contract.d.ts
CHANGED
|
@@ -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` 로 개명하기 전에 저장된
|
|
2529
|
+
* `equipment` 로 개명하기 전에 저장된 트윈 모델은 `movers` 키를 갖고 있다(개명 시점 23개 인스턴스). (vocabulary-guard: allow — 읽기 호환 설명)
|
|
2497
2530
|
* 저장물을 다시 쓰지 않고 **읽을 때 흡수**한다 — 마이그레이션은 되돌리기 어렵고, 읽기 호환은 값싸다.
|
|
2498
2531
|
*
|
|
2499
2532
|
* 규율 둘:
|
|
2500
2533
|
* - 이 함수를 **거치지 않고** `def.equipment` 를 직접 읽는 코드를 두지 않는다. 하나라도 남으면
|
|
2501
|
-
* 그 경로에서만 옛
|
|
2534
|
+
* 그 경로에서만 옛 트윈 모델의 설비가 조용히 사라진다(빈 배열).
|
|
2502
2535
|
* - **쓸 때는 새 이름만** 쓴다. 두 이름으로 쓰기 시작하면 저장물에 두 벌이 영구히 섞인다.
|
|
2503
2536
|
*
|
|
2504
|
-
* 제거 시점: 저장된
|
|
2537
|
+
* 제거 시점: 저장된 트윈 모델이 모두 `equipment` 키로 바뀐 것이 확인되면(운영 데이터 점검 후) 이 함수는
|
|
2505
2538
|
* 사라진다. 그때까지 남겨 두는 이유를 여기 적어 두는 것이 주석의 일이다.
|
|
2506
2539
|
*/
|
|
2507
2540
|
/**
|
|
2508
|
-
* **저장된
|
|
2541
|
+
* **저장된 트윈 모델의 자리를 읽는 단 하나의 입구.** `readBoardEquipment` 와 같은 규율.
|
|
2509
2542
|
*
|
|
2510
|
-
* `nodes` → `locations` 개명(2026-08-01) 전에 저장된
|
|
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
|
* 인원/설비/자산/자재 명세)·라우트·레시피이고 자재는 그중 한 항목의 한 필드다. ② 그 이름은 이미
|