@operato/twin-kernel 0.7.26 → 0.7.27

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.
@@ -1,5 +1,5 @@
1
1
  import type { TwinModelDef, CanonicalEnvelope, StructureShift } from './contract.ts';
2
- import { type ProjectedState } from './state-projector.ts';
2
+ import { type ProjectedState, type ReducerCheckpoint } from './state-projector.ts';
3
3
  export declare class EventJournal {
4
4
  private events;
5
5
  /** 이벤트 1건 기록(추가 전용). runtime/kernel 의 onEvent 에 연결. */
@@ -15,6 +15,29 @@ export declare class EventJournal {
15
15
  }
16
16
  /** 이벤트열 → 상태 재구성(시간여행). `model` = 그 시점의 트윈 모델(토폴로지·자원). */
17
17
  export declare function replay(model: TwinModelDef, events: readonly CanonicalEnvelope[]): ProjectedState;
18
+ /**
19
+ * **재개점에서 이어 접는다** — 0부터 다시 접지 않는다.
20
+ *
21
+ * ── 왜 (2026-08-18 실측) ────────────────────────────────────────────────────
22
+ * 저널이 27만 건인 트윈에서 과거 상태를 물으면 전부 다시 접어야 했다. 그런데 우리는 이미 주기적으로
23
+ * 재개점을 남기고 있다 — 그 지점부터 **뒤에 일어난 것만** 접으면 같은 답이 나온다.
24
+ *
25
+ * 「같은 답」은 말로 보장되지 않는다: 재개점이 리듀서의 **내부 상태 전부**여야 하고(보기가 아니라),
26
+ * 그 사실은 시험이 증명한다(0부터 접기 == 재개점 + 꼬리). 그래서 이 함수는 새 재개점도 함께 낸다 —
27
+ * 소비처가 그것을 저장해 다음 꼬리를 또 이어 붙일 수 있게.
28
+ *
29
+ * 구조가 바뀐 구간은 여기서 다루지 않는다(`replaySegments` 의 몫이다) — 재개점은 **한 구조 안에서**
30
+ * 이어 붙이는 것이다. 구조가 바뀌었으면 부르는 쪽이 그 경계에서 갈라야 한다.
31
+ */
32
+ export declare function replayFrom(model: TwinModelDef, checkpoint: ReducerCheckpoint, events: readonly CanonicalEnvelope[]): {
33
+ state: ProjectedState;
34
+ checkpoint: ReducerCheckpoint;
35
+ };
36
+ /** 이벤트열을 접고 **재개점도 함께** 낸다 — 다음 번에 이어 붙일 수 있게. */
37
+ export declare function replayWithCheckpoint(model: TwinModelDef, events: readonly CanonicalEnvelope[]): {
38
+ state: ProjectedState;
39
+ checkpoint: ReducerCheckpoint;
40
+ };
18
41
  /**
19
42
  * 한 구조 아래에서 일어난 이벤트들 — 재생의 한 마디.
20
43
  *
@@ -39,6 +39,34 @@ export function replay(model, events) {
39
39
  proj.apply(e);
40
40
  return proj.snapshot();
41
41
  }
42
+ /**
43
+ * **재개점에서 이어 접는다** — 0부터 다시 접지 않는다.
44
+ *
45
+ * ── 왜 (2026-08-18 실측) ────────────────────────────────────────────────────
46
+ * 저널이 27만 건인 트윈에서 과거 상태를 물으면 전부 다시 접어야 했다. 그런데 우리는 이미 주기적으로
47
+ * 재개점을 남기고 있다 — 그 지점부터 **뒤에 일어난 것만** 접으면 같은 답이 나온다.
48
+ *
49
+ * 「같은 답」은 말로 보장되지 않는다: 재개점이 리듀서의 **내부 상태 전부**여야 하고(보기가 아니라),
50
+ * 그 사실은 시험이 증명한다(0부터 접기 == 재개점 + 꼬리). 그래서 이 함수는 새 재개점도 함께 낸다 —
51
+ * 소비처가 그것을 저장해 다음 꼬리를 또 이어 붙일 수 있게.
52
+ *
53
+ * 구조가 바뀐 구간은 여기서 다루지 않는다(`replaySegments` 의 몫이다) — 재개점은 **한 구조 안에서**
54
+ * 이어 붙이는 것이다. 구조가 바뀌었으면 부르는 쪽이 그 경계에서 갈라야 한다.
55
+ */
56
+ export function replayFrom(model, checkpoint, events) {
57
+ const proj = new StateProjector(model);
58
+ proj.restore(checkpoint);
59
+ for (const e of events)
60
+ proj.apply(e);
61
+ return { state: proj.snapshot(), checkpoint: proj.serialize() };
62
+ }
63
+ /** 이벤트열을 접고 **재개점도 함께** 낸다 — 다음 번에 이어 붙일 수 있게. */
64
+ export function replayWithCheckpoint(model, events) {
65
+ const proj = new StateProjector(model);
66
+ for (const e of events)
67
+ proj.apply(e);
68
+ return { state: proj.snapshot(), checkpoint: proj.serialize() };
69
+ }
42
70
  /**
43
71
  * **구조가 바뀐 이력까지 이어서 재생한다.**
44
72
  *
@@ -1,4 +1,22 @@
1
- import type { AssetState, TwinModelDef, CanonicalEnvelope, LocationState, ItemState, EquipmentState, PersonState, TaskState, OrderState, StructureShift } from './contract.ts';
1
+ import type { AssetState, MaterialQuantity, TwinModelDef, CanonicalEnvelope, LocationState, ItemState, EquipmentState, PersonState, TaskState, OrderState, StructureShift } from './contract.ts';
2
+ interface ProjItem {
3
+ epc: string;
4
+ /** 로트의 부분(표준 MaterialSubLot.ID) — 비직렬 로트가 자리마다 갈릴 때만. */
5
+ subLotId?: string;
6
+ gtin?: string;
7
+ gtinKey?: string;
8
+ lot?: string;
9
+ location: string;
10
+ disposition?: string;
11
+ parent?: string;
12
+ qty?: number;
13
+ uom?: string;
14
+ /** 선언된 모든 수량 — 표준 `MaterialLot.Quantity`(복수). 첫 항목만 쓰던 것을 고쳤다. */
15
+ quantities?: MaterialQuantity[];
16
+ /** 받은 개체·로트 마스터데이터 원문 — 이름을 모르는 속성도 잃지 않는다. */
17
+ ilmd?: Record<string, unknown>;
18
+ expiry?: number;
19
+ }
2
20
  /**
3
21
  * 마스터 동기 — 선언적 로케이션 upsert/remove.
4
22
  *
@@ -63,6 +81,50 @@ export interface ProjectedState {
63
81
  */
64
82
  acked: string[];
65
83
  }
84
+ /**
85
+ * 리듀서의 **완전한 재개점** — 이어 접기의 씨앗.
86
+ *
87
+ * `ProjectedState`(보기)와 구별한다: 여기에는 소비처가 보지 않는 것도 들어간다(보류된 담김·집합·
88
+ * 반영 못 한 사건 집계). 그것이 빠지면 「이어 접은 결과」가 「0부터 접은 결과」와 조용히 달라진다.
89
+ */
90
+ export interface ReducerCheckpoint {
91
+ revision: number;
92
+ master: {
93
+ id: string;
94
+ type: string;
95
+ capacity?: number;
96
+ parallelism?: number;
97
+ parentId?: string;
98
+ origin: 'master' | 'observed';
99
+ }[];
100
+ items: ProjItem[];
101
+ aggregation: {
102
+ parent: string;
103
+ children: string[];
104
+ }[];
105
+ pendingParent: {
106
+ child: string;
107
+ parent: string;
108
+ }[];
109
+ tasks: TaskState[];
110
+ equipment: EquipmentState[];
111
+ persons: PersonState[];
112
+ assets: AssetState[];
113
+ orders: OrderState[];
114
+ acked: string[];
115
+ corrections: {
116
+ declaredAt: string;
117
+ reason?: string;
118
+ correctiveEventIDs: string[];
119
+ eventID?: string;
120
+ }[];
121
+ unhandled: {
122
+ eventType: string;
123
+ count: number;
124
+ firstAtMs?: number;
125
+ lastAtMs?: number;
126
+ }[];
127
+ }
66
128
  export declare class ObservedReducer {
67
129
  /** 로케이션 마스터 — 출처를 함께 들고 있다(마스터가 말한 자리 vs 관측으로 알게 된 자리). */
68
130
  private master;
@@ -215,5 +277,27 @@ export declare class ObservedReducer {
215
277
  * **이벤트 없이 시각만으로** 넘어간다. 델타로 받아 두면 만료된 자격이 영원히 유효하게 남는다.
216
278
  */
217
279
  private capabilityPart;
280
+ /**
281
+ * **재개점(checkpoint)** — 스냅샷과 다르다.
282
+ *
283
+ * ── 왜 스냅샷으로는 이어 접을 수 없나 (2026-08-18) ─────────────────────────
284
+ * `snapshot()` 은 **소비처가 보는 값**이다(파생된 판정·정리된 목록). 그것으로 리듀서를 되세우면
285
+ * 보이지 않는 것들이 사라진다 — 아직 등장하지 않은 자식의 담김(`pendingParent`), 집합 관계
286
+ * (`aggregation`), 반영 못 한 사건 집계(`unhandled`), 정정 선언(`corrections`). 그 상태에서 뒤 이벤트를
287
+ * 접으면 **0부터 접은 결과와 달라진다.** 다르면 그 차이는 조용하다(오류가 없다).
288
+ *
289
+ * 그래서 재개점은 **내부 상태 전부**다. 「이어 접기」의 정합성은 시험이 증명한다:
290
+ * 0부터 접은 결과 == 앞부분 재개점 + 뒷부분 접기.
291
+ *
292
+ * 모델에서 오는 것(교대·등급·시간대)은 담지 않는다 — 되세울 때 같은 모델을 받기 때문이다.
293
+ */
294
+ serialize(): ReducerCheckpoint;
295
+ /**
296
+ * 재개점에서 되세운다 — **모르는 것은 지어내지 않는다**(없는 축은 비운다).
297
+ *
298
+ * 모델에서 오는 판정 입력(교대·등급·시간대)은 생성자가 이미 세웠으므로 건드리지 않는다.
299
+ */
300
+ restore(cp: ReducerCheckpoint): void;
218
301
  snapshot(): ProjectedState;
219
302
  }
303
+ export {};
@@ -635,6 +635,57 @@ export class ObservedReducer {
635
635
  capability: capabilityOf({ ...r, ...(decl?.window ? { window: decl.window } : {}), ...(decl?.workCalendar ? { workCalendar: decl.workCalendar } : {}) }, { at, utcOffsetMinutes: this.utcOffsetMinutes, requiredTests: requiredTestsFor(directClassIds, defs, at) })
636
636
  };
637
637
  }
638
+ /**
639
+ * **재개점(checkpoint)** — 스냅샷과 다르다.
640
+ *
641
+ * ── 왜 스냅샷으로는 이어 접을 수 없나 (2026-08-18) ─────────────────────────
642
+ * `snapshot()` 은 **소비처가 보는 값**이다(파생된 판정·정리된 목록). 그것으로 리듀서를 되세우면
643
+ * 보이지 않는 것들이 사라진다 — 아직 등장하지 않은 자식의 담김(`pendingParent`), 집합 관계
644
+ * (`aggregation`), 반영 못 한 사건 집계(`unhandled`), 정정 선언(`corrections`). 그 상태에서 뒤 이벤트를
645
+ * 접으면 **0부터 접은 결과와 달라진다.** 다르면 그 차이는 조용하다(오류가 없다).
646
+ *
647
+ * 그래서 재개점은 **내부 상태 전부**다. 「이어 접기」의 정합성은 시험이 증명한다:
648
+ * 0부터 접은 결과 == 앞부분 재개점 + 뒷부분 접기.
649
+ *
650
+ * 모델에서 오는 것(교대·등급·시간대)은 담지 않는다 — 되세울 때 같은 모델을 받기 때문이다.
651
+ */
652
+ serialize() {
653
+ return {
654
+ revision: this.revision,
655
+ master: [...this.master.values()].map(n => ({ ...n })),
656
+ items: [...this.items.values()].map(i => ({ ...i })),
657
+ aggregation: [...this.aggregation.entries()].map(([parent, children]) => ({ parent, children: [...children] })),
658
+ pendingParent: [...this.pendingParent.entries()].map(([child, parent]) => ({ child, parent })),
659
+ tasks: [...this.tasks.values()].map(t => ({ ...t })),
660
+ equipment: [...this.equipment.values()].map(m => ({ ...m })),
661
+ persons: [...this.persons.values()].map(x => ({ ...x })),
662
+ assets: [...this.assets.values()].map(x => ({ ...x })),
663
+ orders: [...this.orders.values()].map(o => ({ ...o })),
664
+ acked: [...this.acked],
665
+ corrections: this.corrections.map(c => ({ ...c })),
666
+ unhandled: [...this.unhandled.entries()].map(([eventType, v]) => ({ eventType, ...v }))
667
+ };
668
+ }
669
+ /**
670
+ * 재개점에서 되세운다 — **모르는 것은 지어내지 않는다**(없는 축은 비운다).
671
+ *
672
+ * 모델에서 오는 판정 입력(교대·등급·시간대)은 생성자가 이미 세웠으므로 건드리지 않는다.
673
+ */
674
+ restore(cp) {
675
+ this.revision = cp?.revision ?? 0;
676
+ this.master = new Map((cp?.master ?? []).map(n => [n.id, { ...n }]));
677
+ this.items = new Map((cp?.items ?? []).map(i => [i.epc, { ...i }]));
678
+ this.aggregation = new Map((cp?.aggregation ?? []).map(a => [a.parent, [...a.children]]));
679
+ this.pendingParent = new Map((cp?.pendingParent ?? []).map(x => [x.child, x.parent]));
680
+ this.tasks = new Map((cp?.tasks ?? []).map(t => [t.id, { ...t }]));
681
+ this.equipment = new Map((cp?.equipment ?? []).map(m => [m.id, { ...m }]));
682
+ this.persons = new Map((cp?.persons ?? []).map(x => [x.id, { ...x }]));
683
+ this.assets = new Map((cp?.assets ?? []).map(x => [x.id, { ...x }]));
684
+ this.orders = new Map((cp?.orders ?? []).map(o => [o.id, { ...o }]));
685
+ this.acked = new Set(cp?.acked ?? []);
686
+ this.corrections = (cp?.corrections ?? []).map(c => ({ ...c }));
687
+ this.unhandled = new Map((cp?.unhandled ?? []).map(({ eventType, ...v }) => [eventType, { ...v }]));
688
+ }
638
689
  snapshot() {
639
690
  const occ = new Map();
640
691
  for (const it of this.items.values())
@@ -1,2 +1,2 @@
1
1
  export { ObservedReducer, ObservedReducer as StateProjector } from './observed-reducer.ts';
2
- export type { MasterUpdate, ProjectedState } from './observed-reducer.ts';
2
+ export type { MasterUpdate, ProjectedState, ReducerCheckpoint } from './observed-reducer.ts';
@@ -144,7 +144,9 @@ __export(index_exports, {
144
144
  relationsFrom: () => relationsFrom,
145
145
  relationsTo: () => relationsTo,
146
146
  replay: () => replay,
147
+ replayFrom: () => replayFrom,
147
148
  replaySegments: () => replaySegments,
149
+ replayWithCheckpoint: () => replayWithCheckpoint,
148
150
  requiredTestsFor: () => requiredTestsFor,
149
151
  retiredVocabularyIn: () => retiredVocabularyIn,
150
152
  sgtinClass: () => sgtinClass,
@@ -1584,6 +1586,57 @@ var ObservedReducer = class {
1584
1586
  )
1585
1587
  };
1586
1588
  }
1589
+ /**
1590
+ * **재개점(checkpoint)** — 스냅샷과 다르다.
1591
+ *
1592
+ * ── 왜 스냅샷으로는 이어 접을 수 없나 (2026-08-18) ─────────────────────────
1593
+ * `snapshot()` 은 **소비처가 보는 값**이다(파생된 판정·정리된 목록). 그것으로 리듀서를 되세우면
1594
+ * 보이지 않는 것들이 사라진다 — 아직 등장하지 않은 자식의 담김(`pendingParent`), 집합 관계
1595
+ * (`aggregation`), 반영 못 한 사건 집계(`unhandled`), 정정 선언(`corrections`). 그 상태에서 뒤 이벤트를
1596
+ * 접으면 **0부터 접은 결과와 달라진다.** 다르면 그 차이는 조용하다(오류가 없다).
1597
+ *
1598
+ * 그래서 재개점은 **내부 상태 전부**다. 「이어 접기」의 정합성은 시험이 증명한다:
1599
+ * 0부터 접은 결과 == 앞부분 재개점 + 뒷부분 접기.
1600
+ *
1601
+ * 모델에서 오는 것(교대·등급·시간대)은 담지 않는다 — 되세울 때 같은 모델을 받기 때문이다.
1602
+ */
1603
+ serialize() {
1604
+ return {
1605
+ revision: this.revision,
1606
+ master: [...this.master.values()].map((n) => ({ ...n })),
1607
+ items: [...this.items.values()].map((i) => ({ ...i })),
1608
+ aggregation: [...this.aggregation.entries()].map(([parent, children]) => ({ parent, children: [...children] })),
1609
+ pendingParent: [...this.pendingParent.entries()].map(([child, parent]) => ({ child, parent })),
1610
+ tasks: [...this.tasks.values()].map((t) => ({ ...t })),
1611
+ equipment: [...this.equipment.values()].map((m) => ({ ...m })),
1612
+ persons: [...this.persons.values()].map((x) => ({ ...x })),
1613
+ assets: [...this.assets.values()].map((x) => ({ ...x })),
1614
+ orders: [...this.orders.values()].map((o) => ({ ...o })),
1615
+ acked: [...this.acked],
1616
+ corrections: this.corrections.map((c) => ({ ...c })),
1617
+ unhandled: [...this.unhandled.entries()].map(([eventType, v]) => ({ eventType, ...v }))
1618
+ };
1619
+ }
1620
+ /**
1621
+ * 재개점에서 되세운다 — **모르는 것은 지어내지 않는다**(없는 축은 비운다).
1622
+ *
1623
+ * 모델에서 오는 판정 입력(교대·등급·시간대)은 생성자가 이미 세웠으므로 건드리지 않는다.
1624
+ */
1625
+ restore(cp) {
1626
+ this.revision = cp?.revision ?? 0;
1627
+ this.master = new Map((cp?.master ?? []).map((n) => [n.id, { ...n }]));
1628
+ this.items = new Map((cp?.items ?? []).map((i) => [i.epc, { ...i }]));
1629
+ this.aggregation = new Map((cp?.aggregation ?? []).map((a) => [a.parent, [...a.children]]));
1630
+ this.pendingParent = new Map((cp?.pendingParent ?? []).map((x) => [x.child, x.parent]));
1631
+ this.tasks = new Map((cp?.tasks ?? []).map((t) => [t.id, { ...t }]));
1632
+ this.equipment = new Map((cp?.equipment ?? []).map((m) => [m.id, { ...m }]));
1633
+ this.persons = new Map((cp?.persons ?? []).map((x) => [x.id, { ...x }]));
1634
+ this.assets = new Map((cp?.assets ?? []).map((x) => [x.id, { ...x }]));
1635
+ this.orders = new Map((cp?.orders ?? []).map((o) => [o.id, { ...o }]));
1636
+ this.acked = new Set(cp?.acked ?? []);
1637
+ this.corrections = (cp?.corrections ?? []).map((c) => ({ ...c }));
1638
+ this.unhandled = new Map((cp?.unhandled ?? []).map(({ eventType, ...v }) => [eventType, { ...v }]));
1639
+ }
1587
1640
  snapshot() {
1588
1641
  const occ = /* @__PURE__ */ new Map();
1589
1642
  for (const it of this.items.values()) occ.set(it.location, (occ.get(it.location) ?? 0) + 1);
@@ -1651,6 +1704,17 @@ function replay(model, events) {
1651
1704
  for (const e of events) proj.apply(e);
1652
1705
  return proj.snapshot();
1653
1706
  }
1707
+ function replayFrom(model, checkpoint, events) {
1708
+ const proj = new ObservedReducer(model);
1709
+ proj.restore(checkpoint);
1710
+ for (const e of events) proj.apply(e);
1711
+ return { state: proj.snapshot(), checkpoint: proj.serialize() };
1712
+ }
1713
+ function replayWithCheckpoint(model, events) {
1714
+ const proj = new ObservedReducer(model);
1715
+ for (const e of events) proj.apply(e);
1716
+ return { state: proj.snapshot(), checkpoint: proj.serialize() };
1717
+ }
1654
1718
  function replaySegments(segments) {
1655
1719
  if (!segments.length) throw new Error("\uC7AC\uC0DD\uD560 \uB9C8\uB514\uAC00 \uC5C6\uB2E4 \u2014 \uAD6C\uC870\uB97C \uD558\uB098\uB3C4 \uC8FC\uC9C0 \uC54A\uC558\uB2E4");
1656
1720
  const proj = new ObservedReducer(segments[0].model);
@@ -7236,7 +7300,9 @@ function retiredVocabularyIn(line) {
7236
7300
  relationsFrom,
7237
7301
  relationsTo,
7238
7302
  replay,
7303
+ replayFrom,
7239
7304
  replaySegments,
7305
+ replayWithCheckpoint,
7240
7306
  requiredTestsFor,
7241
7307
  retiredVocabularyIn,
7242
7308
  sgtinClass,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/twin-kernel",
3
- "version": "0.7.26",
3
+ "version": "0.7.27",
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": {