@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/dist-cjs/index.cjs
CHANGED
|
@@ -614,6 +614,27 @@ var OP_EVENT = {
|
|
|
614
614
|
* 없고, 미러가 이어받지 못한다(§상태 ⊆ 이벤트).
|
|
615
615
|
*/
|
|
616
616
|
test: "test.result",
|
|
617
|
+
/**
|
|
618
|
+
* **이 목록이 전부다** — 연결된 시스템이 현재 목록을 한 바퀴 다 보낸 뒤 그것을 알린다.
|
|
619
|
+
*
|
|
620
|
+
* ── 왜 필요한가 (2026-08-25 실측) ────────────────────────────────────────
|
|
621
|
+
* 연결된 시스템은 매 주기 현재 재고를 전부 보낸다. 그런데 트윈은 그것을 낱낱의 관측으로 받아
|
|
622
|
+
* **더하기만 했다.** 「그리고 이것 말고는 없다」를 받는 곳이 없어서, 목록에서 빠진 줄은 아무 말도
|
|
623
|
+
* 오지 않은 것이 되고 트윈에 영원히 남았다.
|
|
624
|
+
*
|
|
625
|
+
* 실측: 실제 재고 1,704건인데 트윈이 10,038건을 갖고 있었다.
|
|
626
|
+
*
|
|
627
|
+
* ── 왜 「사라진 것을 알려 주기」가 아니라 이 방식인가 ──────────────────────
|
|
628
|
+
* 연결 쪽이 앞 주기와 비교해 사라진 줄을 찾아 알리는 방법도 있다. 그런데 그것은 **하나 빠뜨리면
|
|
629
|
+
* 그 줄이 영원히 남는다.** 이 방식은 주기마다 스스로 바로잡는다 — 이미 잘못 쌓인 것도 다음 주기에
|
|
630
|
+
* 사라진다.
|
|
631
|
+
*
|
|
632
|
+
* ── 보내는 쪽이 지킬 것 ─────────────────────────────────────────────────
|
|
633
|
+
* **끝까지 읽었을 때만 보낸다.** 읽다가 끊긴 주기에 이것을 보내면 **살아 있는 재고를 지운다.**
|
|
634
|
+
* 그리고 `since` 는 그 주기를 **시작한** 시각이다(끝낸 시각이 아니다) — 주기 도중에 들어온 관측이
|
|
635
|
+
* 지워지지 않아야 한다.
|
|
636
|
+
*/
|
|
637
|
+
complete: "axis.complete",
|
|
617
638
|
/**
|
|
618
639
|
* 주목 신호 확인(ack) — **사람이 한 행위**라 파생될 수 없다.
|
|
619
640
|
*
|
|
@@ -1394,7 +1415,7 @@ var ObservedReducer = class {
|
|
|
1394
1415
|
corrections = [];
|
|
1395
1416
|
/** 반영하지 못한 사건의 종류별 집계 — 원문은 쌓지 않는다(저널에 이미 있다). */
|
|
1396
1417
|
unhandled = /* @__PURE__ */ new Map();
|
|
1397
|
-
/** 선언된 판정 기준(
|
|
1418
|
+
/** 선언된 판정 기준(트윈 모델에서 한 번 읽는다) — 판정은 선언한 것에만 걸린다. */
|
|
1398
1419
|
testSpecs = /* @__PURE__ */ new Map();
|
|
1399
1420
|
/**
|
|
1400
1421
|
* 자원별 **교대 선언** — 미러가 "지금 근무 중인가" 를 스스로 판정하기 위한 재료.
|
|
@@ -1410,7 +1431,7 @@ var ObservedReducer = class {
|
|
|
1410
1431
|
* 통과시키는 어긋남이 생기고, 그 어긋남은 조용하다.
|
|
1411
1432
|
*/
|
|
1412
1433
|
classDefs = {};
|
|
1413
|
-
/** 시각 해석 기준(
|
|
1434
|
+
/** 시각 해석 기준(트윈 모델 선언) — 없으면 UTC. 캘린더의 `HH:MM` 이 어느 기준인지 정한다. */
|
|
1414
1435
|
utcOffsetMinutes;
|
|
1415
1436
|
constructor(model) {
|
|
1416
1437
|
this.utcOffsetMinutes = model.utcOffsetMinutes;
|
|
@@ -1514,7 +1535,7 @@ var ObservedReducer = class {
|
|
|
1514
1535
|
* 종류는 모르므로 `unknown`, **용량은 비워 둔다**(발명하지 않는다), 출처는 `observed`.
|
|
1515
1536
|
*
|
|
1516
1537
|
* 출처를 표시하는 이유: 소비처가 "마스터가 말한 자리" 와 "관측으로 알게 된 자리" 를 구별해야 한다
|
|
1517
|
-
* (
|
|
1538
|
+
* (트윈 모델에 좌표가 없고, 용량을 채워야 계획에 참여한다). 마스터 동기가 오면 `master` 로 승격된다.
|
|
1518
1539
|
*/
|
|
1519
1540
|
touchLocation(id) {
|
|
1520
1541
|
if (!id || this.master.has(id)) return;
|
|
@@ -1568,6 +1589,8 @@ var ObservedReducer = class {
|
|
|
1568
1589
|
if (this.stale(`task:${d.taskId}`, e)) return;
|
|
1569
1590
|
this.touchLocation(d.fromNode);
|
|
1570
1591
|
this.touchLocation(d.toNode);
|
|
1592
|
+
const prevTask = this.tasks.get(d.taskId);
|
|
1593
|
+
const keptActual = d.materialActual === void 0 ? prevTask?.materialActual : void 0;
|
|
1571
1594
|
this.tasks.set(d.taskId, {
|
|
1572
1595
|
id: d.taskId,
|
|
1573
1596
|
kind: d.kind,
|
|
@@ -1585,8 +1608,9 @@ var ObservedReducer = class {
|
|
|
1585
1608
|
...d.personnel?.length ? { personnel: d.personnel.slice() } : {},
|
|
1586
1609
|
...d.assets?.length ? { assets: d.assets.slice() } : {},
|
|
1587
1610
|
...d.resources?.length ? { resources: d.resources.slice() } : {},
|
|
1588
|
-
/* 실제 자재 이동 — 인원·설비와 같은 채널로 온다(실적을 한 곳에서 읽는다).
|
|
1589
|
-
|
|
1611
|
+
/* 실제 자재 이동 — 인원·설비와 같은 채널로 온다(실적을 한 곳에서 읽는다).
|
|
1612
|
+
말하지 않았으면 **앞의 것을 지킨다**(위 주석) — 빈 배열은 「지웠다」로 말한 것이다. */
|
|
1613
|
+
...d.materialActual?.length ? { materialActual: d.materialActual.map((r) => ({ ...r })) } : keptActual?.length ? { materialActual: keptActual.map((r) => ({ ...r })) } : {},
|
|
1590
1614
|
...d.priority !== void 0 ? { priority: d.priority } : {},
|
|
1591
1615
|
...d.startTime ? { startTime: d.startTime } : {},
|
|
1592
1616
|
...d.endTime ? { endTime: d.endTime } : {}
|
|
@@ -1665,6 +1689,19 @@ var ObservedReducer = class {
|
|
|
1665
1689
|
item.testResults = [...kept, next];
|
|
1666
1690
|
break;
|
|
1667
1691
|
}
|
|
1692
|
+
case OP_EVENT.complete: {
|
|
1693
|
+
const d = e.data;
|
|
1694
|
+
if (d?.completeAxis !== "items") break;
|
|
1695
|
+
const since = Date.parse(String(d.since ?? ""));
|
|
1696
|
+
if (!Number.isFinite(since)) break;
|
|
1697
|
+
let heard = 0;
|
|
1698
|
+
for (const it of this.items.values()) if ((it.seenAtMs ?? -1) >= since) heard++;
|
|
1699
|
+
if (!heard) break;
|
|
1700
|
+
for (const [key, it] of [...this.items.entries()]) {
|
|
1701
|
+
if ((it.seenAtMs ?? -1) < since) this.remove(key);
|
|
1702
|
+
}
|
|
1703
|
+
break;
|
|
1704
|
+
}
|
|
1668
1705
|
case OP_EVENT.attentionAck: {
|
|
1669
1706
|
const d = e.data;
|
|
1670
1707
|
if (d?.id) this.acked.add(d.id);
|
|
@@ -1809,6 +1846,21 @@ var ObservedReducer = class {
|
|
|
1809
1846
|
}
|
|
1810
1847
|
if (ev.action === "DELETE") {
|
|
1811
1848
|
for (const epc of ev.epcList) this.remove(epc);
|
|
1849
|
+
const goneAt = ev.readPoint?.id;
|
|
1850
|
+
for (const qe of ev.quantityList ?? []) {
|
|
1851
|
+
if (!qe?.epcClass) continue;
|
|
1852
|
+
if (goneAt) {
|
|
1853
|
+
const key = `${qe.epcClass}@${goneAt}`;
|
|
1854
|
+
if (envelope && this.stale(`item:${key}`, envelope)) continue;
|
|
1855
|
+
this.remove(key);
|
|
1856
|
+
continue;
|
|
1857
|
+
}
|
|
1858
|
+
for (const key of [...this.items.keys()]) {
|
|
1859
|
+
if (!key.startsWith(`${qe.epcClass}@`)) continue;
|
|
1860
|
+
if (envelope && this.stale(`item:${key}`, envelope)) continue;
|
|
1861
|
+
this.remove(key);
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1812
1864
|
return;
|
|
1813
1865
|
}
|
|
1814
1866
|
const loc = ev.readPoint?.id;
|
|
@@ -1817,7 +1869,9 @@ var ObservedReducer = class {
|
|
|
1817
1869
|
this.touchLocation(loc);
|
|
1818
1870
|
for (const epc of ev.epcList) {
|
|
1819
1871
|
if (envelope && this.stale(`item:${epc}`, envelope)) continue;
|
|
1820
|
-
|
|
1872
|
+
const seen = this.mergeItem(epc, { location: loc, disposition: ev.disposition, ilmd: ev.ilmd }, q, all);
|
|
1873
|
+
const atSeen = Date.parse(String(envelope?.eventTime ?? ""));
|
|
1874
|
+
this.items.set(itemKeyOf(seen), Number.isFinite(atSeen) ? { ...seen, seenAtMs: atSeen } : seen);
|
|
1821
1875
|
}
|
|
1822
1876
|
if (!ev.epcList?.length) {
|
|
1823
1877
|
for (const qe of ev.quantityList ?? []) {
|
|
@@ -1825,7 +1879,9 @@ var ObservedReducer = class {
|
|
|
1825
1879
|
const mine = all.filter((x) => x.epcClass === qe.epcClass);
|
|
1826
1880
|
const subLotId = `${qe.epcClass}@${loc}`;
|
|
1827
1881
|
if (envelope && this.stale(`item:${subLotId}`, envelope)) continue;
|
|
1828
|
-
|
|
1882
|
+
const seenQty = this.mergeItem(qe.epcClass, { location: loc, disposition: ev.disposition, ilmd: ev.ilmd }, qe, mine, subLotId);
|
|
1883
|
+
const atQty = Date.parse(String(envelope?.eventTime ?? ""));
|
|
1884
|
+
this.items.set(subLotId, Number.isFinite(atQty) ? { ...seenQty, seenAtMs: atQty } : seenQty);
|
|
1829
1885
|
}
|
|
1830
1886
|
}
|
|
1831
1887
|
}
|
|
@@ -2062,8 +2118,16 @@ var ObservedReducer = class {
|
|
|
2062
2118
|
origin: n.origin
|
|
2063
2119
|
};
|
|
2064
2120
|
}),
|
|
2065
|
-
/*
|
|
2066
|
-
|
|
2121
|
+
/*
|
|
2122
|
+
* 갖고 있는 것을 전부 내보낸다 — 줄이면 그 자리에서 정보가 사라진다.
|
|
2123
|
+
*
|
|
2124
|
+
* **`seenAtMs` 는 뺀다.** 그것은 물품에 대한 사실이 아니라 **우리가 언제 들었나**이고, 「이 목록이
|
|
2125
|
+
* 전부다」를 받았을 때 무엇을 지울지 가리는 데만 쓴다. 화면이 알 필요가 없고, 상태에 내보내면
|
|
2126
|
+
* 자체 구동에는 없는 값이라 두 구동이 어긋난다(적합성 검사가 그것을 잡았다).
|
|
2127
|
+
*
|
|
2128
|
+
* 중간 저장본에는 남는다 — 재기동 뒤에도 무엇을 지울지 가릴 수 있어야 한다.
|
|
2129
|
+
*/
|
|
2130
|
+
items: [...this.items.values()].map(({ seenAtMs: _seen, ...i }) => ({ ...i })),
|
|
2067
2131
|
/* 유효 기간 판정은 **저장하지 않고 여기서 낸다** — 시뮬과 **같은 함수**(`effectivityAt`)를 부른다.
|
|
2068
2132
|
만료는 이벤트 없이 시각만으로 일어나므로, 델타로 받아 두면 유휴 자원이 영원히 유효하게 남는다. */
|
|
2069
2133
|
persons: [...this.persons.values()].map((p) => ({ ...p, ...this.effectivityPart(p), ...this.offShiftPart(`person:${p.id}`), ...this.capabilityPart(p, `person:${p.id}`, p.personnelClassIds, this.classDefs.personnel) })),
|
|
@@ -4078,7 +4142,7 @@ var FlowEngine = class {
|
|
|
4078
4142
|
materialSpecCrossKeyOverlaps = 0;
|
|
4079
4143
|
observedDirty = false;
|
|
4080
4144
|
observeMode = false;
|
|
4081
|
-
/** 관측 구동이 투영기를 세울 때 필요한 원본
|
|
4145
|
+
/** 관측 구동이 투영기를 세울 때 필요한 원본 트윈 모델(구조는 이벤트가 아니라 마스터에서 온다). */
|
|
4082
4146
|
boardDef;
|
|
4083
4147
|
/** 명세 소비 기록 — 무엇을 선언값으로, 무엇을 기본값으로 계산했나(정직한 자기보고). */
|
|
4084
4148
|
specUse = /* @__PURE__ */ new Map();
|
|
@@ -4360,7 +4424,7 @@ var FlowEngine = class {
|
|
|
4360
4424
|
/* 계획 정지를 이어받는다 — 잃으면 씨앗이 **정비 중인 설비를 가용으로 놓고** 미래를 시뮬레이션한다
|
|
4361
4425
|
(예측이 낙관 쪽으로 치우친다). 씨앗 왕복 대조가 이것을 잡았다. */
|
|
4362
4426
|
...m.held ? { held: true } : {},
|
|
4363
|
-
/* 관측 스냅샷이 들고 있는 것은 관측을 따른다(미러가
|
|
4427
|
+
/* 관측 스냅샷이 들고 있는 것은 관측을 따른다(미러가 트윈 모델에서 읽어 실어 온다). */
|
|
4364
4428
|
...m.properties ? { properties: m.properties } : {},
|
|
4365
4429
|
...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {},
|
|
4366
4430
|
/* 결과도 이어받는다 — 잃으면 예측이 **자격 만료를 모르는 현장**에서 출발한다(낙관 쪽으로 치우친다). */
|
|
@@ -4775,7 +4839,7 @@ var FlowEngine = class {
|
|
|
4775
4839
|
...this.seedDanglingRefs ? { seedDanglingRefs: this.seedDanglingRefs } : {},
|
|
4776
4840
|
...this.materialSpecCrossKeyOverlaps ? { materialSpecCrossKeyOverlaps: this.materialSpecCrossKeyOverlaps } : {}
|
|
4777
4841
|
} } : {},
|
|
4778
|
-
/* 출처 표시 —
|
|
4842
|
+
/* 출처 표시 — 트윈 모델(마스터)에서 온 자리다. 미러는 관측으로 알게 된 자리를 'observed' 로 구별하는데,
|
|
4779
4843
|
* 시뮬이 아무 표시도 안 하면 소비처가 두 스냅샷을 같은 규칙으로 읽지 못한다. */
|
|
4780
4844
|
locations: [...this.locations.values()].map((n) => {
|
|
4781
4845
|
const { status, ...rest } = n;
|
|
@@ -6485,7 +6549,7 @@ var FlowEngine = class {
|
|
|
6485
6549
|
/**
|
|
6486
6550
|
* 시뮬 시각의 분(0..1439) — 캘린더 판정의 기준.
|
|
6487
6551
|
*
|
|
6488
|
-
*
|
|
6552
|
+
* **트윈 모델이 선언한 기준으로 읽는다**(`utcOffsetMinutes`). 예전에는 UTC 로 읽어서 Rosarito(UTC−7)의
|
|
6489
6553
|
* 06시 교대가 7시간 틀렸다 — 시각대만 적고 기준을 안 적으면 반드시 이렇게 된다.
|
|
6490
6554
|
*/
|
|
6491
6555
|
minuteOfDay() {
|
|
@@ -9030,6 +9094,46 @@ var SPECS = {
|
|
|
9030
9094
|
recordTime: "string"
|
|
9031
9095
|
},
|
|
9032
9096
|
enums: { result: ["pass", "fail"] }
|
|
9097
|
+
},
|
|
9098
|
+
/*
|
|
9099
|
+
* **자리의 물리 관측** — 어느 자리의 어느 속성을 언제 얼마로 쟀나.
|
|
9100
|
+
*
|
|
9101
|
+
* 값에 **단위를 함께** 받는다(`ValueType`). 단위 없는 물리량은 판정의 재료가 못 된다 — 3 이 섭씨인지
|
|
9102
|
+
* 화씨인지 모르면 어떤 기준으로도 판정할 수 없다. 다만 **요구하지는 않는다**: 단위를 비우는 실 원본이
|
|
9103
|
+
* 흔하고, 요구하면 그 원본의 관측을 아예 담지 못한다(그때 판정은 커널이 거부한다 — `outsideLimit`).
|
|
9104
|
+
*
|
|
9105
|
+
* `effectiveTime` 도 요구하지 않는다. 없으면 봉투의 시각이 그 자리를 대신한다(§`resolve`) — 원본이
|
|
9106
|
+
* 시각을 말하지 않는 수기 점검이 실재하고, 그때 지어낸 시각보다 폴링 시각이 정직하다.
|
|
9107
|
+
*/
|
|
9108
|
+
/*
|
|
9109
|
+
* **이 목록이 전부다** — 연결된 시스템이 현재 목록을 한 바퀴 다 보낸 뒤 알린다.
|
|
9110
|
+
*
|
|
9111
|
+
* 둘 다 요구한다: 어느 목록인지와 그 주기를 **시작한** 시각. 시각이 없으면 무엇을 지울지 가릴 수
|
|
9112
|
+
* 없고, 그때 지우면 전부 지운다 — 받지 않는 것이 옳다.
|
|
9113
|
+
*/
|
|
9114
|
+
complete: {
|
|
9115
|
+
eventType: OP_EVENT.complete,
|
|
9116
|
+
identity: "completeAxis",
|
|
9117
|
+
required: ["completeAxis", "since"],
|
|
9118
|
+
fields: { completeAxis: "string", since: "string", recordTime: "string" }
|
|
9119
|
+
},
|
|
9120
|
+
observation: {
|
|
9121
|
+
eventType: OP_EVENT.observation,
|
|
9122
|
+
identity: "locationId",
|
|
9123
|
+
/* 자리와 속성 — 둘 중 하나가 없으면 그 관측은 아무 데도 붙지 못한다. */
|
|
9124
|
+
required: ["locationId", "propertyId"],
|
|
9125
|
+
fields: {
|
|
9126
|
+
locationId: "string",
|
|
9127
|
+
propertyId: "string",
|
|
9128
|
+
value: "string",
|
|
9129
|
+
dataType: "string",
|
|
9130
|
+
uom: "string",
|
|
9131
|
+
effectiveTime: "string",
|
|
9132
|
+
effectiveEndTime: "string",
|
|
9133
|
+
recordTime: "string",
|
|
9134
|
+
source: "string",
|
|
9135
|
+
derived: "boolean"
|
|
9136
|
+
}
|
|
9033
9137
|
}
|
|
9034
9138
|
};
|
|
9035
9139
|
function operationalKindOf(record) {
|
|
@@ -9043,6 +9147,8 @@ function operationalKindOf(record) {
|
|
|
9043
9147
|
if (has("taskId")) return "task";
|
|
9044
9148
|
if (has("orderId")) return "order";
|
|
9045
9149
|
if (has("testableObjectId")) return "test";
|
|
9150
|
+
if (has("locationId") && has("propertyId")) return "observation";
|
|
9151
|
+
if (has("completeAxis")) return "complete";
|
|
9046
9152
|
return void 0;
|
|
9047
9153
|
}
|
|
9048
9154
|
function isOperationalRecord(record) {
|
|
@@ -9353,13 +9459,13 @@ var VOCABULARY_EXCEPTIONS = [
|
|
|
9353
9459
|
{ token: "moverId", why: "journal wire field \u2014 renaming would mix two keys for one fact across history" },
|
|
9354
9460
|
{ token: "fromNode", why: "journal wire field (task.status payload)" },
|
|
9355
9461
|
{ token: "toNode", why: "journal wire field (task.status payload)" },
|
|
9356
|
-
/* ──
|
|
9462
|
+
/* ── 트윈 모델의 옛 세대 키 — 저장된 데이터라 읽어는 줘야 한다 ────────────────
|
|
9357
9463
|
* 설비 배열의 이름은 세 세대를 거쳤다(movers → equipmentList → equipment). 저장된 보드에는
|
|
9358
|
-
* 셋이 섞여 있고(실측 23개 중 13개가 `movers`), 하나라도 안 읽으면 그
|
|
9464
|
+
* 셋이 섞여 있고(실측 23개 중 13개가 `movers`), 하나라도 안 읽으면 그 트윈 모델은 **설비가 0인 공장**
|
|
9359
9465
|
* 으로 조용히 읽힌다 — 화면의 설비 수가 0이 되고 용량 판정에서 자원이 사라진다. 쓰는 곳은
|
|
9360
9466
|
* `readBoardEquipment` 한 곳뿐이고, 거기서 새 이름으로 정규화해 내보낸다. */
|
|
9361
9467
|
{ token: "movers", why: "legacy board key (movers \u2192 equipmentList \u2192 equipment); read-only normalization in readBoardEquipment, 13 stored boards still use it" },
|
|
9362
|
-
/* ── 씬 컴포넌트 타입 —
|
|
9468
|
+
/* ── 씬 컴포넌트 타입 — 트윈 모델에 저장된 값이고, 뜻이 어긋나지도 않는다 ──────
|
|
9363
9469
|
* 보드 7개가 이 타입으로 컴포넌트를 담고 있어 개명하면 그 컴포넌트가 조용히 안 그려진다.
|
|
9364
9470
|
* 그리고 씬에서 이 이름은 자원이 아니라 **움직임 표현**을 가리킨다(표준과 충돌 아님). */
|
|
9365
9471
|
{ token: "twin-mover", why: "scene component type persisted in boards; names a motion representation, not a resource" },
|
package/package.json
CHANGED