@operato/twin-kernel 0.7.60 → 0.7.62
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 +32 -11
- package/dist/contract.js +10 -10
- package/dist/domain-catalog.d.ts +1 -1
- package/dist/epcis.d.ts +10 -0
- package/dist/epcis.js +11 -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 +3 -3
- package/dist/observed-reducer.js +38 -5
- package/dist/operational-ingest.d.ts +1 -1
- package/dist/operational-ingest.js +37 -1
- package/dist/vocabulary.js +3 -3
- package/dist-cjs/index.cjs +58 -13
- 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
|
@@ -1058,7 +1058,7 @@ export interface LocationState {
|
|
|
1058
1058
|
/**
|
|
1059
1059
|
* 이 자리를 **어떻게 알게 됐는가** — `master`(원 시스템 마스터/저작이 말해 준 자리) ·
|
|
1060
1060
|
* `observed`(이벤트에 등장해서 알게 된 자리). 소비처가 둘을 구별해야 한다: 관측으로 알게 된 자리는
|
|
1061
|
-
*
|
|
1061
|
+
* 트윈 모델에 좌표가 없고 용량이 비어 있어 **계획에 참여하지 못한다**(그 사실을 감추지 않기 위한 표시).
|
|
1062
1062
|
*/
|
|
1063
1063
|
origin?: 'master' | 'observed';
|
|
1064
1064
|
/**
|
|
@@ -1633,6 +1633,25 @@ export interface OrderState {
|
|
|
1633
1633
|
* 이것도 상태에 없어서 되살아난 오더는 빈 문자열을 갖고, 이후 모든 이벤트의 거래번호가 빈다.
|
|
1634
1634
|
*/
|
|
1635
1635
|
bizTransaction?: string;
|
|
1636
|
+
/**
|
|
1637
|
+
* 이 오더가 **이행하는 계획** — 표준 `OperationsRequestID`.
|
|
1638
|
+
*
|
|
1639
|
+
* ── 왜 부모-자식이 아닌가 (원문 대조 2026-08-24) ───────────────────────────
|
|
1640
|
+
* 「생산 계획 하나에 작업지시 여럿」을 담아야 했고, `parentOrderId` 같은 위계를 만들려다 원문을
|
|
1641
|
+
* 읽었다. **`OperationsRequestType` 에는 부모 축이 없다** — 계획은 `SegmentRequirement` 로 분해되고
|
|
1642
|
+
* 자식 계획을 갖지 않는다.
|
|
1643
|
+
*
|
|
1644
|
+
* 표준이 쓰는 방향은 반대다: **이행하는 쪽이 계획을 가리킨다.** `OperationsResponseType` 과
|
|
1645
|
+
* `OpSegmentResponseType` **둘 다** `OperationsRequestID` 를 든다(B2MML-OperationsPerformance ·
|
|
1646
|
+
* OperationsPerformanceTypes). 그래서 위계를 발명하지 않고 그 참조를 그대로 쓴다.
|
|
1647
|
+
*
|
|
1648
|
+
* 그러면 「이 계획이 얼마나 이행됐나」는 이 값으로 묶어 답한다 — **새 축을 만들지 않는다.** 계획
|
|
1649
|
+
* 자체도 오더로 들어오면 그 오더의 `requested` 가 계획량이고, 묶인 것들의 합과 **다를 수 있다.**
|
|
1650
|
+
* 그 차이가 곧 진단이므로 둘을 같은 값으로 만들지 않는다.
|
|
1651
|
+
*
|
|
1652
|
+
* `bizTransaction` 과 다르다 — 그것은 외부 거래(PO/SO)이고 이것은 **우리 안의 상위 계획**이다.
|
|
1653
|
+
*/
|
|
1654
|
+
operationsRequestId?: string;
|
|
1636
1655
|
/**
|
|
1637
1656
|
* 도메인이 **약속해 둔 자리와 시각창** — 어디로/언제 들이기로 했는가.
|
|
1638
1657
|
*
|
|
@@ -2083,7 +2102,7 @@ export interface ScenarioDef {
|
|
|
2083
2102
|
* ── 왜 시나리오에 있나 (2026-08-17) ───────────────────────────────────────
|
|
2084
2103
|
* 시나리오는 이미 「우리가 가정한 미래」다. 「30분 뒤 이 설비를 세운다」는 정확히 그 미래의 일부이므로
|
|
2085
2104
|
* 여기 있어야 한다. 호스트가 fork 를 굴리며 밖에서 커맨드를 쏘는 방법도 되지만, 그러면 가정이 fork
|
|
2086
|
-
* 밖에 남아 **다른 소비처가 같은 미래를 재생할 수 없다**(예측·백테스트·AI
|
|
2105
|
+
* 밖에 남아 **다른 소비처가 같은 미래를 재생할 수 없다**(예측·백테스트·AI·트윈 모델이 각자 타이밍을 다시
|
|
2087
2106
|
* 짜야 한다). fork 가 자기완결이면 같은 선언 하나로 어디서든 같은 미래가 나온다.
|
|
2088
2107
|
*
|
|
2089
2108
|
* `atMs` 는 **시나리오를 시작한 시점부터의 경과**다(절대 시각이 아니다) — fork 는 언제 갈라져도
|
|
@@ -2439,6 +2458,8 @@ export interface OrderStatusDelta {
|
|
|
2439
2458
|
*/
|
|
2440
2459
|
allocated?: string[];
|
|
2441
2460
|
bizTransaction?: string;
|
|
2461
|
+
/** 이행하는 계획 — 표준 `OperationsRequestID`(§`OrderState.operationsRequestId`). */
|
|
2462
|
+
operationsRequestId?: string;
|
|
2442
2463
|
/**
|
|
2443
2464
|
* 도메인이 **약속해 둔 자리와 시각창** — 어디로/언제 들이기로 했는가.
|
|
2444
2465
|
*
|
|
@@ -2470,32 +2491,32 @@ export type OperationalDelta = TaskStatusDelta | EquipmentStatusDelta | PersonSt
|
|
|
2470
2491
|
export type EventHandler = (e: CanonicalEnvelope) => void;
|
|
2471
2492
|
export type Unsubscribe = () => void;
|
|
2472
2493
|
/**
|
|
2473
|
-
* **저장된
|
|
2494
|
+
* **저장된 트윈 모델을 읽는 단 하나의 입구.**
|
|
2474
2495
|
*
|
|
2475
|
-
* `equipment` 로 개명하기 전에 저장된
|
|
2496
|
+
* `equipment` 로 개명하기 전에 저장된 트윈 모델은 `movers` 키를 갖고 있다(개명 시점 23개 인스턴스). (vocabulary-guard: allow — 읽기 호환 설명)
|
|
2476
2497
|
* 저장물을 다시 쓰지 않고 **읽을 때 흡수**한다 — 마이그레이션은 되돌리기 어렵고, 읽기 호환은 값싸다.
|
|
2477
2498
|
*
|
|
2478
2499
|
* 규율 둘:
|
|
2479
2500
|
* - 이 함수를 **거치지 않고** `def.equipment` 를 직접 읽는 코드를 두지 않는다. 하나라도 남으면
|
|
2480
|
-
* 그 경로에서만 옛
|
|
2501
|
+
* 그 경로에서만 옛 트윈 모델의 설비가 조용히 사라진다(빈 배열).
|
|
2481
2502
|
* - **쓸 때는 새 이름만** 쓴다. 두 이름으로 쓰기 시작하면 저장물에 두 벌이 영구히 섞인다.
|
|
2482
2503
|
*
|
|
2483
|
-
* 제거 시점: 저장된
|
|
2504
|
+
* 제거 시점: 저장된 트윈 모델이 모두 `equipment` 키로 바뀐 것이 확인되면(운영 데이터 점검 후) 이 함수는
|
|
2484
2505
|
* 사라진다. 그때까지 남겨 두는 이유를 여기 적어 두는 것이 주석의 일이다.
|
|
2485
2506
|
*/
|
|
2486
2507
|
/**
|
|
2487
|
-
* **저장된
|
|
2508
|
+
* **저장된 트윈 모델의 자리를 읽는 단 하나의 입구.** `readBoardEquipment` 와 같은 규율.
|
|
2488
2509
|
*
|
|
2489
|
-
* `nodes` → `locations` 개명(2026-08-01) 전에 저장된
|
|
2510
|
+
* `nodes` → `locations` 개명(2026-08-01) 전에 저장된 트윈 모델은 `nodes` 키를 갖고 있다(개명 시점 23개). (vocabulary-guard: allow — 읽기 호환 설명)
|
|
2490
2511
|
* 이 함수를 거치지 않고 `def.locations` 를 직접 읽는 코드를 두지 않는다 — 하나라도 남으면 그 경로에서만
|
|
2491
|
-
* 옛
|
|
2512
|
+
* 옛 트윈 모델의 자리가 조용히 사라진다(빈 배열 = 자리 없는 트윈 = 아무 일도 일어나지 않는다).
|
|
2492
2513
|
*/
|
|
2493
2514
|
export declare function readBoardLocations(def: TwinModelDef | (Record<string, unknown> & {
|
|
2494
2515
|
locations?: unknown;
|
|
2495
2516
|
nodes?: unknown;
|
|
2496
2517
|
})): TwinModelDef['locations'];
|
|
2497
2518
|
export declare function readBoardEquipment(def: TwinModelDef | Record<string, unknown>): TwinModelDef['equipment'];
|
|
2498
|
-
/** 저장된
|
|
2519
|
+
/** 저장된 트윈 모델의 반복사용 자산 — 설비와 같은 정규화를 거친다. */
|
|
2499
2520
|
export declare function readBoardAssets(def: TwinModelDef | Record<string, unknown>): NonNullable<TwinModelDef['assets']>;
|
|
2500
2521
|
export interface TwinModelDef {
|
|
2501
2522
|
/**
|
|
@@ -2611,7 +2632,7 @@ export interface TwinModelDef {
|
|
|
2611
2632
|
*
|
|
2612
2633
|
* 그 이름의 유래는 자재가 아니라 **소비자**다: `MesKernel` 의 생성자 인자 이름이
|
|
2613
2634
|
* `mesSpec: MesDefinitionSpec` 이고(정의-구동 모드를 MES 에만 도입한 커밋 `ce4fbeb`), 호스트가
|
|
2614
|
-
*
|
|
2635
|
+
* 트윈 모델에 얹을 때 그 인자 이름을 그대로 가져왔다. 소비자가 하나일 때는 어색하지 않았다.
|
|
2615
2636
|
*
|
|
2616
2637
|
* `materialSpec` 도 답이 아니다. ① 담긴 것이 자재가 아니다 — 타입·오퍼레이션(소요·변동·모수·
|
|
2617
2638
|
* 인원/설비/자산/자재 명세)·라우트·레시피이고 자재는 그중 한 항목의 한 필드다. ② 그 이름은 이미
|
package/dist/contract.js
CHANGED
|
@@ -1040,25 +1040,25 @@ export const CMD = {
|
|
|
1040
1040
|
};
|
|
1041
1041
|
// ── 보드 청사진 바인딩 (최소) ──────────────────────────────────────────────
|
|
1042
1042
|
/**
|
|
1043
|
-
* **저장된
|
|
1043
|
+
* **저장된 트윈 모델을 읽는 단 하나의 입구.**
|
|
1044
1044
|
*
|
|
1045
|
-
* `equipment` 로 개명하기 전에 저장된
|
|
1045
|
+
* `equipment` 로 개명하기 전에 저장된 트윈 모델은 `movers` 키를 갖고 있다(개명 시점 23개 인스턴스). (vocabulary-guard: allow — 읽기 호환 설명)
|
|
1046
1046
|
* 저장물을 다시 쓰지 않고 **읽을 때 흡수**한다 — 마이그레이션은 되돌리기 어렵고, 읽기 호환은 값싸다.
|
|
1047
1047
|
*
|
|
1048
1048
|
* 규율 둘:
|
|
1049
1049
|
* - 이 함수를 **거치지 않고** `def.equipment` 를 직접 읽는 코드를 두지 않는다. 하나라도 남으면
|
|
1050
|
-
* 그 경로에서만 옛
|
|
1050
|
+
* 그 경로에서만 옛 트윈 모델의 설비가 조용히 사라진다(빈 배열).
|
|
1051
1051
|
* - **쓸 때는 새 이름만** 쓴다. 두 이름으로 쓰기 시작하면 저장물에 두 벌이 영구히 섞인다.
|
|
1052
1052
|
*
|
|
1053
|
-
* 제거 시점: 저장된
|
|
1053
|
+
* 제거 시점: 저장된 트윈 모델이 모두 `equipment` 키로 바뀐 것이 확인되면(운영 데이터 점검 후) 이 함수는
|
|
1054
1054
|
* 사라진다. 그때까지 남겨 두는 이유를 여기 적어 두는 것이 주석의 일이다.
|
|
1055
1055
|
*/
|
|
1056
1056
|
/**
|
|
1057
|
-
* **저장된
|
|
1057
|
+
* **저장된 트윈 모델의 자리를 읽는 단 하나의 입구.** `readBoardEquipment` 와 같은 규율.
|
|
1058
1058
|
*
|
|
1059
|
-
* `nodes` → `locations` 개명(2026-08-01) 전에 저장된
|
|
1059
|
+
* `nodes` → `locations` 개명(2026-08-01) 전에 저장된 트윈 모델은 `nodes` 키를 갖고 있다(개명 시점 23개). (vocabulary-guard: allow — 읽기 호환 설명)
|
|
1060
1060
|
* 이 함수를 거치지 않고 `def.locations` 를 직접 읽는 코드를 두지 않는다 — 하나라도 남으면 그 경로에서만
|
|
1061
|
-
* 옛
|
|
1061
|
+
* 옛 트윈 모델의 자리가 조용히 사라진다(빈 배열 = 자리 없는 트윈 = 아무 일도 일어나지 않는다).
|
|
1062
1062
|
*/
|
|
1063
1063
|
export function readBoardLocations(def) {
|
|
1064
1064
|
const d = def; // vocabulary-guard: allow — 옛 키 캐스트
|
|
@@ -1066,12 +1066,12 @@ export function readBoardLocations(def) {
|
|
|
1066
1066
|
}
|
|
1067
1067
|
export function readBoardEquipment(def) {
|
|
1068
1068
|
/* `movers` 는 개명 **이전의 이름**이다(movers → equipmentList → equipment). 저장된 보드에는 세 세대가
|
|
1069
|
-
섞여 있어(실측: 23개 중 13개가 `movers`) 하나라도 빠뜨리면 그
|
|
1069
|
+
섞여 있어(실측: 23개 중 13개가 `movers`) 하나라도 빠뜨리면 그 트윈 모델은 **설비가 0인 공장**으로 읽힌다 —
|
|
1070
1070
|
오류 없이. 실제로 그랬다: 화면의 설비 수가 0이고, 용량 판정에 자원이 없고, 카탈로그 통합 테스트가
|
|
1071
1071
|
"완제품 0" 으로 떨어졌다. 셋 다 원인이 이 한 줄이었다. */
|
|
1072
1072
|
const d = def; // vocabulary-guard: allow — 옛 키를 읽어야 하는 자리
|
|
1073
1073
|
const list = d.equipment ?? d.equipmentList ?? d.movers ?? []; // vocabulary-guard: allow — 옛 키 흡수
|
|
1074
|
-
/* 소속 자리 키도 함께 정규화한다 — `homeNode → homeLocation` 개명 전
|
|
1074
|
+
/* 소속 자리 키도 함께 정규화한다 — `homeNode → homeLocation` 개명 전 트윈 모델이 23개 있다. (vocabulary-guard: allow — 옛 키 정규화 설명)
|
|
1075
1075
|
배열만 흡수하고 안쪽 키를 놓치면 설비는 나타나지만 **소속이 전부 비어** 롤업이 통째로 사라진다. */
|
|
1076
1076
|
return list.map(e => normalizeHomeLocation(e));
|
|
1077
1077
|
}
|
|
@@ -1083,7 +1083,7 @@ function normalizeHomeLocation(entry) {
|
|
|
1083
1083
|
const { homeNode: _drop, ...rest } = entry; // vocabulary-guard: allow — 옛 키 제거
|
|
1084
1084
|
return { ...rest, homeLocation: legacy };
|
|
1085
1085
|
}
|
|
1086
|
-
/** 저장된
|
|
1086
|
+
/** 저장된 트윈 모델의 반복사용 자산 — 설비와 같은 정규화를 거친다. */
|
|
1087
1087
|
export function readBoardAssets(def) {
|
|
1088
1088
|
const list = (def.assets ?? []);
|
|
1089
1089
|
return list.map(a => normalizeHomeLocation(a));
|
package/dist/domain-catalog.d.ts
CHANGED
|
@@ -86,7 +86,7 @@ export interface TwinAxisInfo {
|
|
|
86
86
|
* 현장이 낳는 사실이고, 그 집은 커널 상태와 저널이다(ISA-95 Part 4).
|
|
87
87
|
*
|
|
88
88
|
* 이 칸이 없으면 축만 늘려도 소비처가 board 에서 찾다가 **언제나 0 을 답한다** — 오류 없이,
|
|
89
|
-
* 그냥 빈 공장처럼. 이 프로젝트가 이미 그 모양으로 한 번 무너졌다(옛 어휘
|
|
89
|
+
* 그냥 빈 공장처럼. 이 프로젝트가 이미 그 모양으로 한 번 무너졌다(옛 어휘 트윈 모델이 자리 0·설비 0).
|
|
90
90
|
*
|
|
91
91
|
* · `document` — 저장된 트윈 모델 안. `path` 가 그 자리를 말한다
|
|
92
92
|
* · `state` — 커널 상태. **지금의 사실**이고 커널이 돌 때만 있다(멈추면 `null`, 0 이 아니다)
|
package/dist/epcis.d.ts
CHANGED
|
@@ -81,6 +81,16 @@ export declare const CBV_BIZSTEP: {
|
|
|
81
81
|
* 공정 중 검사가 그 모양이다.
|
|
82
82
|
*/
|
|
83
83
|
readonly inspecting: "urn:epcglobal:cbv:bizstep:inspecting";
|
|
84
|
+
/**
|
|
85
|
+
* **어느 단계로도 이름 붙지 않는 활동** — 원문: 「A business step not identified by any other」.
|
|
86
|
+
*
|
|
87
|
+
* 이것은 근사가 아니라 **표준이 준 낱말**이다. 그 구별이 중요하다: 재고 조정처럼 한 낱말이 두 일을
|
|
88
|
+
* 하는 원천(세어 보고 맞춘 것 · 사람이 정정한 것)을 `cycle_counting` 으로 옮기면 **일어나지 않은
|
|
89
|
+
* 계수를 기록**하게 된다. 그때 쓰는 것이 이 값이고, 무슨 일이었는지는 값으로 함께 나른다.
|
|
90
|
+
*
|
|
91
|
+
* 원문 확인: CBV Standard Release 2.0(Ratified Jun 2022) §7.1.
|
|
92
|
+
*/
|
|
93
|
+
readonly other: "urn:epcglobal:cbv:bizstep:other";
|
|
84
94
|
};
|
|
85
95
|
export type EpcisEventType = 'ObjectEvent' | 'AggregationEvent' | 'TransactionEvent' | 'TransformationEvent';
|
|
86
96
|
export type EpcisAction = 'ADD' | 'OBSERVE' | 'DELETE';
|
package/dist/epcis.js
CHANGED
|
@@ -89,7 +89,17 @@ export const CBV_BIZSTEP = {
|
|
|
89
89
|
* 이 단계에 `DISP.conformant`/`DISP.non_conformant` 가 붙어 판정이 처분으로 남는다 — 입고검수·
|
|
90
90
|
* 공정 중 검사가 그 모양이다.
|
|
91
91
|
*/
|
|
92
|
-
inspecting: 'urn:epcglobal:cbv:bizstep:inspecting'
|
|
92
|
+
inspecting: 'urn:epcglobal:cbv:bizstep:inspecting',
|
|
93
|
+
/**
|
|
94
|
+
* **어느 단계로도 이름 붙지 않는 활동** — 원문: 「A business step not identified by any other」.
|
|
95
|
+
*
|
|
96
|
+
* 이것은 근사가 아니라 **표준이 준 낱말**이다. 그 구별이 중요하다: 재고 조정처럼 한 낱말이 두 일을
|
|
97
|
+
* 하는 원천(세어 보고 맞춘 것 · 사람이 정정한 것)을 `cycle_counting` 으로 옮기면 **일어나지 않은
|
|
98
|
+
* 계수를 기록**하게 된다. 그때 쓰는 것이 이 값이고, 무슨 일이었는지는 값으로 함께 나른다.
|
|
99
|
+
*
|
|
100
|
+
* 원문 확인: CBV Standard Release 2.0(Ratified Jun 2022) §7.1.
|
|
101
|
+
*/
|
|
102
|
+
other: 'urn:epcglobal:cbv:bizstep:other'
|
|
93
103
|
};
|
|
94
104
|
// ── GS1 EPC URI 헬퍼 (표준) ────────────────────────────────────────────────
|
|
95
105
|
/** SSCC (물류단위: 팔레트/화물/트레일러) — 결정적 카운터 기반. */
|
package/dist/event-journal.d.ts
CHANGED
|
@@ -41,7 +41,7 @@ export declare function replayWithCheckpoint(model: TwinModelDef, events: readon
|
|
|
41
41
|
/**
|
|
42
42
|
* 한 구조 아래에서 일어난 이벤트들 — 재생의 한 마디.
|
|
43
43
|
*
|
|
44
|
-
* 필드 이름이 `model` 인 이유: 이 값의 정체는 **트윈 모델**이다(`TwinModelDef`).
|
|
44
|
+
* 필드 이름이 `model` 인 이유: 이 값의 정체는 **트윈 모델**이다(`TwinModelDef`). 트윈 모델은 그것을 그리는
|
|
45
45
|
* 표현 중 하나일 뿐이고, 소비처는 이미 전부 `model` 로 옮겼다. 그동안 호스트가 넘길 때마다 `model` 을
|
|
46
46
|
* `board` 키로 되돌려 담는 **어휘 번역기**가 경계에 끼어 있었다 — 조용히 `model` 로 넘기면 커널이
|
|
47
47
|
* `undefined` 를 읽고 재생이 통째로 죽던 자리다. 이름을 맞추면 그 브릿지가 사라진다.
|
package/dist/flow-engine.d.ts
CHANGED
|
@@ -635,7 +635,7 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
635
635
|
private materialSpecCrossKeyOverlaps;
|
|
636
636
|
private observedDirty;
|
|
637
637
|
private observeMode;
|
|
638
|
-
/** 관측 구동이 투영기를 세울 때 필요한 원본
|
|
638
|
+
/** 관측 구동이 투영기를 세울 때 필요한 원본 트윈 모델(구조는 이벤트가 아니라 마스터에서 온다). */
|
|
639
639
|
protected boardDef?: TwinModelDef;
|
|
640
640
|
/** 명세 소비 기록 — 무엇을 선언값으로, 무엇을 기본값으로 계산했나(정직한 자기보고). */
|
|
641
641
|
private specUse;
|
|
@@ -1598,7 +1598,7 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
1598
1598
|
/**
|
|
1599
1599
|
* 시뮬 시각의 분(0..1439) — 캘린더 판정의 기준.
|
|
1600
1600
|
*
|
|
1601
|
-
*
|
|
1601
|
+
* **트윈 모델이 선언한 기준으로 읽는다**(`utcOffsetMinutes`). 예전에는 UTC 로 읽어서 Rosarito(UTC−7)의
|
|
1602
1602
|
* 06시 교대가 7시간 틀렸다 — 시각대만 적고 기준을 안 적으면 반드시 이렇게 된다.
|
|
1603
1603
|
*/
|
|
1604
1604
|
protected minuteOfDay(): number;
|
package/dist/flow-engine.js
CHANGED
|
@@ -705,7 +705,7 @@ export class FlowEngine {
|
|
|
705
705
|
materialSpecCrossKeyOverlaps = 0;
|
|
706
706
|
observedDirty = false;
|
|
707
707
|
observeMode = false;
|
|
708
|
-
/** 관측 구동이 투영기를 세울 때 필요한 원본
|
|
708
|
+
/** 관측 구동이 투영기를 세울 때 필요한 원본 트윈 모델(구조는 이벤트가 아니라 마스터에서 온다). */
|
|
709
709
|
boardDef;
|
|
710
710
|
/** 명세 소비 기록 — 무엇을 선언값으로, 무엇을 기본값으로 계산했나(정직한 자기보고). */
|
|
711
711
|
specUse = new Map();
|
|
@@ -1035,7 +1035,7 @@ export class FlowEngine {
|
|
|
1035
1035
|
/* 계획 정지를 이어받는다 — 잃으면 씨앗이 **정비 중인 설비를 가용으로 놓고** 미래를 시뮬레이션한다
|
|
1036
1036
|
(예측이 낙관 쪽으로 치우친다). 씨앗 왕복 대조가 이것을 잡았다. */
|
|
1037
1037
|
...(m.held ? { held: true } : {}),
|
|
1038
|
-
/* 관측 스냅샷이 들고 있는 것은 관측을 따른다(미러가
|
|
1038
|
+
/* 관측 스냅샷이 들고 있는 것은 관측을 따른다(미러가 트윈 모델에서 읽어 실어 온다). */
|
|
1039
1039
|
...(m.properties ? { properties: m.properties } : {}),
|
|
1040
1040
|
...(m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}),
|
|
1041
1041
|
/* 결과도 이어받는다 — 잃으면 예측이 **자격 만료를 모르는 현장**에서 출발한다(낙관 쪽으로 치우친다). */
|
|
@@ -1561,7 +1561,7 @@ export class FlowEngine {
|
|
|
1561
1561
|
...(this.materialSpecCrossKeyOverlaps ? { materialSpecCrossKeyOverlaps: this.materialSpecCrossKeyOverlaps } : {})
|
|
1562
1562
|
} }
|
|
1563
1563
|
: {}),
|
|
1564
|
-
/* 출처 표시 —
|
|
1564
|
+
/* 출처 표시 — 트윈 모델(마스터)에서 온 자리다. 미러는 관측으로 알게 된 자리를 'observed' 로 구별하는데,
|
|
1565
1565
|
* 시뮬이 아무 표시도 안 하면 소비처가 두 스냅샷을 같은 규칙으로 읽지 못한다. */
|
|
1566
1566
|
locations: [...this.locations.values()].map(n => {
|
|
1567
1567
|
const { status, ...rest } = n;
|
|
@@ -3532,7 +3532,7 @@ export class FlowEngine {
|
|
|
3532
3532
|
/**
|
|
3533
3533
|
* 시뮬 시각의 분(0..1439) — 캘린더 판정의 기준.
|
|
3534
3534
|
*
|
|
3535
|
-
*
|
|
3535
|
+
* **트윈 모델이 선언한 기준으로 읽는다**(`utcOffsetMinutes`). 예전에는 UTC 로 읽어서 Rosarito(UTC−7)의
|
|
3536
3536
|
* 06시 교대가 7시간 틀렸다 — 시각대만 적고 기준을 안 적으면 반드시 이렇게 된다.
|
|
3537
3537
|
*/
|
|
3538
3538
|
minuteOfDay() {
|
|
@@ -179,7 +179,7 @@ export declare class ObservedReducer {
|
|
|
179
179
|
private corrections;
|
|
180
180
|
/** 반영하지 못한 사건의 종류별 집계 — 원문은 쌓지 않는다(저널에 이미 있다). */
|
|
181
181
|
private unhandled;
|
|
182
|
-
/** 선언된 판정 기준(
|
|
182
|
+
/** 선언된 판정 기준(트윈 모델에서 한 번 읽는다) — 판정은 선언한 것에만 걸린다. */
|
|
183
183
|
private testSpecs;
|
|
184
184
|
/**
|
|
185
185
|
* 자원별 **교대 선언** — 미러가 "지금 근무 중인가" 를 스스로 판정하기 위한 재료.
|
|
@@ -195,7 +195,7 @@ export declare class ObservedReducer {
|
|
|
195
195
|
* 통과시키는 어긋남이 생기고, 그 어긋남은 조용하다.
|
|
196
196
|
*/
|
|
197
197
|
private classDefs;
|
|
198
|
-
/** 시각 해석 기준(
|
|
198
|
+
/** 시각 해석 기준(트윈 모델 선언) — 없으면 UTC. 캘린더의 `HH:MM` 이 어느 기준인지 정한다. */
|
|
199
199
|
private utcOffsetMinutes?;
|
|
200
200
|
constructor(model: TwinModelDef);
|
|
201
201
|
/**
|
|
@@ -228,7 +228,7 @@ export declare class ObservedReducer {
|
|
|
228
228
|
* 종류는 모르므로 `unknown`, **용량은 비워 둔다**(발명하지 않는다), 출처는 `observed`.
|
|
229
229
|
*
|
|
230
230
|
* 출처를 표시하는 이유: 소비처가 "마스터가 말한 자리" 와 "관측으로 알게 된 자리" 를 구별해야 한다
|
|
231
|
-
* (
|
|
231
|
+
* (트윈 모델에 좌표가 없고, 용량을 채워야 계획에 참여한다). 마스터 동기가 오면 `master` 로 승격된다.
|
|
232
232
|
*/
|
|
233
233
|
private touchLocation;
|
|
234
234
|
/**
|
package/dist/observed-reducer.js
CHANGED
|
@@ -68,7 +68,7 @@ export class ObservedReducer {
|
|
|
68
68
|
corrections = [];
|
|
69
69
|
/** 반영하지 못한 사건의 종류별 집계 — 원문은 쌓지 않는다(저널에 이미 있다). */
|
|
70
70
|
unhandled = new Map();
|
|
71
|
-
/** 선언된 판정 기준(
|
|
71
|
+
/** 선언된 판정 기준(트윈 모델에서 한 번 읽는다) — 판정은 선언한 것에만 걸린다. */
|
|
72
72
|
testSpecs = new Map();
|
|
73
73
|
/**
|
|
74
74
|
* 자원별 **교대 선언** — 미러가 "지금 근무 중인가" 를 스스로 판정하기 위한 재료.
|
|
@@ -84,7 +84,7 @@ export class ObservedReducer {
|
|
|
84
84
|
* 통과시키는 어긋남이 생기고, 그 어긋남은 조용하다.
|
|
85
85
|
*/
|
|
86
86
|
classDefs = {};
|
|
87
|
-
/** 시각 해석 기준(
|
|
87
|
+
/** 시각 해석 기준(트윈 모델 선언) — 없으면 UTC. 캘린더의 `HH:MM` 이 어느 기준인지 정한다. */
|
|
88
88
|
utcOffsetMinutes;
|
|
89
89
|
constructor(model) {
|
|
90
90
|
this.utcOffsetMinutes = model.utcOffsetMinutes;
|
|
@@ -209,7 +209,7 @@ export class ObservedReducer {
|
|
|
209
209
|
* 종류는 모르므로 `unknown`, **용량은 비워 둔다**(발명하지 않는다), 출처는 `observed`.
|
|
210
210
|
*
|
|
211
211
|
* 출처를 표시하는 이유: 소비처가 "마스터가 말한 자리" 와 "관측으로 알게 된 자리" 를 구별해야 한다
|
|
212
|
-
* (
|
|
212
|
+
* (트윈 모델에 좌표가 없고, 용량을 채워야 계획에 참여한다). 마스터 동기가 오면 `master` 로 승격된다.
|
|
213
213
|
*/
|
|
214
214
|
touchLocation(id) {
|
|
215
215
|
if (!id || this.master.has(id))
|
|
@@ -271,6 +271,32 @@ export class ObservedReducer {
|
|
|
271
271
|
* 예측을 이어 굴릴 수 있고, 무자원이 설계인지(체류) 구별할 수 있다. */
|
|
272
272
|
this.touchLocation(d.fromNode);
|
|
273
273
|
this.touchLocation(d.toNode);
|
|
274
|
+
/*
|
|
275
|
+
* ── ★ **일어난 일은 다시 말하지 않아도 지우지 않는다** (2026-08-24) ────────
|
|
276
|
+
*
|
|
277
|
+
* 이 자리는 매번 **새 객체를 짓는다.** 그래서 뒤에 온 전이가 앞의 필드를 조용히 지웠다 — 물품은
|
|
278
|
+
* 오래전부터 병합하는데(`mergeItem`: 「아는 것을 잃지 않는다」) **작업만 그 규율 밖에 있었다.**
|
|
279
|
+
*
|
|
280
|
+
* 실측(포천): 작업 하나가 전이 여러 건으로 온다((로트,공정) 76짝 중 52짝이 2건 이상). 완료
|
|
281
|
+
* 전이에 투입 실적을 실으면 **그 뒤 어떤 전이도 그것을 지웠다.**
|
|
282
|
+
*
|
|
283
|
+
* ── 그런데 **통째 병합은 반대 방향으로 틀린다** ────────────────────────────
|
|
284
|
+
* 어떤 필드는 **일부러 사라진다**: `remainingMs`·`progress`·`startedAtSimMs` 는 진행 중일 때만
|
|
285
|
+
* 실리고(§`emitTask`), 자원 참조는 놓으면 빠진다. 그것을 지키면 끝난 작업이 남은 시간을 들고
|
|
286
|
+
* 놓은 자원을 계속 쥔 것으로 보인다.
|
|
287
|
+
*
|
|
288
|
+
* ── 그래서 선을 이렇게 긋는다 ──────────────────────────────────────────
|
|
289
|
+
* **과거의 사실**은 다시 말하지 않아도 지우지 않는다 — 일어난 일은 안 일어난 일이 될 수 없다
|
|
290
|
+
* **지금의 값**은 말하지 않으면 없는 것이다 — 그것이 「지금」의 뜻이다
|
|
291
|
+
*
|
|
292
|
+
* 지금 이 규율이 걸리는 것은 `materialActual`(무엇을 얼마나 썼나) 하나다 — 상태가 드는 것 중
|
|
293
|
+
* 「일어난 일」이 그것뿐이다(`outcome` 은 상태 계약에 아직 없다). 명시로 지우려면 원천이
|
|
294
|
+
* **빈 배열**을 보낸다(`materialActual: []`) — 그때는 말한 것이다.
|
|
295
|
+
*
|
|
296
|
+
* 늘릴 때는 「이 필드가 사라지는 것이 사실일 수 있나」를 먼저 물어야 한다. 그렇다면 넣지 않는다.
|
|
297
|
+
*/
|
|
298
|
+
const prevTask = this.tasks.get(d.taskId);
|
|
299
|
+
const keptActual = d.materialActual === undefined ? prevTask?.materialActual : undefined;
|
|
274
300
|
this.tasks.set(d.taskId, {
|
|
275
301
|
id: d.taskId, kind: d.kind, status: d.status, fromNode: d.fromNode, toNode: d.toNode,
|
|
276
302
|
itemRefs: d.itemRefs, resourceRef: d.resourceRef, orderId: d.orderId,
|
|
@@ -279,8 +305,13 @@ export class ObservedReducer {
|
|
|
279
305
|
...(d.personnel?.length ? { personnel: d.personnel.slice() } : {}),
|
|
280
306
|
...(d.assets?.length ? { assets: d.assets.slice() } : {}),
|
|
281
307
|
...(d.resources?.length ? { resources: d.resources.slice() } : {}),
|
|
282
|
-
/* 실제 자재 이동 — 인원·설비와 같은 채널로 온다(실적을 한 곳에서 읽는다).
|
|
283
|
-
|
|
308
|
+
/* 실제 자재 이동 — 인원·설비와 같은 채널로 온다(실적을 한 곳에서 읽는다).
|
|
309
|
+
말하지 않았으면 **앞의 것을 지킨다**(위 주석) — 빈 배열은 「지웠다」로 말한 것이다. */
|
|
310
|
+
...(d.materialActual?.length
|
|
311
|
+
? { materialActual: d.materialActual.map(r => ({ ...r })) }
|
|
312
|
+
: keptActual?.length
|
|
313
|
+
? { materialActual: keptActual.map(r => ({ ...r })) }
|
|
314
|
+
: {}),
|
|
284
315
|
...(d.priority !== undefined ? { priority: d.priority } : {}),
|
|
285
316
|
...(d.startTime ? { startTime: d.startTime } : {}),
|
|
286
317
|
...(d.endTime ? { endTime: d.endTime } : {})
|
|
@@ -433,6 +464,8 @@ export class ObservedReducer {
|
|
|
433
464
|
...(d.recipeKey ? { recipeKey: d.recipeKey } : {}),
|
|
434
465
|
...(d.allocated?.length ? { allocated: d.allocated.slice() } : {}),
|
|
435
466
|
...(d.bizTransaction ? { bizTransaction: d.bizTransaction } : {}),
|
|
467
|
+
/* 이행하는 계획 — 「이 계획이 얼마나 됐나」를 이 값으로 묶어 답한다(§`OrderState.operationsRequestId`). */
|
|
468
|
+
...(d.operationsRequestId ? { operationsRequestId: d.operationsRequestId } : {}),
|
|
436
469
|
/* 약속해 둔 자리·시각창도 채운다 — 시뮬만 알면 미러 위 예측이 「어디로 들일지」를 모른다.
|
|
437
470
|
(이 필드가 어느 길에서 빠지면 무슨 일이 나는지는 §dockDoor 에 적혀 있다.) */
|
|
438
471
|
...(d.dockDoor ? { dockDoor: d.dockDoor } : {}),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { IngestResult } from './face2-adapter.ts';
|
|
2
2
|
/** 이 문이 받는 여섯 가지 — 리듀서가 접는 것과 같은 목록(주목 확인은 우리 안의 행위라 제외). */
|
|
3
|
-
export type OperationalKind = 'task' | 'equipment' | 'person' | 'asset' | 'order' | 'quality' | 'test';
|
|
3
|
+
export type OperationalKind = 'task' | 'equipment' | 'person' | 'asset' | 'order' | 'quality' | 'test' | 'observation';
|
|
4
4
|
/**
|
|
5
5
|
* 정규 운영 레코드 — **델타의 필드 이름 + 시각(`at`)**.
|
|
6
6
|
*
|
|
@@ -109,7 +109,7 @@ const SPECS = {
|
|
|
109
109
|
fields: {
|
|
110
110
|
orderId: 'string', kind: 'string', status: 'string', requested: 'number', fulfilled: 'number',
|
|
111
111
|
gtin: 'string', held: 'boolean', lines: 'object[]', priority: 'number', startTime: 'string', endTime: 'string',
|
|
112
|
-
allocated: 'string[]', bizTransaction: 'string', dockDoor: 'string', windowStartMs: 'number', recordTime: 'string'
|
|
112
|
+
allocated: 'string[]', bizTransaction: 'string', operationsRequestId: 'string', dockDoor: 'string', windowStartMs: 'number', recordTime: 'string'
|
|
113
113
|
}
|
|
114
114
|
/* 상태·종류는 도메인이 소유한다 — 닫지 않는다. */
|
|
115
115
|
},
|
|
@@ -140,6 +140,28 @@ const SPECS = {
|
|
|
140
140
|
derived: 'boolean', propertyMeasurements: 'object[]', recordTime: 'string'
|
|
141
141
|
},
|
|
142
142
|
enums: { result: ['pass', 'fail'] }
|
|
143
|
+
},
|
|
144
|
+
/*
|
|
145
|
+
* **자리의 물리 관측** — 어느 자리의 어느 속성을 언제 얼마로 쟀나.
|
|
146
|
+
*
|
|
147
|
+
* 값에 **단위를 함께** 받는다(`ValueType`). 단위 없는 물리량은 판정의 재료가 못 된다 — 3 이 섭씨인지
|
|
148
|
+
* 화씨인지 모르면 어떤 기준으로도 판정할 수 없다. 다만 **요구하지는 않는다**: 단위를 비우는 실 원본이
|
|
149
|
+
* 흔하고, 요구하면 그 원본의 관측을 아예 담지 못한다(그때 판정은 커널이 거부한다 — `outsideLimit`).
|
|
150
|
+
*
|
|
151
|
+
* `effectiveTime` 도 요구하지 않는다. 없으면 봉투의 시각이 그 자리를 대신한다(§`resolve`) — 원본이
|
|
152
|
+
* 시각을 말하지 않는 수기 점검이 실재하고, 그때 지어낸 시각보다 폴링 시각이 정직하다.
|
|
153
|
+
*/
|
|
154
|
+
observation: {
|
|
155
|
+
eventType: OP_EVENT.observation,
|
|
156
|
+
identity: 'locationId',
|
|
157
|
+
/* 자리와 속성 — 둘 중 하나가 없으면 그 관측은 아무 데도 붙지 못한다. */
|
|
158
|
+
required: ['locationId', 'propertyId'],
|
|
159
|
+
fields: {
|
|
160
|
+
locationId: 'string', propertyId: 'string',
|
|
161
|
+
value: 'string', dataType: 'string', uom: 'string',
|
|
162
|
+
effectiveTime: 'string', effectiveEndTime: 'string', recordTime: 'string',
|
|
163
|
+
source: 'string', derived: 'boolean'
|
|
164
|
+
}
|
|
143
165
|
}
|
|
144
166
|
};
|
|
145
167
|
/**
|
|
@@ -187,6 +209,20 @@ export function operationalKindOf(record) {
|
|
|
187
209
|
*/
|
|
188
210
|
if (has('testableObjectId'))
|
|
189
211
|
return 'test';
|
|
212
|
+
/*
|
|
213
|
+
* ── ★ **채널을 열고 또 길을 내지 않았다** (2026-08-24) ──────────────────────
|
|
214
|
+
* `OP_EVENT.observation`(`location.measured`)을 내고 상태(`LocationState.observations`)와 조회
|
|
215
|
+
* (`observationAt`)까지 붙였는데 **이 라우팅이 `locationId` 를 보지 않았다.** 커넥터가 방의 온습도를
|
|
216
|
+
* 실어 보내면 「어느 운영 사실인지 모른다」로 거부됐다.
|
|
217
|
+
*
|
|
218
|
+
* 같은 부류를 하루에 일곱 번 만났다. 다만 이번엔 **거부되고 이유가 남았다** — 시험 결과 때는 어느
|
|
219
|
+
* 통도 아니어서 조용히 사라졌다. 그 차이가 이것을 5분 만에 찾게 했다(§`isOperationalRecord`).
|
|
220
|
+
*
|
|
221
|
+
* **둘을 함께 요구한다.** `locationId` 만으로는 자리를 말하는 다른 사실과 섞인다. 관측은 「어느
|
|
222
|
+
* 자리의 **무엇**을 쟀나」이므로 속성 없이는 담을 곳이 없다 — 그때는 받지 않는 것이 옳다.
|
|
223
|
+
*/
|
|
224
|
+
if (has('locationId') && has('propertyId'))
|
|
225
|
+
return 'observation';
|
|
190
226
|
return undefined;
|
|
191
227
|
}
|
|
192
228
|
/** 이 레코드가 운영 사실인가 — 호스트의 라우팅이 묻는 자리. */
|
package/dist/vocabulary.js
CHANGED
|
@@ -34,13 +34,13 @@ export const VOCABULARY_EXCEPTIONS = [
|
|
|
34
34
|
{ token: 'moverId', why: 'journal wire field — renaming would mix two keys for one fact across history' },
|
|
35
35
|
{ token: 'fromNode', why: 'journal wire field (task.status payload)' },
|
|
36
36
|
{ token: 'toNode', why: 'journal wire field (task.status payload)' },
|
|
37
|
-
/* ──
|
|
37
|
+
/* ── 트윈 모델의 옛 세대 키 — 저장된 데이터라 읽어는 줘야 한다 ────────────────
|
|
38
38
|
* 설비 배열의 이름은 세 세대를 거쳤다(movers → equipmentList → equipment). 저장된 보드에는
|
|
39
|
-
* 셋이 섞여 있고(실측 23개 중 13개가 `movers`), 하나라도 안 읽으면 그
|
|
39
|
+
* 셋이 섞여 있고(실측 23개 중 13개가 `movers`), 하나라도 안 읽으면 그 트윈 모델은 **설비가 0인 공장**
|
|
40
40
|
* 으로 조용히 읽힌다 — 화면의 설비 수가 0이 되고 용량 판정에서 자원이 사라진다. 쓰는 곳은
|
|
41
41
|
* `readBoardEquipment` 한 곳뿐이고, 거기서 새 이름으로 정규화해 내보낸다. */
|
|
42
42
|
{ token: 'movers', why: 'legacy board key (movers → equipmentList → equipment); read-only normalization in readBoardEquipment, 13 stored boards still use it' },
|
|
43
|
-
/* ── 씬 컴포넌트 타입 —
|
|
43
|
+
/* ── 씬 컴포넌트 타입 — 트윈 모델에 저장된 값이고, 뜻이 어긋나지도 않는다 ──────
|
|
44
44
|
* 보드 7개가 이 타입으로 컴포넌트를 담고 있어 개명하면 그 컴포넌트가 조용히 안 그려진다.
|
|
45
45
|
* 그리고 씬에서 이 이름은 자원이 아니라 **움직임 표현**을 가리킨다(표준과 충돌 아님). */
|
|
46
46
|
{ token: 'twin-mover', why: 'scene component type persisted in boards; names a motion representation, not a resource' },
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -1078,7 +1078,17 @@ var CBV_BIZSTEP = {
|
|
|
1078
1078
|
* 이 단계에 `DISP.conformant`/`DISP.non_conformant` 가 붙어 판정이 처분으로 남는다 — 입고검수·
|
|
1079
1079
|
* 공정 중 검사가 그 모양이다.
|
|
1080
1080
|
*/
|
|
1081
|
-
inspecting: "urn:epcglobal:cbv:bizstep:inspecting"
|
|
1081
|
+
inspecting: "urn:epcglobal:cbv:bizstep:inspecting",
|
|
1082
|
+
/**
|
|
1083
|
+
* **어느 단계로도 이름 붙지 않는 활동** — 원문: 「A business step not identified by any other」.
|
|
1084
|
+
*
|
|
1085
|
+
* 이것은 근사가 아니라 **표준이 준 낱말**이다. 그 구별이 중요하다: 재고 조정처럼 한 낱말이 두 일을
|
|
1086
|
+
* 하는 원천(세어 보고 맞춘 것 · 사람이 정정한 것)을 `cycle_counting` 으로 옮기면 **일어나지 않은
|
|
1087
|
+
* 계수를 기록**하게 된다. 그때 쓰는 것이 이 값이고, 무슨 일이었는지는 값으로 함께 나른다.
|
|
1088
|
+
*
|
|
1089
|
+
* 원문 확인: CBV Standard Release 2.0(Ratified Jun 2022) §7.1.
|
|
1090
|
+
*/
|
|
1091
|
+
other: "urn:epcglobal:cbv:bizstep:other"
|
|
1082
1092
|
};
|
|
1083
1093
|
function ssccUri(companyPrefix, serial) {
|
|
1084
1094
|
return `urn:epc:id:sscc:${companyPrefix}.${String(serial).padStart(10, "0")}`;
|
|
@@ -1384,7 +1394,7 @@ var ObservedReducer = class {
|
|
|
1384
1394
|
corrections = [];
|
|
1385
1395
|
/** 반영하지 못한 사건의 종류별 집계 — 원문은 쌓지 않는다(저널에 이미 있다). */
|
|
1386
1396
|
unhandled = /* @__PURE__ */ new Map();
|
|
1387
|
-
/** 선언된 판정 기준(
|
|
1397
|
+
/** 선언된 판정 기준(트윈 모델에서 한 번 읽는다) — 판정은 선언한 것에만 걸린다. */
|
|
1388
1398
|
testSpecs = /* @__PURE__ */ new Map();
|
|
1389
1399
|
/**
|
|
1390
1400
|
* 자원별 **교대 선언** — 미러가 "지금 근무 중인가" 를 스스로 판정하기 위한 재료.
|
|
@@ -1400,7 +1410,7 @@ var ObservedReducer = class {
|
|
|
1400
1410
|
* 통과시키는 어긋남이 생기고, 그 어긋남은 조용하다.
|
|
1401
1411
|
*/
|
|
1402
1412
|
classDefs = {};
|
|
1403
|
-
/** 시각 해석 기준(
|
|
1413
|
+
/** 시각 해석 기준(트윈 모델 선언) — 없으면 UTC. 캘린더의 `HH:MM` 이 어느 기준인지 정한다. */
|
|
1404
1414
|
utcOffsetMinutes;
|
|
1405
1415
|
constructor(model) {
|
|
1406
1416
|
this.utcOffsetMinutes = model.utcOffsetMinutes;
|
|
@@ -1504,7 +1514,7 @@ var ObservedReducer = class {
|
|
|
1504
1514
|
* 종류는 모르므로 `unknown`, **용량은 비워 둔다**(발명하지 않는다), 출처는 `observed`.
|
|
1505
1515
|
*
|
|
1506
1516
|
* 출처를 표시하는 이유: 소비처가 "마스터가 말한 자리" 와 "관측으로 알게 된 자리" 를 구별해야 한다
|
|
1507
|
-
* (
|
|
1517
|
+
* (트윈 모델에 좌표가 없고, 용량을 채워야 계획에 참여한다). 마스터 동기가 오면 `master` 로 승격된다.
|
|
1508
1518
|
*/
|
|
1509
1519
|
touchLocation(id) {
|
|
1510
1520
|
if (!id || this.master.has(id)) return;
|
|
@@ -1558,6 +1568,8 @@ var ObservedReducer = class {
|
|
|
1558
1568
|
if (this.stale(`task:${d.taskId}`, e)) return;
|
|
1559
1569
|
this.touchLocation(d.fromNode);
|
|
1560
1570
|
this.touchLocation(d.toNode);
|
|
1571
|
+
const prevTask = this.tasks.get(d.taskId);
|
|
1572
|
+
const keptActual = d.materialActual === void 0 ? prevTask?.materialActual : void 0;
|
|
1561
1573
|
this.tasks.set(d.taskId, {
|
|
1562
1574
|
id: d.taskId,
|
|
1563
1575
|
kind: d.kind,
|
|
@@ -1575,8 +1587,9 @@ var ObservedReducer = class {
|
|
|
1575
1587
|
...d.personnel?.length ? { personnel: d.personnel.slice() } : {},
|
|
1576
1588
|
...d.assets?.length ? { assets: d.assets.slice() } : {},
|
|
1577
1589
|
...d.resources?.length ? { resources: d.resources.slice() } : {},
|
|
1578
|
-
/* 실제 자재 이동 — 인원·설비와 같은 채널로 온다(실적을 한 곳에서 읽는다).
|
|
1579
|
-
|
|
1590
|
+
/* 실제 자재 이동 — 인원·설비와 같은 채널로 온다(실적을 한 곳에서 읽는다).
|
|
1591
|
+
말하지 않았으면 **앞의 것을 지킨다**(위 주석) — 빈 배열은 「지웠다」로 말한 것이다. */
|
|
1592
|
+
...d.materialActual?.length ? { materialActual: d.materialActual.map((r) => ({ ...r })) } : keptActual?.length ? { materialActual: keptActual.map((r) => ({ ...r })) } : {},
|
|
1580
1593
|
...d.priority !== void 0 ? { priority: d.priority } : {},
|
|
1581
1594
|
...d.startTime ? { startTime: d.startTime } : {},
|
|
1582
1595
|
...d.endTime ? { endTime: d.endTime } : {}
|
|
@@ -1681,6 +1694,8 @@ var ObservedReducer = class {
|
|
|
1681
1694
|
...d.recipeKey ? { recipeKey: d.recipeKey } : {},
|
|
1682
1695
|
...d.allocated?.length ? { allocated: d.allocated.slice() } : {},
|
|
1683
1696
|
...d.bizTransaction ? { bizTransaction: d.bizTransaction } : {},
|
|
1697
|
+
/* 이행하는 계획 — 「이 계획이 얼마나 됐나」를 이 값으로 묶어 답한다(§`OrderState.operationsRequestId`). */
|
|
1698
|
+
...d.operationsRequestId ? { operationsRequestId: d.operationsRequestId } : {},
|
|
1684
1699
|
/* 약속해 둔 자리·시각창도 채운다 — 시뮬만 알면 미러 위 예측이 「어디로 들일지」를 모른다.
|
|
1685
1700
|
(이 필드가 어느 길에서 빠지면 무슨 일이 나는지는 §dockDoor 에 적혀 있다.) */
|
|
1686
1701
|
...d.dockDoor ? { dockDoor: d.dockDoor } : {},
|
|
@@ -4066,7 +4081,7 @@ var FlowEngine = class {
|
|
|
4066
4081
|
materialSpecCrossKeyOverlaps = 0;
|
|
4067
4082
|
observedDirty = false;
|
|
4068
4083
|
observeMode = false;
|
|
4069
|
-
/** 관측 구동이 투영기를 세울 때 필요한 원본
|
|
4084
|
+
/** 관측 구동이 투영기를 세울 때 필요한 원본 트윈 모델(구조는 이벤트가 아니라 마스터에서 온다). */
|
|
4070
4085
|
boardDef;
|
|
4071
4086
|
/** 명세 소비 기록 — 무엇을 선언값으로, 무엇을 기본값으로 계산했나(정직한 자기보고). */
|
|
4072
4087
|
specUse = /* @__PURE__ */ new Map();
|
|
@@ -4348,7 +4363,7 @@ var FlowEngine = class {
|
|
|
4348
4363
|
/* 계획 정지를 이어받는다 — 잃으면 씨앗이 **정비 중인 설비를 가용으로 놓고** 미래를 시뮬레이션한다
|
|
4349
4364
|
(예측이 낙관 쪽으로 치우친다). 씨앗 왕복 대조가 이것을 잡았다. */
|
|
4350
4365
|
...m.held ? { held: true } : {},
|
|
4351
|
-
/* 관측 스냅샷이 들고 있는 것은 관측을 따른다(미러가
|
|
4366
|
+
/* 관측 스냅샷이 들고 있는 것은 관측을 따른다(미러가 트윈 모델에서 읽어 실어 온다). */
|
|
4352
4367
|
...m.properties ? { properties: m.properties } : {},
|
|
4353
4368
|
...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {},
|
|
4354
4369
|
/* 결과도 이어받는다 — 잃으면 예측이 **자격 만료를 모르는 현장**에서 출발한다(낙관 쪽으로 치우친다). */
|
|
@@ -4763,7 +4778,7 @@ var FlowEngine = class {
|
|
|
4763
4778
|
...this.seedDanglingRefs ? { seedDanglingRefs: this.seedDanglingRefs } : {},
|
|
4764
4779
|
...this.materialSpecCrossKeyOverlaps ? { materialSpecCrossKeyOverlaps: this.materialSpecCrossKeyOverlaps } : {}
|
|
4765
4780
|
} } : {},
|
|
4766
|
-
/* 출처 표시 —
|
|
4781
|
+
/* 출처 표시 — 트윈 모델(마스터)에서 온 자리다. 미러는 관측으로 알게 된 자리를 'observed' 로 구별하는데,
|
|
4767
4782
|
* 시뮬이 아무 표시도 안 하면 소비처가 두 스냅샷을 같은 규칙으로 읽지 못한다. */
|
|
4768
4783
|
locations: [...this.locations.values()].map((n) => {
|
|
4769
4784
|
const { status, ...rest } = n;
|
|
@@ -6473,7 +6488,7 @@ var FlowEngine = class {
|
|
|
6473
6488
|
/**
|
|
6474
6489
|
* 시뮬 시각의 분(0..1439) — 캘린더 판정의 기준.
|
|
6475
6490
|
*
|
|
6476
|
-
*
|
|
6491
|
+
* **트윈 모델이 선언한 기준으로 읽는다**(`utcOffsetMinutes`). 예전에는 UTC 로 읽어서 Rosarito(UTC−7)의
|
|
6477
6492
|
* 06시 교대가 7시간 틀렸다 — 시각대만 적고 기준을 안 적으면 반드시 이렇게 된다.
|
|
6478
6493
|
*/
|
|
6479
6494
|
minuteOfDay() {
|
|
@@ -8975,6 +8990,7 @@ var SPECS = {
|
|
|
8975
8990
|
endTime: "string",
|
|
8976
8991
|
allocated: "string[]",
|
|
8977
8992
|
bizTransaction: "string",
|
|
8993
|
+
operationsRequestId: "string",
|
|
8978
8994
|
dockDoor: "string",
|
|
8979
8995
|
windowStartMs: "number",
|
|
8980
8996
|
recordTime: "string"
|
|
@@ -9017,6 +9033,34 @@ var SPECS = {
|
|
|
9017
9033
|
recordTime: "string"
|
|
9018
9034
|
},
|
|
9019
9035
|
enums: { result: ["pass", "fail"] }
|
|
9036
|
+
},
|
|
9037
|
+
/*
|
|
9038
|
+
* **자리의 물리 관측** — 어느 자리의 어느 속성을 언제 얼마로 쟀나.
|
|
9039
|
+
*
|
|
9040
|
+
* 값에 **단위를 함께** 받는다(`ValueType`). 단위 없는 물리량은 판정의 재료가 못 된다 — 3 이 섭씨인지
|
|
9041
|
+
* 화씨인지 모르면 어떤 기준으로도 판정할 수 없다. 다만 **요구하지는 않는다**: 단위를 비우는 실 원본이
|
|
9042
|
+
* 흔하고, 요구하면 그 원본의 관측을 아예 담지 못한다(그때 판정은 커널이 거부한다 — `outsideLimit`).
|
|
9043
|
+
*
|
|
9044
|
+
* `effectiveTime` 도 요구하지 않는다. 없으면 봉투의 시각이 그 자리를 대신한다(§`resolve`) — 원본이
|
|
9045
|
+
* 시각을 말하지 않는 수기 점검이 실재하고, 그때 지어낸 시각보다 폴링 시각이 정직하다.
|
|
9046
|
+
*/
|
|
9047
|
+
observation: {
|
|
9048
|
+
eventType: OP_EVENT.observation,
|
|
9049
|
+
identity: "locationId",
|
|
9050
|
+
/* 자리와 속성 — 둘 중 하나가 없으면 그 관측은 아무 데도 붙지 못한다. */
|
|
9051
|
+
required: ["locationId", "propertyId"],
|
|
9052
|
+
fields: {
|
|
9053
|
+
locationId: "string",
|
|
9054
|
+
propertyId: "string",
|
|
9055
|
+
value: "string",
|
|
9056
|
+
dataType: "string",
|
|
9057
|
+
uom: "string",
|
|
9058
|
+
effectiveTime: "string",
|
|
9059
|
+
effectiveEndTime: "string",
|
|
9060
|
+
recordTime: "string",
|
|
9061
|
+
source: "string",
|
|
9062
|
+
derived: "boolean"
|
|
9063
|
+
}
|
|
9020
9064
|
}
|
|
9021
9065
|
};
|
|
9022
9066
|
function operationalKindOf(record) {
|
|
@@ -9030,6 +9074,7 @@ function operationalKindOf(record) {
|
|
|
9030
9074
|
if (has("taskId")) return "task";
|
|
9031
9075
|
if (has("orderId")) return "order";
|
|
9032
9076
|
if (has("testableObjectId")) return "test";
|
|
9077
|
+
if (has("locationId") && has("propertyId")) return "observation";
|
|
9033
9078
|
return void 0;
|
|
9034
9079
|
}
|
|
9035
9080
|
function isOperationalRecord(record) {
|
|
@@ -9340,13 +9385,13 @@ var VOCABULARY_EXCEPTIONS = [
|
|
|
9340
9385
|
{ token: "moverId", why: "journal wire field \u2014 renaming would mix two keys for one fact across history" },
|
|
9341
9386
|
{ token: "fromNode", why: "journal wire field (task.status payload)" },
|
|
9342
9387
|
{ token: "toNode", why: "journal wire field (task.status payload)" },
|
|
9343
|
-
/* ──
|
|
9388
|
+
/* ── 트윈 모델의 옛 세대 키 — 저장된 데이터라 읽어는 줘야 한다 ────────────────
|
|
9344
9389
|
* 설비 배열의 이름은 세 세대를 거쳤다(movers → equipmentList → equipment). 저장된 보드에는
|
|
9345
|
-
* 셋이 섞여 있고(실측 23개 중 13개가 `movers`), 하나라도 안 읽으면 그
|
|
9390
|
+
* 셋이 섞여 있고(실측 23개 중 13개가 `movers`), 하나라도 안 읽으면 그 트윈 모델은 **설비가 0인 공장**
|
|
9346
9391
|
* 으로 조용히 읽힌다 — 화면의 설비 수가 0이 되고 용량 판정에서 자원이 사라진다. 쓰는 곳은
|
|
9347
9392
|
* `readBoardEquipment` 한 곳뿐이고, 거기서 새 이름으로 정규화해 내보낸다. */
|
|
9348
9393
|
{ token: "movers", why: "legacy board key (movers \u2192 equipmentList \u2192 equipment); read-only normalization in readBoardEquipment, 13 stored boards still use it" },
|
|
9349
|
-
/* ── 씬 컴포넌트 타입 —
|
|
9394
|
+
/* ── 씬 컴포넌트 타입 — 트윈 모델에 저장된 값이고, 뜻이 어긋나지도 않는다 ──────
|
|
9350
9395
|
* 보드 7개가 이 타입으로 컴포넌트를 담고 있어 개명하면 그 컴포넌트가 조용히 안 그려진다.
|
|
9351
9396
|
* 그리고 씬에서 이 이름은 자원이 아니라 **움직임 표현**을 가리킨다(표준과 충돌 아님). */
|
|
9352
9397
|
{ token: "twin-mover", why: "scene component type persisted in boards; names a motion representation, not a resource" },
|
package/package.json
CHANGED