@operato/twin-kernel 0.7.88 → 0.7.89

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.
@@ -671,7 +671,27 @@ export type Effectivity = 'not-yet' | 'expired';
671
671
  * `at` 를 주지 않으면 **판단하지 않는다**(`undefined`). 모르는 시각으로 폐기를 단정하면, 시각을 안 넘긴
672
672
  * 소비처 전부가 자원을 잃는다. 파싱 불가한 시각도 같다 — 짐작해 고치지 않는다.
673
673
  */
674
- export declare function effectivityAt(p: EffectivePeriod | undefined, at?: ISOTime): Effectivity | undefined;
674
+ /**
675
+ * 끝나는 시각을 **포함하는가** — 축마다 다르다. 규칙은 한 벌이고 규약만 밝힌다.
676
+ *
677
+ * ── 왜 이 칸이 생겼나 (2026-08-30) ────────────────────────────────────────
678
+ * 처음에는 이 판정이 한 곳뿐이었고 끝을 포함했다. 자원에는 그것이 맞다 — 폐기 일자가 있는 지게차는
679
+ * 띄엄띄엄 있고, 두 자원의 기간이 한 순간을 함께 갖는 일이 문제가 되지 않는다.
680
+ *
681
+ * 그런데 **선언은 빈틈없이 이어진다.** 8월 요금 주기가 끝나는 순간이 9월 주기가 시작하는 순간이다.
682
+ * 끝을 포함하면 그 한 순간에 두 단가가 동시에 유효해지고, 어느 것으로 계산했는지 말할 수 없다.
683
+ *
684
+ * 그때 같은 판정을 하는 함수를 하나 더 만들었다. 규칙이 두 벌이 되면 한쪽만 고쳐지는 날이 온다 —
685
+ * 이 저장소가 오늘 하루 그 부류를 여러 번 고쳤다. 그래서 함수는 하나로 두고 **규약을 인자로** 받는다.
686
+ */
687
+ export interface EffectivityOptions {
688
+ /**
689
+ * `'inclusive'`(기본) — 끝나는 시각까지 유효하다. 자원의 유효 기간이 이쪽이다.
690
+ * `'exclusive'` — 끝나는 시각은 다음 구간의 것이다. 이어지는 주기(요금·단가)가 이쪽이다.
691
+ */
692
+ end?: 'inclusive' | 'exclusive';
693
+ }
694
+ export declare function effectivityAt(p: EffectivePeriod | undefined, at?: ISOTime, opts?: EffectivityOptions): Effectivity | undefined;
675
695
  /**
676
696
  * 자원 **등급 정의** — ISA-95 가 세 자원에 똑같이 정의한 한 모양.
677
697
  *
package/dist/contract.js CHANGED
@@ -376,17 +376,7 @@ export function judgeAgainstSpec(spec, result) {
376
376
  }
377
377
  return allJudged ? 'pass' : undefined;
378
378
  }
379
- /**
380
- * 이 시각에 유효 기간 밖인가 — **한 규칙**으로 개체·등급·설비↔자산 매핑을 모두 판정한다.
381
- *
382
- * **표준은 날짜만 정하고 "밖이면 어떻게 되는가" 는 정하지 않는다.** 그 판단은 우리 것이므로 여기 밝힌다:
383
- * 기간 밖이면 **그 자원은 그 시각의 모델에 참여하지 않는다**(배정되지 않고, 가용 분모에 들지 않는다).
384
- * 지우지는 않는다 — 이유를 달아 남긴다. 조용히 사라지면 "왜 없어졌나" 를 아무도 답할 수 없다.
385
- *
386
- * `at` 를 주지 않으면 **판단하지 않는다**(`undefined`). 모르는 시각으로 폐기를 단정하면, 시각을 안 넘긴
387
- * 소비처 전부가 자원을 잃는다. 파싱 불가한 시각도 같다 — 짐작해 고치지 않는다.
388
- */
389
- export function effectivityAt(p, at) {
379
+ export function effectivityAt(p, at, opts) {
390
380
  if (!p || !at)
391
381
  return undefined;
392
382
  const atMs = parsedMs(at);
@@ -396,7 +386,7 @@ export function effectivityAt(p, at) {
396
386
  if (Number.isFinite(from) && atMs < from)
397
387
  return 'not-yet';
398
388
  const to = p.effectiveEnd ? parsedMs(p.effectiveEnd) : NaN;
399
- if (Number.isFinite(to) && atMs > to)
389
+ if (Number.isFinite(to) && (opts?.end === 'exclusive' ? atMs >= to : atMs > to))
400
390
  return 'expired';
401
391
  return undefined;
402
392
  }
@@ -28,6 +28,8 @@
28
28
  import { FlowEngine } from "./flow-engine.js";
29
29
  /* 기간 사실의 이름은 유입 문과 한 규칙이다(§`periodFactId`). */
30
30
  import { periodFactId } from "./energy-ingest.js";
31
+ /* 「이 시각에 유효한가」는 커널에 규칙이 한 벌이다 — 선언 축도 그것을 쓴다. */
32
+ import { effectivityAt } from "./contract.js";
31
33
  import { firstFitPolicy } from "./allocation-policy.js";
32
34
  import { CMD, ENERGY_EVENT, minuteOfDayAt, localDayIndexAt, localDayStartMs } from "./contract.js";
33
35
  import { EMS_PROPERTY, METER_DIRECTION, electricalUpstreamOf, generationFractionAt } from "./ems-profile.js";
@@ -1749,22 +1751,30 @@ function putDeclaration(list, d) {
1749
1751
  return next.length > 24 ? next.slice(next.length - 24) : next;
1750
1752
  }
1751
1753
  /**
1752
- * **이 시각에 유효한 선언** — 시작했고 아직 끝나지 않은 것.
1754
+ * **이 시각에 유효한 선언** — 판정은 커널의 한 규칙이 한다(§`effectivityAt`).
1755
+ *
1756
+ * 여기서 하는 일은 **고르는 것**뿐이다. 「유효한가」를 여기서 다시 판정하면 규칙이 두 벌이 되고,
1757
+ * 한쪽만 고쳐지는 날이 온다. 실제로 이 함수는 처음에 자기 판정을 들고 있었다.
1758
+ *
1759
+ * 선언은 **빈틈없이 이어진다** — 8월 주기가 끝나는 순간이 9월 주기가 시작하는 순간이다. 그래서 끝을
1760
+ * 포함하지 않는다(`end: 'exclusive'`). 포함하면 그 한 순간에 두 단가가 동시에 유효해진다.
1753
1761
  *
1754
1762
  * 유효한 것이 없으면 답하지 않는다. 가장 가까운 것을 골라 주지 않는다 — 아직 시작하지 않은 단가로
1755
- * 오늘을 계산하거나, 이미 끝난 단가를 지금 것인 양 쓰게 된다. 둘 다 오류 없이 틀린 값을 만든다.
1763
+ * 오늘을 계산하거나 이미 끝난 단가를 지금 것인 양 쓰게 된다. 둘 다 오류 없이 틀린 값을 만든다.
1756
1764
  *
1757
1765
  * 구간이 겹치면 **나중에 시작한 것**이 이긴다(더 좁게 정한 선언이 앞선다).
1758
1766
  */
1759
1767
  function inEffectAt(list, atMs) {
1768
+ if (!list?.length)
1769
+ return undefined;
1770
+ const at = new Date(atMs).toISOString();
1760
1771
  let best;
1761
1772
  let bestFrom = -Infinity;
1762
- for (const d of list ?? []) {
1763
- const from = Date.parse(String(d.from));
1764
- const to = Date.parse(String(d.to));
1765
- if (!Number.isFinite(from) || !Number.isFinite(to))
1773
+ for (const d of list) {
1774
+ if (effectivityAt({ effectiveStart: d.from, effectiveEnd: d.to }, at, { end: 'exclusive' }) !== undefined)
1766
1775
  continue;
1767
- if (atMs < from || atMs >= to)
1776
+ const from = Date.parse(String(d.from));
1777
+ if (!Number.isFinite(from))
1768
1778
  continue;
1769
1779
  if (from > bestFrom) {
1770
1780
  best = d;
@@ -366,14 +366,14 @@ function judgeAgainstSpec(spec, result) {
366
366
  }
367
367
  return allJudged ? "pass" : void 0;
368
368
  }
369
- function effectivityAt(p, at) {
369
+ function effectivityAt(p, at, opts) {
370
370
  if (!p || !at) return void 0;
371
371
  const atMs = parsedMs(at);
372
372
  if (!Number.isFinite(atMs)) return void 0;
373
373
  const from = p.effectiveStart ? parsedMs(p.effectiveStart) : NaN;
374
374
  if (Number.isFinite(from) && atMs < from) return "not-yet";
375
375
  const to = p.effectiveEnd ? parsedMs(p.effectiveEnd) : NaN;
376
- if (Number.isFinite(to) && atMs > to) return "expired";
376
+ if (Number.isFinite(to) && (opts?.end === "exclusive" ? atMs >= to : atMs > to)) return "expired";
377
377
  return void 0;
378
378
  }
379
379
  var PARSED_MAX = 64;
@@ -9650,13 +9650,14 @@ function putDeclaration(list, d) {
9650
9650
  return next.length > 24 ? next.slice(next.length - 24) : next;
9651
9651
  }
9652
9652
  function inEffectAt(list, atMs) {
9653
+ if (!list?.length) return void 0;
9654
+ const at = new Date(atMs).toISOString();
9653
9655
  let best;
9654
9656
  let bestFrom = -Infinity;
9655
- for (const d of list ?? []) {
9657
+ for (const d of list) {
9658
+ if (effectivityAt({ effectiveStart: d.from, effectiveEnd: d.to }, at, { end: "exclusive" }) !== void 0) continue;
9656
9659
  const from = Date.parse(String(d.from));
9657
- const to = Date.parse(String(d.to));
9658
- if (!Number.isFinite(from) || !Number.isFinite(to)) continue;
9659
- if (atMs < from || atMs >= to) continue;
9660
+ if (!Number.isFinite(from)) continue;
9660
9661
  if (from > bestFrom) {
9661
9662
  best = d;
9662
9663
  bestFrom = from;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/twin-kernel",
3
- "version": "0.7.88",
3
+ "version": "0.7.89",
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": {