@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.
@@ -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
- samples.push(opts.metric(fc.getSnapshot()));
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
- samples.push(opts.metric(fc.getSnapshot()));
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 { runs, samples, min: sorted[0], max: sorted[sorted.length - 1], mean, p50: pct(0.5), p90: pct(0.9) };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/twin-kernel",
3
- "version": "0.7.54",
3
+ "version": "0.7.56",
4
4
  "type": "module",
5
5
  "description": "Twin Domain Kernel — framework-agnostic, zero-dep (domain + sim + 3-channel contract). WMS/YMS/MES, EPCIS 2.0 · ISA-95.",
6
6
  "publishConfig": {