@operato/twin-kernel 0.7.16 → 0.7.17

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.
@@ -1450,6 +1450,26 @@ export interface ScenarioDef {
1450
1450
  * 아무도 답할 수 없다).
1451
1451
  */
1452
1452
  interventions?: ScenarioIntervention[];
1453
+ /**
1454
+ * **가정한 선언** — 「이렇게 선언돼 있었다면?」(예: 축전지에 피크 억제 임계를 넣으면).
1455
+ *
1456
+ * 개입(`interventions`)이 시각을 가진 **행동**이라면 이쪽은 시각이 없는 **모델의 변주**다. 둘을 한
1457
+ * 자리에 두지 않는 이유: 행동은 저널에 남을 사실이 되고, 변주는 「그런 현장이었다면」이라는 가정이다.
1458
+ *
1459
+ * 시나리오와 함께 다니므로 fork 가 자기완결이다 — 같은 선언 하나로 예측·백테스트·AI 가 같은 미래를
1460
+ * 재생한다. 다만 **실행 중 트윈에 이 시나리오를 걸면 그 트윈의 선언이 실제로 바뀐다**: 그것은
1461
+ * 「우리 현장에 그 자동화가 있다」는 주장이므로, 무엇을 덮어썼는지 소비처가 읽을 수 있어야 한다
1462
+ * (`declarationOverrides()`).
1463
+ */
1464
+ overrides?: ScenarioOverride[];
1465
+ }
1466
+ /** 가정한 선언 하나 — 어느 자원의 어느 속성을 무엇으로. */
1467
+ export interface ScenarioOverride {
1468
+ resourceId: string;
1469
+ propertyId: string;
1470
+ value: string | number;
1471
+ /** 그 자원이 없었으면 false — 조용히 성공한 척하지 않는다. */
1472
+ applied?: boolean;
1453
1473
  }
1454
1474
  /** 가정한 개입 하나 — 시각 + 행동. */
1455
1475
  export interface ScenarioIntervention {
@@ -554,6 +554,15 @@ export class EmsKernel extends FlowEngine {
554
554
  const state = this.equipment.get(e.id);
555
555
  if (!state)
556
556
  continue;
557
+ /*
558
+ * 시뮬의 출발 SOC — 선언에서 한 번 심는다(계측이 들어오면 그것이 이긴다).
559
+ * 이것이 없던 동안, 정책을 선언해도 「SOC 를 모른다」에 걸려 배터리가 아무 일도 하지 못했다.
560
+ */
561
+ if (!Number.isFinite(Number(state.soc))) {
562
+ const seed = this.numberProperty(e, EMS_PROPERTY.initialSoc);
563
+ if (seed !== undefined)
564
+ state.soc = seed;
565
+ }
557
566
  const over = demandBeforeStorageKW - dispatched - above;
558
567
  if (over <= 0) {
559
568
  /* 임계 아래 — 쓸 이유가 없다. 낡은 방전값을 남기지 않는다(상태가 지난 틱을 말하게 된다). */
@@ -30,5 +30,15 @@ export declare const EMS_PROPERTY: {
30
30
  readonly maxDischargeKW: "dispatch.maxDischargeKW";
31
31
  /** 예비 SOC(%) — 이 아래로는 쓰지 않는다(비상 대비). 없으면 0 으로 본다. */
32
32
  readonly reserveSoc: "dispatch.reserveSoc";
33
+ /**
34
+ * 시뮬레이션이 **출발할 때의 SOC(%)** — 씨앗값이다.
35
+ *
36
+ * 계측이 SOC 를 알려 주는 트윈에는 필요 없다(잰 값이 진실이다). 시뮬 트윈에는 알려 줄 것이 없어서
37
+ * 「SOC 를 모르면 방전하지 않는다」는 규율에 걸려 **배터리가 아무 일도 하지 못했다** — 정책을 선언해도
38
+ * 피크가 한 톨도 깎이지 않았다(실화면에서 그렇게 났다).
39
+ *
40
+ * 이것은 상태의 씨앗이지 계측이 아니다. 그래서 계측이 들어오는 순간 그것이 이깁니다.
41
+ */
42
+ readonly initialSoc: "storage.initialSoc";
33
43
  };
34
44
  export declare const EMS_TYPES: TwinTypeInfo[];
@@ -61,7 +61,17 @@ export const EMS_PROPERTY = {
61
61
  /** 최대 방전율(kW) — 인버터가 낼 수 있는 한계. */
62
62
  maxDischargeKW: 'dispatch.maxDischargeKW',
63
63
  /** 예비 SOC(%) — 이 아래로는 쓰지 않는다(비상 대비). 없으면 0 으로 본다. */
64
- reserveSoc: 'dispatch.reserveSoc'
64
+ reserveSoc: 'dispatch.reserveSoc',
65
+ /**
66
+ * 시뮬레이션이 **출발할 때의 SOC(%)** — 씨앗값이다.
67
+ *
68
+ * 계측이 SOC 를 알려 주는 트윈에는 필요 없다(잰 값이 진실이다). 시뮬 트윈에는 알려 줄 것이 없어서
69
+ * 「SOC 를 모르면 방전하지 않는다」는 규율에 걸려 **배터리가 아무 일도 하지 못했다** — 정책을 선언해도
70
+ * 피크가 한 톨도 깎이지 않았다(실화면에서 그렇게 났다).
71
+ *
72
+ * 이것은 상태의 씨앗이지 계측이 아니다. 그래서 계측이 들어오는 순간 그것이 이깁니다.
73
+ */
74
+ initialSoc: 'storage.initialSoc'
65
75
  };
66
76
  export const EMS_TYPES = [
67
77
  /* ── 자리: 전기적 구간 ─────────────────────────────────────────────────── */
@@ -1,4 +1,4 @@
1
- import type { TestResult, ISOTime, MaterialQuantity, WorkCalendarEntry, EffectivePeriod, Effectivity, OffCalendarReason, ResourceProperty, ResourceClassDef, MaterialDefinition, Attention, TwinModelDef, CanonicalEnvelope, Command, CommandAck, EventHandler, EquipmentMotion, OeeMetrics, AssetState, GeneratorSpec, InterventionOutcome, OrderState, PersonState, ScenarioControl, StateSnapshot, TwinKernel, Unsubscribe, LocationState, ItemState, EquipmentState, OrderStatusDelta, TaskState, StructureShift } from './contract.ts';
1
+ import type { TestResult, ISOTime, MaterialQuantity, WorkCalendarEntry, EffectivePeriod, Effectivity, OffCalendarReason, ResourceProperty, ResourceClassDef, MaterialDefinition, Attention, TwinModelDef, CanonicalEnvelope, Command, CommandAck, EventHandler, EquipmentMotion, OeeMetrics, AssetState, GeneratorSpec, InterventionOutcome, OrderState, PersonState, ScenarioControl, ScenarioOverride, StateSnapshot, TwinKernel, Unsubscribe, LocationState, ItemState, EquipmentState, OrderStatusDelta, TaskState, StructureShift } from './contract.ts';
2
2
  import type { EpcisEvent, BizTransactionElement } from './epcis.ts';
3
3
  import type { AllocationPolicy, SlotView } from './allocation-policy.ts';
4
4
  import type { DurationEstimator, DurationContext } from './duration-estimator.ts';
@@ -329,6 +329,8 @@ export declare abstract class FlowEngine implements TwinKernel {
329
329
  private interventions;
330
330
  /** 개입이 어떻게 됐나 — 걸린 것·거절된 것. 조용히 사라지지 않게 소비처가 읽는다. */
331
331
  private interventionLog;
332
+ /** 덮어쓴 선언 — 「이 값은 가정이다」를 소비처가 말할 수 있게. */
333
+ private overrideLog;
332
334
  private generating;
333
335
  private speed;
334
336
  private eventSeq;
@@ -480,6 +482,13 @@ export declare abstract class FlowEngine implements TwinKernel {
480
482
  * 아직 시각이 오지 않은 것은 여기 없다(일어나지 않은 일을 결과로 적지 않는다).
481
483
  */
482
484
  interventionOutcomes(): InterventionOutcome[];
485
+ /**
486
+ * 이 커널이 **가정으로 덮어쓴 선언들** — 없으면 빈 배열.
487
+ *
488
+ * 비어 있지 않다는 것은 「이 트윈의 수는 선언된 현장이 아니라 가정한 현장의 것」이라는 뜻이다.
489
+ * 화면·AI 가 그 사실을 함께 말해야 한다.
490
+ */
491
+ declarationOverrides(): ScenarioOverride[];
483
492
  /**
484
493
  * 이 커널의 **지금**(ms) — 시각으로 바뀌는 모든 판정의 단일 기준.
485
494
  *
@@ -290,6 +290,8 @@ export class FlowEngine {
290
290
  interventions = [];
291
291
  /** 개입이 어떻게 됐나 — 걸린 것·거절된 것. 조용히 사라지지 않게 소비처가 읽는다. */
292
292
  interventionLog = [];
293
+ /** 덮어쓴 선언 — 「이 값은 가정이다」를 소비처가 말할 수 있게. */
294
+ overrideLog = [];
293
295
  generating = false;
294
296
  speed = 1;
295
297
  eventSeq = 0;
@@ -875,6 +877,14 @@ export class FlowEngine {
875
877
  /* 개입은 **시작할 때** 시각이 정해진다(경과 기준) — 여기서는 받아만 둔다. */
876
878
  this.interventions = (def.interventions ?? []).map(spec => ({ spec, dueMs: 0, done: false }));
877
879
  this.interventionLog = [];
880
+ /*
881
+ * 가정한 선언은 **지금 곧 걸린다** — 시각이 없는 변주이므로 미룰 것이 없다.
882
+ * 무엇을 덮어썼는지 남긴다: 말하지 않으면 소비처가 what-if 결과를 트윈의 현재 상태로 읽는다.
883
+ */
884
+ this.overrideLog = (def.overrides ?? []).map(o => ({
885
+ ...o,
886
+ applied: this.setResourceProperty(o.resourceId, o.propertyId, o.value)
887
+ }));
878
888
  },
879
889
  start: () => {
880
890
  if (this.generating)
@@ -889,7 +899,7 @@ export class FlowEngine {
889
899
  }
890
900
  },
891
901
  pause: () => { this.generating = false; },
892
- reset: () => { this.generating = false; this.gens = []; this.interventions = []; this.interventionLog = []; },
902
+ reset: () => { this.generating = false; this.gens = []; this.interventions = []; this.interventionLog = []; this.overrideLog = []; },
893
903
  setSpeed: (f) => { this.speed = f; }
894
904
  };
895
905
  tick(dtMs) {
@@ -1117,6 +1127,15 @@ export class FlowEngine {
1117
1127
  interventionOutcomes() {
1118
1128
  return this.interventionLog.map(o => ({ ...o }));
1119
1129
  }
1130
+ /**
1131
+ * 이 커널이 **가정으로 덮어쓴 선언들** — 없으면 빈 배열.
1132
+ *
1133
+ * 비어 있지 않다는 것은 「이 트윈의 수는 선언된 현장이 아니라 가정한 현장의 것」이라는 뜻이다.
1134
+ * 화면·AI 가 그 사실을 함께 말해야 한다.
1135
+ */
1136
+ declarationOverrides() {
1137
+ return this.overrideLog.map(o => ({ ...o }));
1138
+ }
1120
1139
  // ── 보호 헬퍼 (도메인 hook 에서 사용) ──────────────────────────────────────
1121
1140
  /**
1122
1141
  * 이 커널의 **지금**(ms) — 시각으로 바뀌는 모든 판정의 단일 기준.
@@ -1725,7 +1725,17 @@ var EMS_PROPERTY = {
1725
1725
  /** 최대 방전율(kW) — 인버터가 낼 수 있는 한계. */
1726
1726
  maxDischargeKW: "dispatch.maxDischargeKW",
1727
1727
  /** 예비 SOC(%) — 이 아래로는 쓰지 않는다(비상 대비). 없으면 0 으로 본다. */
1728
- reserveSoc: "dispatch.reserveSoc"
1728
+ reserveSoc: "dispatch.reserveSoc",
1729
+ /**
1730
+ * 시뮬레이션이 **출발할 때의 SOC(%)** — 씨앗값이다.
1731
+ *
1732
+ * 계측이 SOC 를 알려 주는 트윈에는 필요 없다(잰 값이 진실이다). 시뮬 트윈에는 알려 줄 것이 없어서
1733
+ * 「SOC 를 모르면 방전하지 않는다」는 규율에 걸려 **배터리가 아무 일도 하지 못했다** — 정책을 선언해도
1734
+ * 피크가 한 톨도 깎이지 않았다(실화면에서 그렇게 났다).
1735
+ *
1736
+ * 이것은 상태의 씨앗이지 계측이 아니다. 그래서 계측이 들어오는 순간 그것이 이깁니다.
1737
+ */
1738
+ initialSoc: "storage.initialSoc"
1729
1739
  };
1730
1740
  var EMS_TYPES = [
1731
1741
  /* ── 자리: 전기적 구간 ─────────────────────────────────────────────────── */
@@ -2996,6 +3006,8 @@ var FlowEngine = class {
2996
3006
  interventions = [];
2997
3007
  /** 개입이 어떻게 됐나 — 걸린 것·거절된 것. 조용히 사라지지 않게 소비처가 읽는다. */
2998
3008
  interventionLog = [];
3009
+ /** 덮어쓴 선언 — 「이 값은 가정이다」를 소비처가 말할 수 있게. */
3010
+ overrideLog = [];
2999
3011
  generating = false;
3000
3012
  speed = 1;
3001
3013
  eventSeq = 0;
@@ -3523,6 +3535,10 @@ var FlowEngine = class {
3523
3535
  this.gens = def.generators.map((spec) => ({ spec, nextMs: 0 }));
3524
3536
  this.interventions = (def.interventions ?? []).map((spec) => ({ spec, dueMs: 0, done: false }));
3525
3537
  this.interventionLog = [];
3538
+ this.overrideLog = (def.overrides ?? []).map((o) => ({
3539
+ ...o,
3540
+ applied: this.setResourceProperty(o.resourceId, o.propertyId, o.value)
3541
+ }));
3526
3542
  },
3527
3543
  start: () => {
3528
3544
  if (this.generating) return;
@@ -3541,6 +3557,7 @@ var FlowEngine = class {
3541
3557
  this.gens = [];
3542
3558
  this.interventions = [];
3543
3559
  this.interventionLog = [];
3560
+ this.overrideLog = [];
3544
3561
  },
3545
3562
  setSpeed: (f) => {
3546
3563
  this.speed = f;
@@ -3744,6 +3761,15 @@ var FlowEngine = class {
3744
3761
  interventionOutcomes() {
3745
3762
  return this.interventionLog.map((o) => ({ ...o }));
3746
3763
  }
3764
+ /**
3765
+ * 이 커널이 **가정으로 덮어쓴 선언들** — 없으면 빈 배열.
3766
+ *
3767
+ * 비어 있지 않다는 것은 「이 트윈의 수는 선언된 현장이 아니라 가정한 현장의 것」이라는 뜻이다.
3768
+ * 화면·AI 가 그 사실을 함께 말해야 한다.
3769
+ */
3770
+ declarationOverrides() {
3771
+ return this.overrideLog.map((o) => ({ ...o }));
3772
+ }
3747
3773
  // ── 보호 헬퍼 (도메인 hook 에서 사용) ──────────────────────────────────────
3748
3774
  /**
3749
3775
  * 이 커널의 **지금**(ms) — 시각으로 바뀌는 모든 판정의 단일 기준.
@@ -6292,6 +6318,10 @@ var EmsKernel = class extends FlowEngine {
6292
6318
  if (!(capacity > 0) || !(maxRate > 0)) continue;
6293
6319
  const state = this.equipment.get(e.id);
6294
6320
  if (!state) continue;
6321
+ if (!Number.isFinite(Number(state.soc))) {
6322
+ const seed = this.numberProperty(e, EMS_PROPERTY.initialSoc);
6323
+ if (seed !== void 0) state.soc = seed;
6324
+ }
6295
6325
  const over = demandBeforeStorageKW - dispatched - above;
6296
6326
  if (over <= 0) {
6297
6327
  if (Number(state.dischargeKW) > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/twin-kernel",
3
- "version": "0.7.16",
3
+ "version": "0.7.17",
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": {