@operato/twin-kernel 0.7.54 → 0.7.56
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/dist/contract.d.ts +179 -4
- package/dist/contract.js +104 -1
- package/dist/epcis.d.ts +61 -0
- package/dist/epcis.js +63 -2
- package/dist/flow-engine.d.ts +37 -1
- package/dist/flow-engine.js +115 -2
- package/dist/forecast.d.ts +17 -0
- package/dist/forecast.js +22 -6
- package/dist/observed-reducer.d.ts +8 -1
- package/dist/observed-reducer.js +31 -0
- package/dist-cjs/index.cjs +251 -10
- package/package.json +1 -1
package/dist-cjs/index.cjs
CHANGED
|
@@ -96,6 +96,7 @@ __export(index_exports, {
|
|
|
96
96
|
constantDuration: () => constantDuration,
|
|
97
97
|
conversionFactorOf: () => conversionFactorOf,
|
|
98
98
|
counterfactualAt: () => counterfactualAt,
|
|
99
|
+
criterionSaysNothing: () => criterionSaysNothing,
|
|
99
100
|
demandWindowStart: () => demandWindowStart,
|
|
100
101
|
deriveAttentions: () => deriveAttentions,
|
|
101
102
|
documentPath: () => documentPath,
|
|
@@ -142,10 +143,12 @@ __export(index_exports, {
|
|
|
142
143
|
monteCarloForecastAsync: () => monteCarloForecastAsync,
|
|
143
144
|
objectEvent: () => objectEvent,
|
|
144
145
|
objectUri: () => objectUri,
|
|
146
|
+
observationAt: () => observationAt,
|
|
145
147
|
offCalendarAt: () => offCalendarAt,
|
|
146
148
|
offCalendarReasonAt: () => offCalendarReasonAt,
|
|
147
149
|
operationalKindOf: () => operationalKindOf,
|
|
148
150
|
operationsCapabilityOf: () => operationsCapabilityOf,
|
|
151
|
+
outsideLimit: () => outsideLimit,
|
|
149
152
|
parseEpc: () => parseEpc,
|
|
150
153
|
parseIsoDuration: () => parseIsoDuration,
|
|
151
154
|
partialFitPolicy: () => partialFitPolicy,
|
|
@@ -290,6 +293,28 @@ function meetsTests(required, results, at) {
|
|
|
290
293
|
}
|
|
291
294
|
return true;
|
|
292
295
|
}
|
|
296
|
+
function criterionSaysNothing(c) {
|
|
297
|
+
if (c?.expression?.trim()) return false;
|
|
298
|
+
const l = c?.limit;
|
|
299
|
+
return !(typeof l?.minimum === "number" || typeof l?.maximum === "number");
|
|
300
|
+
}
|
|
301
|
+
function outsideLimit(criterion, observed) {
|
|
302
|
+
const l = criterion?.limit;
|
|
303
|
+
if (!l) return void 0;
|
|
304
|
+
const hasMin = typeof l.minimum === "number";
|
|
305
|
+
const hasMax = typeof l.maximum === "number";
|
|
306
|
+
if (!hasMin && !hasMax) return void 0;
|
|
307
|
+
const raw = observed?.value;
|
|
308
|
+
if (raw === void 0 || raw === null || String(raw).trim() === "") return void 0;
|
|
309
|
+
const n = Number(raw);
|
|
310
|
+
if (!Number.isFinite(n)) return void 0;
|
|
311
|
+
const lu = l.uom?.trim();
|
|
312
|
+
const ou = observed?.uom?.trim();
|
|
313
|
+
if (lu && ou && lu !== ou) return void 0;
|
|
314
|
+
if (hasMin && n < l.minimum) return true;
|
|
315
|
+
if (hasMax && n > l.maximum) return true;
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
293
318
|
function testEvidenceGaps(spec, result) {
|
|
294
319
|
const criteria = spec?.criteria ?? [];
|
|
295
320
|
const measurements = result?.propertyMeasurements ?? [];
|
|
@@ -532,6 +557,26 @@ function capabilityOf(r, ctx) {
|
|
|
532
557
|
if (r.status && r.status !== "idle" && r.status !== "available") return { available: false, reason: "working" };
|
|
533
558
|
return { available: true, reason: "available" };
|
|
534
559
|
}
|
|
560
|
+
function observationAt(observations, propertyId, at) {
|
|
561
|
+
const t = parsedMs(at);
|
|
562
|
+
if (!(t >= 0)) return void 0;
|
|
563
|
+
let best;
|
|
564
|
+
let bestStart = -1;
|
|
565
|
+
for (const o of observations ?? []) {
|
|
566
|
+
if (o.propertyId !== propertyId) continue;
|
|
567
|
+
const from = parsedMs(o.effectiveTime);
|
|
568
|
+
if (!(from >= 0) || from > t) continue;
|
|
569
|
+
if (o.effectiveEndTime) {
|
|
570
|
+
const to = parsedMs(o.effectiveEndTime);
|
|
571
|
+
if (to >= 0 && to < t) continue;
|
|
572
|
+
}
|
|
573
|
+
if (from > bestStart) {
|
|
574
|
+
best = o;
|
|
575
|
+
bestStart = from;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return best;
|
|
579
|
+
}
|
|
535
580
|
var OP_EVENT = {
|
|
536
581
|
task: "task.status",
|
|
537
582
|
equipment: "equipment.status",
|
|
@@ -554,7 +599,19 @@ var OP_EVENT = {
|
|
|
554
599
|
* "확인해라"(요청)이고 이벤트는 "확인했다"(사실)다. 같은 문자열을 쓰면 저널에서 요청과 사실이
|
|
555
600
|
* 구별되지 않는다.
|
|
556
601
|
*/
|
|
557
|
-
attentionAck: "attention.acked"
|
|
602
|
+
attentionAck: "attention.acked",
|
|
603
|
+
/**
|
|
604
|
+
* **자리에서 관측된 물리량** — 냉장실 온도·습도, 세척수 유량 같은 것(§`LocationObservation`).
|
|
605
|
+
*
|
|
606
|
+
* ── 왜 채널이 필요한가 ────────────────────────────────────────────────────
|
|
607
|
+
* `LocationState.observations` 를 상태에 두었는데 그것을 낳는 사건이 없었다. 그러면 **상태 ⊆ 이벤트**
|
|
608
|
+
* 가 깨진다: 채워도 재기동에서 사라지고, 폴드가 되살릴 수 없고, 미러가 이어받을 수도 없다. 상태에만
|
|
609
|
+
* 있는 축은 조용히 사라지는 축이다.
|
|
610
|
+
*
|
|
611
|
+
* 이름을 `energy.measured` 와 같은 결로 둔다 — 그쪽이 에너지 계량의 도착이고 이쪽이 그 일반형이다.
|
|
612
|
+
* 두 채널을 합치지 않는 이유는 에너지 쪽이 **구간에 누적되는 표본**이라 처리가 다르기 때문이다.
|
|
613
|
+
*/
|
|
614
|
+
observation: "location.measured"
|
|
558
615
|
};
|
|
559
616
|
var ENERGY_EVENT = {
|
|
560
617
|
/** 계량 도착 — 그 시점의 유효전력·누적량. 15분 수요 구간에 누적된다. */
|
|
@@ -825,13 +882,16 @@ function clockOf2(twin) {
|
|
|
825
882
|
function monteCarloForecast(twin, opts) {
|
|
826
883
|
const now = clockOf2(twin);
|
|
827
884
|
const samples = [];
|
|
885
|
+
let gaps = 0;
|
|
828
886
|
for (let i = 0; i < opts.runs; i++) {
|
|
829
887
|
const { fc, step, target } = startRun(twin, opts, now, i);
|
|
830
888
|
let guard = 0;
|
|
831
889
|
while (clockOf2(fc) < target && guard++ < 1e6) fc.tick(step);
|
|
832
|
-
|
|
890
|
+
const snap = fc.getSnapshot();
|
|
891
|
+
gaps = Math.max(gaps, snap.stepsWithoutMaterial ?? 0);
|
|
892
|
+
samples.push(opts.metric(snap));
|
|
833
893
|
}
|
|
834
|
-
return summarize(opts.runs, samples);
|
|
894
|
+
return summarize(opts.runs, samples, gaps);
|
|
835
895
|
}
|
|
836
896
|
function startRun(twin, opts, nowMs, i) {
|
|
837
897
|
const fc = twin.fork();
|
|
@@ -844,6 +904,7 @@ async function monteCarloForecastAsync(twin, opts) {
|
|
|
844
904
|
const yieldFn = opts.yieldFn ?? (() => Promise.resolve());
|
|
845
905
|
const everyTicks = Math.max(1, opts.yieldEveryTicks ?? 50);
|
|
846
906
|
const samples = [];
|
|
907
|
+
let gaps = 0;
|
|
847
908
|
for (let i = 0; i < opts.runs; i++) {
|
|
848
909
|
const { fc, step, target } = startRun(twin, opts, now, i);
|
|
849
910
|
let guard = 0;
|
|
@@ -855,16 +916,28 @@ async function monteCarloForecastAsync(twin, opts) {
|
|
|
855
916
|
await yieldFn();
|
|
856
917
|
}
|
|
857
918
|
}
|
|
858
|
-
|
|
919
|
+
const snap = fc.getSnapshot();
|
|
920
|
+
gaps = Math.max(gaps, snap.stepsWithoutMaterial ?? 0);
|
|
921
|
+
samples.push(opts.metric(snap));
|
|
859
922
|
if (i < opts.runs - 1) await yieldFn();
|
|
860
923
|
}
|
|
861
|
-
return summarize(opts.runs, samples);
|
|
924
|
+
return summarize(opts.runs, samples, gaps);
|
|
862
925
|
}
|
|
863
|
-
function summarize(runs, samples) {
|
|
926
|
+
function summarize(runs, samples, stepsWithoutMaterial = 0) {
|
|
864
927
|
const sorted = [...samples].sort((a, b) => a - b);
|
|
865
928
|
const pct = (p) => sorted[Math.min(sorted.length - 1, Math.floor(p * sorted.length))];
|
|
866
929
|
const mean = samples.reduce((a, b) => a + b, 0) / (samples.length || 1);
|
|
867
|
-
return {
|
|
930
|
+
return {
|
|
931
|
+
runs,
|
|
932
|
+
samples,
|
|
933
|
+
min: sorted[0],
|
|
934
|
+
max: sorted[sorted.length - 1],
|
|
935
|
+
mean,
|
|
936
|
+
p50: pct(0.5),
|
|
937
|
+
p90: pct(0.9),
|
|
938
|
+
/* 0 은 싣지 않는다 — 「구멍이 없다」와 「이 축을 모른다」를 화면이 구별할 수 있게. */
|
|
939
|
+
...stepsWithoutMaterial > 0 ? { stepsWithoutMaterial } : {}
|
|
940
|
+
};
|
|
868
941
|
}
|
|
869
942
|
|
|
870
943
|
// src/twin-observer.ts
|
|
@@ -910,14 +983,75 @@ var DISP = {
|
|
|
910
983
|
sellable: "urn:epcglobal:cbv:disp:sellable_accessible",
|
|
911
984
|
reserved: "urn:epcglobal:cbv:disp:reserved",
|
|
912
985
|
in_transit: "urn:epcglobal:cbv:disp:in_transit",
|
|
913
|
-
non_sellable: "urn:epcglobal:cbv:disp:non_sellable_other"
|
|
986
|
+
non_sellable: "urn:epcglobal:cbv:disp:non_sellable_other",
|
|
914
987
|
// 불량/scrap
|
|
988
|
+
/**
|
|
989
|
+
* **기한이 지났다** — CBV `expired`.
|
|
990
|
+
*
|
|
991
|
+
* ── 왜 `non_sellable` 로 접지 않나 (2026-08-24) ─────────────────────────────
|
|
992
|
+
* 커널의 `non_sellable` 은 CBV 의 `non_sellable_other`, 즉 **「그 밖의 이유」**다. 기한 지남을 거기
|
|
993
|
+
* 넣으면 「기한이 지나 못 판다」와 「깨져서 못 판다」가 같은 값이 되고, 화면은 회수·폐기의 사유를
|
|
994
|
+
* 구별할 수 없다. 식품에서 그 둘은 다른 조치다.
|
|
995
|
+
*
|
|
996
|
+
* 그리고 표준에 **정확한 낱말이 있다** — 접는 것은 있는 낱말을 버리는 것이다.
|
|
997
|
+
*
|
|
998
|
+
* ── 기한 날짜와 다른 축이다 ────────────────────────────────────────────────
|
|
999
|
+
* `ItemState.expiry` 는 **날짜**이고 이것은 **상태**다. 날짜가 있으면 「지났나」는 파생이지만, 원본이
|
|
1000
|
+
* 「기한 지남」을 상태로 선언하는 시스템이 있다 — 그때 이 값은 관측이다.
|
|
1001
|
+
*
|
|
1002
|
+
* 둘이 어긋나면(날짜는 남았는데 상태가 지남, 또는 그 반대) **어느 쪽이 맞다고 정하지 않는다** —
|
|
1003
|
+
* 아직 그 판정을 세울 근거가 없다. 어긋남의 구분을 없애지 않는 것이 지금의 규율이다.
|
|
1004
|
+
*
|
|
1005
|
+
* ── 원문으로 확인했다 (2026-08-24) ────────────────────────────────────────
|
|
1006
|
+
* 1차 출처: **CBV Standard Release 2.0, Ratified Jun 2022** §7.2.3 처분 값 표(38개). 이 객체의
|
|
1007
|
+
* 다른 값들(`in_progress`·`sellable_accessible`·`reserved`·`in_transit`·`non_sellable_other`)도
|
|
1008
|
+
* 그 표에 있다.
|
|
1009
|
+
*
|
|
1010
|
+
* **`non_sellable_expired` 를 쓰지 않는 이유**: 그 값은 CBV 1.0 의 것이고 표준이 **폐기**했다 —
|
|
1011
|
+
* 「deprecated in favour of new disposition values expired, damaged, disposed, … introduced in
|
|
1012
|
+
* CBV 1.1」. 폐기된 값을 쓰면 새 소비처가 읽지 못한다.
|
|
1013
|
+
*
|
|
1014
|
+
* 참고: GS1 어휘 등록처(`ref.gs1.org/cbv/…`)로는 확인할 수 없었다 — **없는 값에도 같은 응답**을
|
|
1015
|
+
* 준다(지어낸 값의 JSON-LD 가 실재 값과 바이트까지 같았다). 그 경로를 근거로 삼지 말 것.
|
|
1016
|
+
*/
|
|
1017
|
+
expired: "urn:epcglobal:cbv:disp:expired",
|
|
1018
|
+
/**
|
|
1019
|
+
* **검사에 합격했다 / 불합격했다** — CBV `conformant` / `non_conformant`.
|
|
1020
|
+
*
|
|
1021
|
+
* 1차 출처(CBV 2.0 §7.2.3) 정의 그대로다.
|
|
1022
|
+
*
|
|
1023
|
+
* conformant Outcome of a successful/passed inspection in an inspecting or repairing step
|
|
1024
|
+
* non_conformant Outcome of an unsuccessful/failed inspection in an inspecting or repairing step
|
|
1025
|
+
*
|
|
1026
|
+
* ── 왜 시험 결과 축을 자원에 더하지 않고 이것을 쓰나 (2026-08-24) ────────────
|
|
1027
|
+
* 로트의 검사 판정을 담을 자리를 찾다가 `ItemState.testResults` 를 더하려 했다. 그런데 표준은 그
|
|
1028
|
+
* 사실을 **이미 처분으로 말한다**: `bizStep: inspecting` 사건에 이 처분이 붙는다.
|
|
1029
|
+
*
|
|
1030
|
+
* 처분을 쓰면 두 가지가 공짜로 성립한다.
|
|
1031
|
+
* ① **상태 ⊆ 이벤트** — 처분은 이미 사건에서 온다. 상태에만 있는 축을 만들지 않는다
|
|
1032
|
+
* ② **운영에 곧 닿는다** — 「이 자재를 쓸 수 있나」가 처분으로 답해진다(판정을 따로 읽지 않는다)
|
|
1033
|
+
*
|
|
1034
|
+
* 시험의 **자세한 내용**(어느 명세로, 무엇을 재어)은 다른 물음이고, 표준은 그것을 `TestResult` 로
|
|
1035
|
+
* 두며 결과가 대상을 가리킨다(`TestableObjectID`) — 대상이 결과를 들지 않는다. 그 축이 필요해지면
|
|
1036
|
+
* 그때 열되, **판정 자체는 여기서 끝난다.**
|
|
1037
|
+
*/
|
|
1038
|
+
conformant: "urn:epcglobal:cbv:disp:conformant",
|
|
1039
|
+
non_conformant: "urn:epcglobal:cbv:disp:non_conformant"
|
|
915
1040
|
};
|
|
916
1041
|
var CBV_BIZSTEP = {
|
|
917
1042
|
/** 공정에 자재가 들어갔다 — ISA-95 `MaterialUse: Consumed`. */
|
|
918
1043
|
consuming: "urn:epcglobal:cbv:bizstep:consuming",
|
|
919
1044
|
/** 새 물품이 생겨 계보가 시작된다 — ISA-95 `MaterialUse: Produced`. */
|
|
920
|
-
commissioning: "urn:epcglobal:cbv:bizstep:commissioning"
|
|
1045
|
+
commissioning: "urn:epcglobal:cbv:bizstep:commissioning",
|
|
1046
|
+
/**
|
|
1047
|
+
* **검사** — CBV `inspecting`. 1차 출처(CBV 2.0) 정의: 「Process of reviewing objects to address
|
|
1048
|
+
* potential physical or documentation defects」이고, 「표본과 달리 검사된 대상은 그대로 남는다」고
|
|
1049
|
+
* 이어진다(즉 검사는 물건을 소비하지 않는다).
|
|
1050
|
+
*
|
|
1051
|
+
* 이 단계에 `DISP.conformant`/`DISP.non_conformant` 가 붙어 판정이 처분으로 남는다 — 입고검수·
|
|
1052
|
+
* 공정 중 검사가 그 모양이다.
|
|
1053
|
+
*/
|
|
1054
|
+
inspecting: "urn:epcglobal:cbv:bizstep:inspecting"
|
|
921
1055
|
};
|
|
922
1056
|
function ssccUri(companyPrefix, serial) {
|
|
923
1057
|
return `urn:epc:id:sscc:${companyPrefix}.${String(serial).padStart(10, "0")}`;
|
|
@@ -1195,6 +1329,8 @@ function effectiveOf(r) {
|
|
|
1195
1329
|
var ObservedReducer = class {
|
|
1196
1330
|
/** 로케이션 마스터 — 출처를 함께 들고 있다(마스터가 말한 자리 vs 관측으로 알게 된 자리). */
|
|
1197
1331
|
master = /* @__PURE__ */ new Map();
|
|
1332
|
+
/** 자리별 · 속성별 **마지막 관측** — 이력이 아니다(§`OP_EVENT.observation`). */
|
|
1333
|
+
observations = /* @__PURE__ */ new Map();
|
|
1198
1334
|
items = /* @__PURE__ */ new Map();
|
|
1199
1335
|
aggregation = /* @__PURE__ */ new Map();
|
|
1200
1336
|
/** 아직 관측되지 않은 자식의 담김 — 물품을 지어내지 않고 보류했다가 등장할 때 붙인다. */
|
|
@@ -1463,6 +1599,16 @@ var ObservedReducer = class {
|
|
|
1463
1599
|
this.touchLocation(d.location);
|
|
1464
1600
|
break;
|
|
1465
1601
|
}
|
|
1602
|
+
case OP_EVENT.observation: {
|
|
1603
|
+
const d = e.data;
|
|
1604
|
+
if (!d?.locationId || !d?.propertyId) break;
|
|
1605
|
+
if (this.stale(`observation:${d.locationId}:${d.propertyId}`, e)) return;
|
|
1606
|
+
this.touchLocation(d.locationId);
|
|
1607
|
+
const bin = this.observations.get(d.locationId) ?? /* @__PURE__ */ new Map();
|
|
1608
|
+
bin.set(d.propertyId, { ...d });
|
|
1609
|
+
this.observations.set(d.locationId, bin);
|
|
1610
|
+
break;
|
|
1611
|
+
}
|
|
1466
1612
|
case OP_EVENT.attentionAck: {
|
|
1467
1613
|
const d = e.data;
|
|
1468
1614
|
if (d?.id) this.acked.add(d.id);
|
|
@@ -1785,6 +1931,8 @@ var ObservedReducer = class {
|
|
|
1785
1931
|
*/
|
|
1786
1932
|
serialize() {
|
|
1787
1933
|
return {
|
|
1934
|
+
/* 관측도 재개점에 든다 — 없으면 이어 접기가 그 축을 0부터 다시 만든다. */
|
|
1935
|
+
observations: [...this.observations.entries()].map(([id, bin]) => ({ id, values: [...bin.values()] })),
|
|
1788
1936
|
revision: this.revision,
|
|
1789
1937
|
master: [...this.master.values()].map((n) => ({ ...n })),
|
|
1790
1938
|
items: [...this.items.values()].map((i) => ({ ...i })),
|
|
@@ -1809,6 +1957,9 @@ var ObservedReducer = class {
|
|
|
1809
1957
|
* 모델에서 오는 판정 입력(교대·등급·시간대)은 생성자가 이미 세웠으므로 건드리지 않는다.
|
|
1810
1958
|
*/
|
|
1811
1959
|
restore(cp) {
|
|
1960
|
+
this.observations = new Map(
|
|
1961
|
+
(cp?.observations ?? []).map((o) => [o.id, new Map((o.values ?? []).map((v) => [v.propertyId, { ...v }]))])
|
|
1962
|
+
);
|
|
1812
1963
|
this.revision = cp?.revision ?? 0;
|
|
1813
1964
|
this.master = new Map((cp?.master ?? []).map((n) => [n.id, { ...n }]));
|
|
1814
1965
|
this.items = new Map((cp?.items ?? []).map((i) => [i.epc, { ...i }]));
|
|
@@ -1844,6 +1995,11 @@ var ObservedReducer = class {
|
|
|
1844
1995
|
...n.capacity === void 0 ? {} : { capacity: n.capacity },
|
|
1845
1996
|
...n.parallelism === void 0 ? {} : { parallelism: n.parallelism },
|
|
1846
1997
|
...n.parentId ? { parentId: n.parentId } : {},
|
|
1998
|
+
/* 관측된 물리량 — 없으면 **키를 만들지 않는다**(빈 배열은 「센서가 없다」와 「못 들었다」를 같게 만든다). */
|
|
1999
|
+
...(() => {
|
|
2000
|
+
const bin = this.observations.get(n.id);
|
|
2001
|
+
return bin?.size ? { observations: [...bin.values()] } : {};
|
|
2002
|
+
})(),
|
|
1847
2003
|
origin: n.origin
|
|
1848
2004
|
};
|
|
1849
2005
|
}),
|
|
@@ -3360,6 +3516,34 @@ function deriveAttentions(view, acked, nowIso, thresholds) {
|
|
|
3360
3516
|
}
|
|
3361
3517
|
}
|
|
3362
3518
|
for (const n of view.locations) {
|
|
3519
|
+
for (const c of n.criteria ?? []) {
|
|
3520
|
+
const propertyId = c.evaluatedPropertyId;
|
|
3521
|
+
if (!propertyId) continue;
|
|
3522
|
+
const observed = (n.observations ?? []).find((o) => o.propertyId === propertyId);
|
|
3523
|
+
if (!observed) continue;
|
|
3524
|
+
if (outsideLimit(c, observed) !== true) continue;
|
|
3525
|
+
out.push({
|
|
3526
|
+
/* 자리·기준마다 하나 — 같은 방의 온도와 습도가 한 신호로 뭉치지 않는다. */
|
|
3527
|
+
id: `observation-out-of-limit:${n.id}:${c.id}`,
|
|
3528
|
+
kind: "observation-out-of-limit",
|
|
3529
|
+
severity: "high",
|
|
3530
|
+
anchor: { locationId: n.id },
|
|
3531
|
+
/* 언어중립 원시값만 — 문장은 표현계층이 kind 로 골라 렌더한다. */
|
|
3532
|
+
params: {
|
|
3533
|
+
locationId: n.id,
|
|
3534
|
+
criterionId: c.id,
|
|
3535
|
+
propertyId,
|
|
3536
|
+
/* 값이 없으면 판정이 서지 않으므로 여기 오지 않는다 — 그래도 타입을 좁혀 둔다. */
|
|
3537
|
+
value: observed.value ?? "",
|
|
3538
|
+
...observed.uom ? { uom: observed.uom } : {},
|
|
3539
|
+
...c.limit?.minimum !== void 0 ? { minimum: c.limit.minimum } : {},
|
|
3540
|
+
...c.limit?.maximum !== void 0 ? { maximum: c.limit.maximum } : {},
|
|
3541
|
+
/* **언제의 값인가** — 4개월 전 값으로 지금을 판정한 것인지 소비처가 알아야 한다. */
|
|
3542
|
+
effectiveTime: observed.effectiveTime,
|
|
3543
|
+
...observed.derived ? { derived: "true" } : {}
|
|
3544
|
+
}
|
|
3545
|
+
});
|
|
3546
|
+
}
|
|
3363
3547
|
if ((n.capacity ?? 0) > 0) {
|
|
3364
3548
|
const r = (n.occupancy ?? 0) / n.capacity;
|
|
3365
3549
|
const declaredPct = thresholds?.congestionPctOf?.(n.id);
|
|
@@ -3740,6 +3924,47 @@ var FlowEngine = class {
|
|
|
3740
3924
|
localParams = /* @__PURE__ */ new Map();
|
|
3741
3925
|
/** 관측 구동(P0) — 이벤트를 접는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
|
|
3742
3926
|
observer;
|
|
3927
|
+
/**
|
|
3928
|
+
* **관측 리듀서의 재개점을 꺼낸다** — 호스트가 저장해 다음 기동에서 되돌릴 수 있게.
|
|
3929
|
+
*
|
|
3930
|
+
* ── 무엇이 문제였나 (2026-08-24 실측) ──────────────────────────────────────
|
|
3931
|
+
* 미러는 재기동마다 저널을 **0부터** 다시 집계했다. 실측으로 저널이 2,960만 줄이고, 그 때문에 기동
|
|
3932
|
+
* 직후 몇 분간 상태가 비어 있었다. 계측처럼 경계 없이 자라는 흐름이 들어오면 그 몇 분이 몇십 분이
|
|
3933
|
+
* 된다 — 불편이 아니라 벽이다.
|
|
3934
|
+
*
|
|
3935
|
+
* 재개점 자체는 오래전부터 있었다(`serialize`/`restore`). 조회 경로는 그것을 쓰는데
|
|
3936
|
+
* (`replayFrom`) **라이브 경로에는 꺼낼 문이 없었다.** 그래서 호스트가 저장할 수 없었다.
|
|
3937
|
+
*
|
|
3938
|
+
* ── 상태 스냅샷으로는 대신할 수 없다 ──────────────────────────────────────
|
|
3939
|
+
* 리듀서는 소비처가 보는 값 말고도 든다: 부모를 기다리는 담김·집계 중인 수량·담을 줄 몰라 세어 둔
|
|
3940
|
+
* 사건. 상태만 되돌리고 이어 집계하면 **0부터 집계한 결과와 조용히 달라진다**(§`ReducerCheckpoint`).
|
|
3941
|
+
*
|
|
3942
|
+
* 관측 구동이 아니면 `undefined` — 시뮬은 리듀서를 갖지 않는다(저장할 것이 없다).
|
|
3943
|
+
*/
|
|
3944
|
+
observedCheckpoint() {
|
|
3945
|
+
return this.observer?.serialize();
|
|
3946
|
+
}
|
|
3947
|
+
/**
|
|
3948
|
+
* **재개점에서 관측 리듀서를 되세운다** — 저널을 0부터 다시 집계하지 않게.
|
|
3949
|
+
*
|
|
3950
|
+
* 리듀서가 아직 없으면 만든다: 미러는 첫 봉투가 올 때 리듀서를 만드는데(§`apply`), 되돌리기는 그보다
|
|
3951
|
+
* 먼저 일어나야 한다(그러지 않으면 첫 봉투가 빈 리듀서를 만들고 되돌린 것을 덮는다).
|
|
3952
|
+
*
|
|
3953
|
+
* 되돌린 뒤 상태로 옮긴다 — 그러지 않으면 첫 스냅샷이 빈 상태를 보인다.
|
|
3954
|
+
*
|
|
3955
|
+
* **구조가 다르면 되돌리지 않는다**: 재개점은 그 모델 위에서 만들어진 것이고, 다른 공장의 재개점을
|
|
3956
|
+
* 얹으면 없는 자리·설비가 생긴다. 판단은 부르는 쪽이 한다(`structureRev` 를 아는 것은 호스트다) —
|
|
3957
|
+
* 여기서는 받은 것을 그대로 세운다.
|
|
3958
|
+
*/
|
|
3959
|
+
restoreObserved(cp) {
|
|
3960
|
+
if (!this.observer) {
|
|
3961
|
+
this.observer = new ObservedReducer(this.boardDef ?? { locations: [], equipment: [] });
|
|
3962
|
+
this.observeMode = true;
|
|
3963
|
+
}
|
|
3964
|
+
this.observer.restore(cp);
|
|
3965
|
+
this.observedDirty = true;
|
|
3966
|
+
this.settleObserved();
|
|
3967
|
+
}
|
|
3743
3968
|
/**
|
|
3744
3969
|
* 이 커널의 상태가 **관측에서 왔나** — 미러인가.
|
|
3745
3970
|
*
|
|
@@ -4639,10 +4864,23 @@ var FlowEngine = class {
|
|
|
4639
4864
|
*/
|
|
4640
4865
|
collectAttentions() {
|
|
4641
4866
|
const now = this.now();
|
|
4867
|
+
const specById = new Map((this.boardDef?.testSpecifications ?? []).map((sp) => [sp.id, sp]));
|
|
4868
|
+
const criteriaOf = /* @__PURE__ */ new Map();
|
|
4869
|
+
if (specById.size) {
|
|
4870
|
+
for (const n of readBoardLocations(this.boardDef ?? {})) {
|
|
4871
|
+
const ids = n.testSpecificationIds;
|
|
4872
|
+
if (!ids?.length) continue;
|
|
4873
|
+
const cs = ids.flatMap((id) => specById.get(id)?.criteria ?? []);
|
|
4874
|
+
if (cs.length) criteriaOf.set(n.id, cs);
|
|
4875
|
+
}
|
|
4876
|
+
}
|
|
4642
4877
|
const out = deriveAttentions(
|
|
4643
4878
|
{
|
|
4644
4879
|
equipment: [...this.equipment.values()],
|
|
4645
|
-
locations: [...this.locations.values()]
|
|
4880
|
+
locations: [...this.locations.values()].map((n) => {
|
|
4881
|
+
const criteria = criteriaOf.get(n.id);
|
|
4882
|
+
return criteria?.length ? { ...n, criteria } : n;
|
|
4883
|
+
}),
|
|
4646
4884
|
orders: [...this.orders.values()],
|
|
4647
4885
|
tasks: [...this.tasks.values()]
|
|
4648
4886
|
},
|
|
@@ -9126,6 +9364,7 @@ function retiredVocabularyIn(line) {
|
|
|
9126
9364
|
constantDuration,
|
|
9127
9365
|
conversionFactorOf,
|
|
9128
9366
|
counterfactualAt,
|
|
9367
|
+
criterionSaysNothing,
|
|
9129
9368
|
demandWindowStart,
|
|
9130
9369
|
deriveAttentions,
|
|
9131
9370
|
documentPath,
|
|
@@ -9172,10 +9411,12 @@ function retiredVocabularyIn(line) {
|
|
|
9172
9411
|
monteCarloForecastAsync,
|
|
9173
9412
|
objectEvent,
|
|
9174
9413
|
objectUri,
|
|
9414
|
+
observationAt,
|
|
9175
9415
|
offCalendarAt,
|
|
9176
9416
|
offCalendarReasonAt,
|
|
9177
9417
|
operationalKindOf,
|
|
9178
9418
|
operationsCapabilityOf,
|
|
9419
|
+
outsideLimit,
|
|
9179
9420
|
parseEpc,
|
|
9180
9421
|
parseIsoDuration,
|
|
9181
9422
|
partialFitPolicy,
|
package/package.json
CHANGED