@operato/twin-kernel 0.7.28 → 0.7.30

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.
@@ -2,7 +2,7 @@ import type { TestResult, ISOTime, MaterialQuantity, WorkCalendarEntry, Effectiv
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';
5
- import type { OperationDef } from './domain-definition.ts';
5
+ import type { OperationDef, IsoDuration } from './domain-definition.ts';
6
6
  import { type CapacityAnalysis } from './capacity.ts';
7
7
  import { type CommittedDemand, type OperationsCapabilityReport } from './operations-capability.ts';
8
8
  export interface FlowLocation {
@@ -237,6 +237,18 @@ export declare const ATTENTION_DEFAULTS: {
237
237
  readonly scrapPct: 15;
238
238
  readonly scrapMinSamples: 10;
239
239
  };
240
+ /**
241
+ * 공정에 선언할 수 있는 것의 **어휘** — 지금은 소요시간 하나다(`declareDurations` 가 읽는다).
242
+ *
243
+ * 자리·설비의 속성처럼 「속성 id」로 부른다: 선언을 받는 문·화면·AI 가 같은 이름을 써야 하고, 그 이름이
244
+ * 두 곳에 적히면 한쪽만 바뀌는 순간 값이 조용히 버려진다.
245
+ *
246
+ * 이 값이 얹히는 대상은 **자리·설비가 아니라 공정 종류**다(`FlowTask.kind` = `OperationDef.key`).
247
+ */
248
+ export declare const OPERATION_PROPERTY: {
249
+ /** 그 현장에서 이 공정이 걸리는 시간 — 실측이 없을 때 상수를 대신한다. */
250
+ readonly duration: "operation.duration";
251
+ };
240
252
  /** 판정에 쓸 기준 — 대상별 선언이 있으면 그것, 없으면 기본값(그 사실을 함께 든다). */
241
253
  export interface AttentionThresholds {
242
254
  congestionPctOf?: (locationId: string) => number | undefined;
@@ -347,6 +359,13 @@ export declare abstract class FlowEngine implements TwinKernel {
347
359
  * 도메인 정의에서 실어 온다(`loadOperations`). 없으면 커널 기본값을 쓰고 그 사실을 `specCoverage()` 가 밝힌다.
348
360
  */
349
361
  protected operationSpecs: Map<string, OperationDef>;
362
+ /**
363
+ * 현장이 선언한 소요시간(작업 종류 → ms) — `declareDurations` 로 들어온다.
364
+ *
365
+ * 명세 행과 **따로** 두는 이유: 행이 없는 종류에도 시간을 줄 수 있어야 하고(창고·야드 트윈에는 행이
366
+ * 없다), 행을 지어 만들면 지어낸 `intent` 가 능력 계산까지 오염시킨다.
367
+ */
368
+ protected localDurations: Map<string, number>;
350
369
  /** 관측 구동(P0) — 이벤트를 접는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
351
370
  private observer?;
352
371
  /** 관측분이 아직 커널 상태로 옮겨지지 않았다 — 스냅샷·fork 직전에 한 번만 옮긴다. */
@@ -673,6 +692,27 @@ export declare abstract class FlowEngine implements TwinKernel {
673
692
  */
674
693
  protected declaredOperations(): readonly OperationDef[];
675
694
  loadOperations(ops?: OperationDef[]): void;
695
+ /**
696
+ * **현장이 정한 소요시간** — 종류별로 시간만 받는다(2026-08-18).
697
+ *
698
+ * ── 왜 명세 행과 따로 받나 ──────────────────────────────────────────────────
699
+ * 창고·야드 트윈에는 오퍼레이션 명세 행이 **아예 없다**(실측: 35개 트윈 중 WMS·YMS 전부 0개). 그런데
700
+ * 그 트윈들도 `putaway`·`pick`·`spot`·`dwell` 을 돌리고, 시간은 커널 상수에서 온다 — 그 현장이 실제로
701
+ * 몇 분 걸리는지 말할 문이 없었다.
702
+ *
703
+ * 명세 행을 지어 만들 수는 없다: `OperationDef` 는 `label`·`intent` 를 요구하고(ISA-95
704
+ * `OperationsSegment`), 그 둘은 **지어내면 다른 답까지 오염시킨다**(`capacity()`·능력 보고가 의도별로
705
+ * 자원을 센다). 「몇 분 걸리나」를 말하려고 「무슨 종류의 작업인가」를 발명하지 않는다.
706
+ *
707
+ * ── 순서: 실측 > 현장 선언 > 원천 명세 > 상수 ──────────────────────────────
708
+ * 이력에서 배운 값이 가장 강하고(그 현장이 실제로 그랬다), 그다음이 **이 선언**이다 — 원천 명세보다
709
+ * 이긴다(원천 사본의 값이 낡았을 때 고칠 자리가 여기다). 무엇을 썼는지는 `specCoverage()` 가 밝힌다.
710
+ *
711
+ * ── 조용히 버리지 않는다 ────────────────────────────────────────────────────
712
+ * 읽을 수 없는 값은 던진다. 받아 두고 무시하면 화면은 「넣었습니다」라고 말하고 시뮬은 상수로 도는데,
713
+ * 그 어긋남을 아무도 볼 수 없다(이 시스템에서 가장 비싼 종류의 침묵이다).
714
+ */
715
+ declareDurations(durations: Record<string, IsoDuration> | undefined | null): void;
676
716
  /**
677
717
  * 라우트(공정 순서) — 수율을 거슬러 올릴 때 필요하다. 기본은 모른다(선언 순서를 쓴다).
678
718
  * 생산 정의를 가진 커널이 override 해서 자기 라우트를 답한다.
@@ -727,10 +767,12 @@ export declare abstract class FlowEngine implements TwinKernel {
727
767
  */
728
768
  protected committedDemands(): CommittedDemand[];
729
769
  /**
730
- * task 소요 산출 — 우선순위: **① 추정기(이력 보정) → ② 명세(ISA-95 Duration + 변동) → ③ 도메인 상수.**
770
+ * task 소요 산출 — 우선순위: **① 추정기(이력 보정) → ② 현장 선언(`declareDurations`) →
771
+ * ③ 명세(ISA-95 Duration + 변동) → ④ 도메인 상수.**
731
772
  *
732
773
  * 이 순서인 이유: 실측에서 배운 값이 선언값을 이기고, 선언값이 우리가 코드에 고정한 상수를 이긴다.
733
- * 무엇을 썼는지는 `specCoverage()` 드러낸다 상수를 것이 조용히 넘어가지 않게.
774
+ * 선언이 둘로 갈리는 이유는 권위다 **현장이 정한 값**이 원천이 그려 명세를 이긴다(ADR-0034).
775
+ * 무엇을 썼는지는 `specCoverage()` 로 드러낸다 — 상수를 쓴 것이 조용히 넘어가지 않게.
734
776
  * "얼마"만 소비하고 "경로"는 씬이 소유한다(좌표-free 유지).
735
777
  */
736
778
  protected durationOf(ctx: DurationContext, fallbackMs: number): number;
@@ -67,6 +67,18 @@ export const ATTENTION_PROPERTY = {
67
67
  };
68
68
  /** 선언이 없을 때 쓰는 기본값 — **드러내 둔다**(코드 안에 숨은 상수가 아니라 계약의 일부다). */
69
69
  export const ATTENTION_DEFAULTS = { congestionPct: 90, scrapPct: 15, scrapMinSamples: 10 };
70
+ /**
71
+ * 공정에 선언할 수 있는 것의 **어휘** — 지금은 소요시간 하나다(`declareDurations` 가 읽는다).
72
+ *
73
+ * 자리·설비의 속성처럼 「속성 id」로 부른다: 선언을 받는 문·화면·AI 가 같은 이름을 써야 하고, 그 이름이
74
+ * 두 곳에 적히면 한쪽만 바뀌는 순간 값이 조용히 버려진다.
75
+ *
76
+ * 이 값이 얹히는 대상은 **자리·설비가 아니라 공정 종류**다(`FlowTask.kind` = `OperationDef.key`).
77
+ */
78
+ export const OPERATION_PROPERTY = {
79
+ /** 그 현장에서 이 공정이 걸리는 시간 — 실측이 없을 때 상수를 대신한다. */
80
+ duration: 'operation.duration'
81
+ };
70
82
  export function deriveAttentions(view, acked,
71
83
  /** 지금(ISO) — 납기 판정에 필요하다. **주지 않으면 지연을 판정하지 않는다**(모르면 판단하지 않는다). */
72
84
  nowIso,
@@ -311,6 +323,13 @@ export class FlowEngine {
311
323
  * 도메인 정의에서 실어 온다(`loadOperations`). 없으면 커널 기본값을 쓰고 그 사실을 `specCoverage()` 가 밝힌다.
312
324
  */
313
325
  operationSpecs = new Map();
326
+ /**
327
+ * 현장이 선언한 소요시간(작업 종류 → ms) — `declareDurations` 로 들어온다.
328
+ *
329
+ * 명세 행과 **따로** 두는 이유: 행이 없는 종류에도 시간을 줄 수 있어야 하고(창고·야드 트윈에는 행이
330
+ * 없다), 행을 지어 만들면 지어낸 `intent` 가 능력 계산까지 오염시킨다.
331
+ */
332
+ localDurations = new Map();
314
333
  /** 관측 구동(P0) — 이벤트를 접는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
315
334
  observer;
316
335
  /** 관측분이 아직 커널 상태로 옮겨지지 않았다 — 스냅샷·fork 직전에 한 번만 옮긴다. */
@@ -1425,6 +1444,39 @@ export class FlowEngine {
1425
1444
  if (o?.key)
1426
1445
  this.operationSpecs.set(o.key, o);
1427
1446
  }
1447
+ /**
1448
+ * **현장이 정한 소요시간** — 종류별로 시간만 받는다(2026-08-18).
1449
+ *
1450
+ * ── 왜 명세 행과 따로 받나 ──────────────────────────────────────────────────
1451
+ * 창고·야드 트윈에는 오퍼레이션 명세 행이 **아예 없다**(실측: 35개 트윈 중 WMS·YMS 전부 0개). 그런데
1452
+ * 그 트윈들도 `putaway`·`pick`·`spot`·`dwell` 을 돌리고, 시간은 커널 상수에서 온다 — 그 현장이 실제로
1453
+ * 몇 분 걸리는지 말할 문이 없었다.
1454
+ *
1455
+ * 명세 행을 지어 만들 수는 없다: `OperationDef` 는 `label`·`intent` 를 요구하고(ISA-95
1456
+ * `OperationsSegment`), 그 둘은 **지어내면 다른 답까지 오염시킨다**(`capacity()`·능력 보고가 의도별로
1457
+ * 자원을 센다). 「몇 분 걸리나」를 말하려고 「무슨 종류의 작업인가」를 발명하지 않는다.
1458
+ *
1459
+ * ── 순서: 실측 > 현장 선언 > 원천 명세 > 상수 ──────────────────────────────
1460
+ * 이력에서 배운 값이 가장 강하고(그 현장이 실제로 그랬다), 그다음이 **이 선언**이다 — 원천 명세보다
1461
+ * 이긴다(원천 사본의 값이 낡았을 때 고칠 자리가 여기다). 무엇을 썼는지는 `specCoverage()` 가 밝힌다.
1462
+ *
1463
+ * ── 조용히 버리지 않는다 ────────────────────────────────────────────────────
1464
+ * 읽을 수 없는 값은 던진다. 받아 두고 무시하면 화면은 「넣었습니다」라고 말하고 시뮬은 상수로 도는데,
1465
+ * 그 어긋남을 아무도 볼 수 없다(이 시스템에서 가장 비싼 종류의 침묵이다).
1466
+ */
1467
+ declareDurations(durations) {
1468
+ for (const [kind, text] of Object.entries(durations ?? {})) {
1469
+ const key = String(kind ?? '').trim();
1470
+ if (!key)
1471
+ throw new Error('declareDurations: an operation kind is empty — say which operation the duration belongs to');
1472
+ const ms = parseIsoDuration(text);
1473
+ if (ms === undefined)
1474
+ throw new Error(`declareDurations: "${text}" is not an ISO 8601 duration (operation "${key}") — the value would be dropped without a trace`);
1475
+ if (ms <= 0)
1476
+ throw new Error(`declareDurations: operation "${key}" cannot take ${ms}ms — a task with no duration never finishes`);
1477
+ this.localDurations.set(key, ms);
1478
+ }
1479
+ }
1428
1480
  /**
1429
1481
  * 라우트(공정 순서) — 수율을 거슬러 올릴 때 필요하다. 기본은 모른다(선언 순서를 쓴다).
1430
1482
  * 생산 정의를 가진 커널이 override 해서 자기 라우트를 답한다.
@@ -1528,10 +1580,12 @@ export class FlowEngine {
1528
1580
  return out;
1529
1581
  }
1530
1582
  /**
1531
- * task 소요 산출 — 우선순위: **① 추정기(이력 보정) → ② 명세(ISA-95 Duration + 변동) → ③ 도메인 상수.**
1583
+ * task 소요 산출 — 우선순위: **① 추정기(이력 보정) → ② 현장 선언(`declareDurations`) →
1584
+ * ③ 명세(ISA-95 Duration + 변동) → ④ 도메인 상수.**
1532
1585
  *
1533
1586
  * 이 순서인 이유: 실측에서 배운 값이 선언값을 이기고, 선언값이 우리가 코드에 고정한 상수를 이긴다.
1534
- * 무엇을 썼는지는 `specCoverage()` 드러낸다 상수를 것이 조용히 넘어가지 않게.
1587
+ * 선언이 둘로 갈리는 이유는 권위다 **현장이 정한 값**이 원천이 그려 명세를 이긴다(ADR-0034).
1588
+ * 무엇을 썼는지는 `specCoverage()` 로 드러낸다 — 상수를 쓴 것이 조용히 넘어가지 않게.
1535
1589
  * "얼마"만 소비하고 "경로"는 씬이 소유한다(좌표-free 유지).
1536
1590
  */
1537
1591
  durationOf(ctx, fallbackMs) {
@@ -1548,6 +1602,16 @@ export class FlowEngine {
1548
1602
  return this.sampleSpread(estimated.meanMs, estimated.spread);
1549
1603
  }
1550
1604
  const spec = this.operationSpecs.get(ctx.kind);
1605
+ /*
1606
+ * **현장이 정한 시간이 원천 명세를 이긴다** — 원천 사본의 값이 낡았을 때 고칠 자리가 그것뿐이다
1607
+ * (ADR-0034: 우리가 정한 값이 원천이 그려 준 값을 이긴다). 변동은 명세 행이 말한 것을 그대로 쓴다 —
1608
+ * 평균만 우리가 고쳤다고 퍼짐을 0 으로 만들면 줄이 사라진다.
1609
+ */
1610
+ const local = this.localDurations.get(ctx.kind);
1611
+ if (local !== undefined) {
1612
+ this.noteSpecUse(ctx.kind, 'declared');
1613
+ return this.applyVariability(local, spec?.variability);
1614
+ }
1551
1615
  const declared = parseIsoDuration(spec?.duration);
1552
1616
  if (declared === undefined) {
1553
1617
  this.noteSpecUse(ctx.kind, 'default');
@@ -55,6 +55,7 @@ __export(index_exports, {
55
55
  MES_PRODUCT_GTINS: () => MES_PRODUCT_GTINS,
56
56
  MES_TYPES: () => MES_TYPES,
57
57
  MesKernel: () => MesKernel,
58
+ OPERATION_PROPERTY: () => OPERATION_PROPERTY,
58
59
  OP_EVENT: () => OP_EVENT,
59
60
  OP_PARAM: () => OP_PARAM,
60
61
  ObservedReducer: () => ObservedReducer,
@@ -2963,6 +2964,10 @@ var ATTENTION_PROPERTY = {
2963
2964
  scrapMinSamples: "attention.scrapMinSamples"
2964
2965
  };
2965
2966
  var ATTENTION_DEFAULTS = { congestionPct: 90, scrapPct: 15, scrapMinSamples: 10 };
2967
+ var OPERATION_PROPERTY = {
2968
+ /** 그 현장에서 이 공정이 걸리는 시간 — 실측이 없을 때 상수를 대신한다. */
2969
+ duration: "operation.duration"
2970
+ };
2966
2971
  function deriveAttentions(view, acked, nowIso, thresholds) {
2967
2972
  const out = [];
2968
2973
  for (const m of view.equipment) {
@@ -3163,6 +3168,13 @@ var FlowEngine = class {
3163
3168
  * 도메인 정의에서 실어 온다(`loadOperations`). 없으면 커널 기본값을 쓰고 그 사실을 `specCoverage()` 가 밝힌다.
3164
3169
  */
3165
3170
  operationSpecs = /* @__PURE__ */ new Map();
3171
+ /**
3172
+ * 현장이 선언한 소요시간(작업 종류 → ms) — `declareDurations` 로 들어온다.
3173
+ *
3174
+ * 명세 행과 **따로** 두는 이유: 행이 없는 종류에도 시간을 줄 수 있어야 하고(창고·야드 트윈에는 행이
3175
+ * 없다), 행을 지어 만들면 지어낸 `intent` 가 능력 계산까지 오염시킨다.
3176
+ */
3177
+ localDurations = /* @__PURE__ */ new Map();
3166
3178
  /** 관측 구동(P0) — 이벤트를 접는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
3167
3179
  observer;
3168
3180
  /** 관측분이 아직 커널 상태로 옮겨지지 않았다 — 스냅샷·fork 직전에 한 번만 옮긴다. */
@@ -4170,6 +4182,37 @@ var FlowEngine = class {
4170
4182
  loadOperations(ops = []) {
4171
4183
  for (const o of ops) if (o?.key) this.operationSpecs.set(o.key, o);
4172
4184
  }
4185
+ /**
4186
+ * **현장이 정한 소요시간** — 종류별로 시간만 받는다(2026-08-18).
4187
+ *
4188
+ * ── 왜 명세 행과 따로 받나 ──────────────────────────────────────────────────
4189
+ * 창고·야드 트윈에는 오퍼레이션 명세 행이 **아예 없다**(실측: 35개 트윈 중 WMS·YMS 전부 0개). 그런데
4190
+ * 그 트윈들도 `putaway`·`pick`·`spot`·`dwell` 을 돌리고, 시간은 커널 상수에서 온다 — 그 현장이 실제로
4191
+ * 몇 분 걸리는지 말할 문이 없었다.
4192
+ *
4193
+ * 명세 행을 지어 만들 수는 없다: `OperationDef` 는 `label`·`intent` 를 요구하고(ISA-95
4194
+ * `OperationsSegment`), 그 둘은 **지어내면 다른 답까지 오염시킨다**(`capacity()`·능력 보고가 의도별로
4195
+ * 자원을 센다). 「몇 분 걸리나」를 말하려고 「무슨 종류의 작업인가」를 발명하지 않는다.
4196
+ *
4197
+ * ── 순서: 실측 > 현장 선언 > 원천 명세 > 상수 ──────────────────────────────
4198
+ * 이력에서 배운 값이 가장 강하고(그 현장이 실제로 그랬다), 그다음이 **이 선언**이다 — 원천 명세보다
4199
+ * 이긴다(원천 사본의 값이 낡았을 때 고칠 자리가 여기다). 무엇을 썼는지는 `specCoverage()` 가 밝힌다.
4200
+ *
4201
+ * ── 조용히 버리지 않는다 ────────────────────────────────────────────────────
4202
+ * 읽을 수 없는 값은 던진다. 받아 두고 무시하면 화면은 「넣었습니다」라고 말하고 시뮬은 상수로 도는데,
4203
+ * 그 어긋남을 아무도 볼 수 없다(이 시스템에서 가장 비싼 종류의 침묵이다).
4204
+ */
4205
+ declareDurations(durations) {
4206
+ for (const [kind, text] of Object.entries(durations ?? {})) {
4207
+ const key = String(kind ?? "").trim();
4208
+ if (!key) throw new Error("declareDurations: an operation kind is empty \u2014 say which operation the duration belongs to");
4209
+ const ms2 = parseIsoDuration(text);
4210
+ if (ms2 === void 0)
4211
+ throw new Error(`declareDurations: "${text}" is not an ISO 8601 duration (operation "${key}") \u2014 the value would be dropped without a trace`);
4212
+ if (ms2 <= 0) throw new Error(`declareDurations: operation "${key}" cannot take ${ms2}ms \u2014 a task with no duration never finishes`);
4213
+ this.localDurations.set(key, ms2);
4214
+ }
4215
+ }
4173
4216
  /**
4174
4217
  * 라우트(공정 순서) — 수율을 거슬러 올릴 때 필요하다. 기본은 모른다(선언 순서를 쓴다).
4175
4218
  * 생산 정의를 가진 커널이 override 해서 자기 라우트를 답한다.
@@ -4271,10 +4314,12 @@ var FlowEngine = class {
4271
4314
  return out;
4272
4315
  }
4273
4316
  /**
4274
- * task 소요 산출 — 우선순위: **① 추정기(이력 보정) → ② 명세(ISA-95 Duration + 변동) → ③ 도메인 상수.**
4317
+ * task 소요 산출 — 우선순위: **① 추정기(이력 보정) → ② 현장 선언(`declareDurations`) →
4318
+ * ③ 명세(ISA-95 Duration + 변동) → ④ 도메인 상수.**
4275
4319
  *
4276
4320
  * 이 순서인 이유: 실측에서 배운 값이 선언값을 이기고, 선언값이 우리가 코드에 고정한 상수를 이긴다.
4277
- * 무엇을 썼는지는 `specCoverage()` 드러낸다 상수를 것이 조용히 넘어가지 않게.
4321
+ * 선언이 둘로 갈리는 이유는 권위다 **현장이 정한 값**이 원천이 그려 명세를 이긴다(ADR-0034).
4322
+ * 무엇을 썼는지는 `specCoverage()` 로 드러낸다 — 상수를 쓴 것이 조용히 넘어가지 않게.
4278
4323
  * "얼마"만 소비하고 "경로"는 씬이 소유한다(좌표-free 유지).
4279
4324
  */
4280
4325
  durationOf(ctx, fallbackMs) {
@@ -4288,6 +4333,11 @@ var FlowEngine = class {
4288
4333
  return this.sampleSpread(estimated.meanMs, estimated.spread);
4289
4334
  }
4290
4335
  const spec = this.operationSpecs.get(ctx.kind);
4336
+ const local = this.localDurations.get(ctx.kind);
4337
+ if (local !== void 0) {
4338
+ this.noteSpecUse(ctx.kind, "declared");
4339
+ return this.applyVariability(local, spec?.variability);
4340
+ }
4291
4341
  const declared = parseIsoDuration(spec?.duration);
4292
4342
  if (declared === void 0) {
4293
4343
  this.noteSpecUse(ctx.kind, "default");
@@ -7276,6 +7326,7 @@ function retiredVocabularyIn(line) {
7276
7326
  MES_PRODUCT_GTINS,
7277
7327
  MES_TYPES,
7278
7328
  MesKernel,
7329
+ OPERATION_PROPERTY,
7279
7330
  OP_EVENT,
7280
7331
  OP_PARAM,
7281
7332
  ObservedReducer,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/twin-kernel",
3
- "version": "0.7.28",
3
+ "version": "0.7.30",
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": {