@operato/twin-kernel 0.7.39 → 0.7.41

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.
@@ -46,13 +46,11 @@ __export(index_exports, {
46
46
  FlowEngine: () => FlowEngine,
47
47
  GUARD_PRAGMA: () => GUARD_PRAGMA,
48
48
  ILMD_ATTR: () => ILMD_ATTR,
49
+ ItemStore: () => ItemStore,
49
50
  LOCATION_SATURATION_NEAR: () => LOCATION_SATURATION_NEAR,
50
51
  MATERIAL_PROPERTY: () => MATERIAL_PROPERTY,
51
52
  MES_BIZSTEP: () => MES_BIZSTEP,
52
53
  MES_LOCATION_TYPES: () => MES_LOCATION_TYPES,
53
- MES_PART_GTINS: () => MES_PART_GTINS,
54
- MES_PRODUCTS: () => MES_PRODUCTS,
55
- MES_PRODUCT_GTINS: () => MES_PRODUCT_GTINS,
56
54
  MES_TYPES: () => MES_TYPES,
57
55
  MesKernel: () => MesKernel,
58
56
  OPERATION_PROPERTY: () => OPERATION_PROPERTY,
@@ -86,9 +84,11 @@ __export(index_exports, {
86
84
  axisAppliesTo: () => axisAppliesTo,
87
85
  axisInfo: () => axisInfo,
88
86
  axisSource: () => axisSource,
87
+ bizTransactionUri: () => bizTransactionUri,
89
88
  capabilitiesForType: () => capabilitiesForType,
90
89
  capabilityOf: () => capabilityOf,
91
90
  classClosure: () => classClosure,
91
+ classIdentifierViolation: () => classIdentifierViolation,
92
92
  commandsOf: () => commandsOf,
93
93
  compareStates: () => compareStates,
94
94
  computeOee: () => computeOee,
@@ -112,6 +112,7 @@ __export(index_exports, {
112
112
  generationFractionAt: () => generationFractionAt,
113
113
  graiUri: () => graiUri,
114
114
  hierarchyOf: () => hierarchyOf,
115
+ identityGroundingOf: () => identityGroundingOf,
115
116
  inWorkCalendar: () => inWorkCalendar,
116
117
  inWorkCalendarAt: () => inWorkCalendarAt,
117
118
  ingest: () => ingest,
@@ -262,7 +263,7 @@ function hierarchyOf(s, levelOfType) {
262
263
  function testPassedAt(r, at) {
263
264
  if (r.result !== "pass") return false;
264
265
  if (!r.expiresAt || !at) return r.result === "pass";
265
- return Date.parse(at) <= Date.parse(r.expiresAt);
266
+ return parsedMs(at) <= parsedMs(r.expiresAt);
266
267
  }
267
268
  function meetsTests(required, results, at) {
268
269
  if (!required.length) return true;
@@ -325,8 +326,8 @@ function priorityRank(p) {
325
326
  }
326
327
  function dueStatusOf(x, nowIso) {
327
328
  if (!x.endTime || !nowIso) return void 0;
328
- const due = Date.parse(x.endTime);
329
- const now = Date.parse(nowIso);
329
+ const due = parsedMs(x.endTime);
330
+ const now = parsedMs(nowIso);
330
331
  if (!Number.isFinite(due) || !Number.isFinite(now)) return void 0;
331
332
  return now > due ? "late" : "on-time";
332
333
  }
@@ -494,7 +495,7 @@ function capabilityOf(r, ctx) {
494
495
  if (r.held) return { available: false, reason: "held" };
495
496
  if (r.status === "down") return { available: false, reason: "down" };
496
497
  if (at) {
497
- const ms2 = Date.parse(at);
498
+ const ms2 = parsedMs(at);
498
499
  if (Number.isFinite(ms2)) {
499
500
  const why = offCalendarReasonAt(r, ms2, ctx?.utcOffsetMinutes);
500
501
  if (why === "non-working") return { available: false, reason: "resting" };
@@ -604,6 +605,24 @@ function readBoardAssets(def) {
604
605
  const list = def.assets ?? [];
605
606
  return list.map((a) => normalizeHomeLocation(a));
606
607
  }
608
+ function identityGroundingOf(spec, kernelConstantPrefix) {
609
+ const declared = spec?.identity?.namespaces?.filter((n) => typeof n === "string" && n.length > 0) ?? [];
610
+ if (spec?.identity?.issuedClaim && declared.length) {
611
+ return { grounding: "issued", basis: "claimed-issued", namespaces: [...declared] };
612
+ }
613
+ if (declared.length) return { grounding: "declared", basis: "declared-namespace", namespaces: [...declared] };
614
+ if (spec?.companyPrefix && spec.binding) {
615
+ return { grounding: "declared", basis: "gs1-shaped", namespaces: gs1Namespaces(spec.companyPrefix) };
616
+ }
617
+ return {
618
+ grounding: "fabricated",
619
+ basis: "kernel-constant",
620
+ namespaces: kernelConstantPrefix ? gs1Namespaces(kernelConstantPrefix) : []
621
+ };
622
+ }
623
+ function gs1Namespaces(prefix) {
624
+ return [`urn:epc:idpat:sgtin:${prefix}.`, `urn:epc:class:lgtin:${prefix}.`, `urn:epc:id:sgtin:${prefix}.`];
625
+ }
607
626
 
608
627
  // src/scenario-validate.ts
609
628
  var DISTRIBUTIONS = /* @__PURE__ */ new Set(["poisson", "uniform", "constant", "profile"]);
@@ -677,6 +696,7 @@ function validateDomainDefinition(def) {
677
696
  const locationKeys2 = new Set((def.locationTypes || []).map((n) => n.key));
678
697
  const resKeys = new Set((def.resourceTypes || []).map((r) => r.key));
679
698
  const matKeys = new Set((def.materials || []).map((m) => m.key));
699
+ const matByKey = new Map((def.materials || []).map((m) => [m.key, m]));
680
700
  const opKeys = new Set((def.operations || []).map((o) => o.key));
681
701
  const routeKeys = new Set((def.routes || []).map((r) => r.key));
682
702
  for (const [name, arr] of [["locationTypes", def.locationTypes], ["resourceTypes", def.resourceTypes], ["materials", def.materials], ["operations", def.operations], ["routes", def.routes], ["recipes", def.recipes]]) {
@@ -698,6 +718,9 @@ function validateDomainDefinition(def) {
698
718
  for (const p of [...rc.inputs || [], ...rc.outputs || []]) {
699
719
  if (!matKeys.has(p.material)) v.push(`recipe '${rc.key}' material '${p.material}' \uBBF8\uC815\uC758`);
700
720
  if (typeof p.qty !== "number" || p.qty <= 0) v.push(`recipe '${rc.key}' material '${p.material}' qty \uBD80\uC815`);
721
+ const mat = matByKey.get(p.material);
722
+ if (mat && !mat.locationType) v.push(`recipe '${rc.key}' material '${p.material}' locationType \uBBF8\uC120\uC5B8`);
723
+ if (mat?.locationType && !locationKeys2.has(mat.locationType)) v.push(`recipe '${rc.key}' material '${p.material}' locationType '${mat.locationType}' \uBBF8\uC815\uC758`);
701
724
  }
702
725
  }
703
726
  return v;
@@ -923,6 +946,16 @@ function parseEpc(uri) {
923
946
  function gdtiUri(companyPrefix, docType, serial) {
924
947
  return `urn:epc:id:gdti:${companyPrefix}.${docType}.${serial}`;
925
948
  }
949
+ function bizTransactionUri(namespace, transId) {
950
+ const ns = namespace?.trim();
951
+ if (!ns) return void 0;
952
+ const id = String(transId);
953
+ if (!id || id.includes("/") || id.includes(":")) return void 0;
954
+ if (/^https?:\/\/[^/\s]+/.test(ns)) return `${ns.replace(/\/+$/, "")}/bt/${id}`;
955
+ if (/^urn:epc(global)?:/.test(ns)) return void 0;
956
+ if (/^urn:[^:\s]+/.test(ns)) return `${ns.replace(/:+$/, "")}:bt:${id}`;
957
+ return void 0;
958
+ }
926
959
  function header(type, eventTime, bizStep, opts) {
927
960
  const h = {
928
961
  "@context": EPCIS_CONTEXT,
@@ -992,6 +1025,17 @@ function transformationEvent(p) {
992
1025
  var ISO_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
993
1026
  var TZ_RE = /^[+-]\d{2}:\d{2}$/;
994
1027
  var ACTIONS = ["ADD", "OBSERVE", "DELETE"];
1028
+ var EPC_CLASS_PREFIXES = ["urn:epc:idpat:", "urn:epc:class:"];
1029
+ var DL_CANONICAL = "https://id.gs1.org/";
1030
+ var CBV_URL_CLASS = /^https?:\/\/[^/\s]+\/(?:[^/\s]+\/)*class\/[^/\s]+$/;
1031
+ var CBV_URN_CLASS = /^urn:[^:\s]+:(?:[^:\s]+:)*class:[^:\s]+$/;
1032
+ function classIdentifierViolation(epcClass) {
1033
+ if (!epcClass) return `quantity epcClass \uBD80\uC815: ${epcClass}`;
1034
+ if (EPC_CLASS_PREFIXES.some((p) => epcClass.startsWith(p))) return void 0;
1035
+ if (epcClass.startsWith(DL_CANONICAL)) return void 0;
1036
+ if (CBV_URL_CLASS.test(epcClass) || CBV_URN_CLASS.test(epcClass)) return void 0;
1037
+ return `quantity epcClass \uBD80\uC815: ${epcClass} \u2014 \uD074\uB798\uC2A4 \uC2DD\uBCC4\uC790\uB294 \uC18C\uC720 \uAD8C\uD55C\uC744 \uB9D0\uD560 \uC218 \uC788\uB294 \uBAA8\uC591\uC774\uC5B4\uC57C \uD55C\uB2E4 (EPC \uD074\uB798\uC2A4 urn:epc:idpat:/urn:epc:class: \xB7 CBV \xA78.3.4 http(s)://<\uB3C4\uBA54\uC778>/**/class/<id> \xB7 CBV \xA78.3.3 urn:<\uC774\uB984\uACF5\uAC04>:**:class:<id>)`;
1038
+ }
995
1039
  function validateEpcisEvent(e) {
996
1040
  const v = [];
997
1041
  if (e["@context"] !== EPCIS_CONTEXT) v.push("@context \uB204\uB77D/\uBD88\uC77C\uCE58");
@@ -1059,7 +1103,8 @@ function validateEpcisEvent(e) {
1059
1103
  "outputQuantityList" in e ? e.outputQuantityList : void 0
1060
1104
  ];
1061
1105
  for (const list of qtyLists) for (const q of list ?? []) {
1062
- if (!q.epcClass?.startsWith("urn:epc:idpat:") && !q.epcClass?.startsWith("urn:epc:class:")) v.push(`quantity epcClass \uBD80\uC815: ${q.epcClass}`);
1106
+ const bad2 = classIdentifierViolation(q.epcClass);
1107
+ if (bad2) v.push(bad2);
1063
1108
  const hasQty = q.quantity !== void 0 && q.quantity !== null;
1064
1109
  if (!hasQty) {
1065
1110
  if (q.uom !== void 0) v.push("quantity \uC5C6\uC73C\uBA74 uom \uB3C4 \uC5C6\uC5B4\uC57C \uD55C\uB2E4(\uC218\uB7C9 \uBBF8\uC9C0\uC815)");
@@ -1376,6 +1421,8 @@ var ObservedReducer = class {
1376
1421
  /* 진행 중 확보분과 거래번호 — **미러도 같은 필드를 채운다.** 시뮬만 아는 사실을 두면
1377
1422
  그 위에서 세운 예측이 두 구동에서 달라진다(패리티 검사가 지키는 것이 이것이다). */
1378
1423
  ...d.gtin ? { gtin: d.gtin } : {},
1424
+ /* 어느 레시피로 만드는가 — 품목만으로는 「무엇으로」가 남지 않는다(같은 품목에 대체 레시피). */
1425
+ ...d.recipeKey ? { recipeKey: d.recipeKey } : {},
1379
1426
  ...d.allocated?.length ? { allocated: d.allocated.slice() } : {},
1380
1427
  ...d.bizTransaction ? { bizTransaction: d.bizTransaction } : {},
1381
1428
  /* 약속해 둔 자리·시각창도 채운다 — 시뮬만 알면 미러 위 예측이 「어디로 들일지」를 모른다.
@@ -1449,7 +1496,7 @@ var ObservedReducer = class {
1449
1496
  const cur = this.items.get(ev.parentID);
1450
1497
  if (!cur) this.pendingQuantities.set(ev.parentID, [...ev.childQuantityList]);
1451
1498
  else {
1452
- this.items.set(cur.subLotId ?? cur.epc, this.withContainedQuantity(cur, q2, ev.childQuantityList));
1499
+ this.items.set(itemKeyOf(cur), this.withContainedQuantity(cur, q2, ev.childQuantityList));
1453
1500
  this.qtyAggregation.set(ev.parentID, [...ev.childQuantityList]);
1454
1501
  }
1455
1502
  }
@@ -1475,7 +1522,7 @@ var ObservedReducer = class {
1475
1522
  const cur = this.items.get(ev.parentID);
1476
1523
  if (cur) {
1477
1524
  const { gtin, gtinKey, qty, uom, quantities, lot, ...rest } = cur;
1478
- this.items.set(cur.subLotId ?? cur.epc, { ...rest, qty: 1 });
1525
+ this.items.set(itemKeyOf(cur), { ...rest, qty: 1 });
1479
1526
  }
1480
1527
  }
1481
1528
  this.pendingQuantities.delete(ev.parentID);
@@ -1533,7 +1580,7 @@ var ObservedReducer = class {
1533
1580
  return void 0;
1534
1581
  }
1535
1582
  mergeItem(epc, patch, q, all, subLotId) {
1536
- const cur = this.items.get(subLotId ?? epc);
1583
+ const cur = this.items.get(itemKeyOf({ epc, subLotId }));
1537
1584
  const parsedClass = q?.epcClass ? parseEpc(q.epcClass) : void 0;
1538
1585
  const parsedSelf = parseEpc(epc);
1539
1586
  const pendingQty = this.pendingQuantities.get(epc);
@@ -2822,8 +2869,21 @@ function mapRecordChecked(record, mapping, eventTime) {
2822
2869
  };
2823
2870
  }
2824
2871
  const epc = resolve(mapping.epc, record, "epc", errors);
2872
+ const quantityList = resolveQuantityList(mapping.quantityList, record, "quantityList", errors);
2873
+ if (!epc && !quantityList.length) {
2874
+ errors.push("epc \uC640 quantityList \uAC00 \uB458 \uB2E4 \uBE44\uC5C8\uB2E4 \u2014 \uAC1C\uCCB4 \uC2DD\uBCC4\uC790\uB098 \uD074\uB798\uC2A4+\uC218\uB7C9 \uC911 \uD558\uB098\uB294 \uC788\uC5B4\uC57C \uD55C\uB2E4");
2875
+ }
2825
2876
  return {
2826
- event: objectEvent({ eventTime, action, bizStep, disposition, epcList: epc ? [epc] : [], readPoint, bizLocation }),
2877
+ event: objectEvent({
2878
+ eventTime,
2879
+ action,
2880
+ bizStep,
2881
+ disposition,
2882
+ epcList: epc ? [epc] : [],
2883
+ ...quantityList.length ? { quantityList } : {},
2884
+ readPoint,
2885
+ bizLocation
2886
+ }),
2827
2887
  errors
2828
2888
  };
2829
2889
  }
@@ -3262,10 +3322,124 @@ function energyFieldsOf(m) {
3262
3322
  }
3263
3323
  return out;
3264
3324
  }
3325
+ var ItemStore = class _ItemStore {
3326
+ map = /* @__PURE__ */ new Map();
3327
+ byLocation = /* @__PURE__ */ new Map();
3328
+ get size() {
3329
+ return this.map.size;
3330
+ }
3331
+ get(key) {
3332
+ return this.map.get(key);
3333
+ }
3334
+ has(key) {
3335
+ return this.map.has(key);
3336
+ }
3337
+ values() {
3338
+ return this.map.values();
3339
+ }
3340
+ keys() {
3341
+ return this.map.keys();
3342
+ }
3343
+ entries() {
3344
+ return this.map.entries();
3345
+ }
3346
+ [Symbol.iterator]() {
3347
+ return this.map[Symbol.iterator]();
3348
+ }
3349
+ forEach(fn) {
3350
+ this.map.forEach((v, k) => fn(v, k));
3351
+ }
3352
+ set(key, item) {
3353
+ const prev = this.map.get(key);
3354
+ if (prev) this.unindex(key, prev.location);
3355
+ this.map.set(key, item);
3356
+ this.index(key, item.location);
3357
+ return this;
3358
+ }
3359
+ delete(key) {
3360
+ const prev = this.map.get(key);
3361
+ if (prev) this.unindex(key, prev.location);
3362
+ return this.map.delete(key);
3363
+ }
3364
+ clear() {
3365
+ this.map.clear();
3366
+ this.byLocation.clear();
3367
+ }
3368
+ /**
3369
+ * 물품을 다른 자리로 옮긴다 — **색인이 함께 움직이는 유일한 통로.**
3370
+ *
3371
+ * 없는 물품을 옮기라는 요청은 조용히 넘기지 않는다: 그 요청을 낸 쪽의 계산이 이미 어긋난 것이고,
3372
+ * 지나가면 그 뒤의 이동·처분이 엉뚱한 자리에 적힌다.
3373
+ */
3374
+ relocate(key, to) {
3375
+ const it = this.map.get(key);
3376
+ if (!it) throw new Error(`relocate: \uBB3C\uD488 '${key}' \uC774 \uC0C1\uD0DC\uC5D0 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 \uC5C6\uB294 \uAC83\uC744 \uC62E\uAE38 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4`);
3377
+ if (it.location !== to) {
3378
+ this.unindex(key, it.location);
3379
+ it.location = to;
3380
+ this.index(key, to);
3381
+ }
3382
+ return it;
3383
+ }
3384
+ /**
3385
+ * 사본 — **자기 복제 방법을 스스로 안다.**
3386
+ *
3387
+ * `fork()` 는 `Map` 만 알고 나머지는 `structuredClone` 으로 복제한다. 그 함수는 클래스의 메서드를
3388
+ * 복제하지 못하므로(평범한 객체가 된다), 저장소가 복제를 맡지 않으면 사본의 물품 조회가 통째로
3389
+ * 깨진다 — 실제로 그렇게 42건이 붉어졌다. 복제 방법을 아는 객체가 스스로 복제한다.
3390
+ */
3391
+ clone() {
3392
+ const out = new _ItemStore();
3393
+ for (const [k, it] of this.map) out.set(k, structuredClone(it));
3394
+ return out;
3395
+ }
3396
+ /** 그 자리에 있는 물품들 — 색인이 답한다(전체 순회가 아니다). */
3397
+ at(location) {
3398
+ const keys = this.byLocation.get(location);
3399
+ if (!keys) return [];
3400
+ const out = [];
3401
+ for (const k of keys) {
3402
+ const it = this.map.get(k);
3403
+ if (it) out.push(it);
3404
+ }
3405
+ return out;
3406
+ }
3407
+ /**
3408
+ * 색인이 맵과 어긋난 자리 — **시험이 쓰는 확인 통로**(전체를 다시 세므로 비싸다).
3409
+ *
3410
+ * 어긋남은 조용한 결함이다(자재가 있는데 없다고 판정된다). 그래서 「어긋나지 않을 것이다」를 믿지 않고
3411
+ * 시나리오를 돌린 뒤 이 값으로 확인한다.
3412
+ */
3413
+ indexDrift() {
3414
+ const drift = [];
3415
+ for (const [key, it] of this.map) {
3416
+ if (!this.byLocation.get(it.location)?.has(key)) drift.push(`${key} \uC774 '${it.location}' \uC0C9\uC778\uC5D0 \uC5C6\uB2E4`);
3417
+ }
3418
+ for (const [loc, keys] of this.byLocation) {
3419
+ for (const k of keys) {
3420
+ const it = this.map.get(k);
3421
+ if (!it) drift.push(`${k} \uC774 \uC9C0\uC6CC\uC84C\uB294\uB370 '${loc}' \uC0C9\uC778\uC5D0 \uB0A8\uC544 \uC788\uB2E4`);
3422
+ else if (it.location !== loc) drift.push(`${k} \uC740 '${it.location}' \uC5D0 \uC788\uB294\uB370 '${loc}' \uC0C9\uC778\uC5D0 \uC788\uB2E4`);
3423
+ }
3424
+ }
3425
+ return drift;
3426
+ }
3427
+ index(key, location) {
3428
+ const set = this.byLocation.get(location) ?? /* @__PURE__ */ new Set();
3429
+ set.add(key);
3430
+ this.byLocation.set(location, set);
3431
+ }
3432
+ unindex(key, location) {
3433
+ const set = this.byLocation.get(location);
3434
+ if (!set) return;
3435
+ set.delete(key);
3436
+ if (!set.size) this.byLocation.delete(location);
3437
+ }
3438
+ };
3265
3439
  var FlowEngine = class {
3266
3440
  tenantId;
3267
3441
  locations = /* @__PURE__ */ new Map();
3268
- items = /* @__PURE__ */ new Map();
3442
+ items = new ItemStore();
3269
3443
  equipment = /* @__PURE__ */ new Map();
3270
3444
  /** 등급 정의(표준 `<X>Class`) — 상속·유효기간 판정의 재료. 선언 안 하면 비어 있고, 소속 그대로 판정한다. */
3271
3445
  classDefs = {};
@@ -3329,6 +3503,21 @@ var FlowEngine = class {
3329
3503
  /** 관측 구동(P0) — 이벤트를 접는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
3330
3504
  observer;
3331
3505
  /** 관측분이 아직 커널 상태로 옮겨지지 않았다 — 스냅샷·fork 직전에 한 번만 옮긴다. */
3506
+ /**
3507
+ * **관측 모드에서 원본과 어긋난 횟수** — 받아들였지만 사실이 맞지 않았다.
3508
+ *
3509
+ * 미러는 원본을 비추는 쪽이라 어긋남을 만나도 멈추지 않는다(멈추면 원본의 한 건이 트윈 전체를
3510
+ * 세운다). 그래서 **세어 둔다** — 세지 않으면 그 결함이 아무 데도 드러나지 않고, 화면은 어긋난 적이
3511
+ * 없는 트윈과 구별되지 않는다.
3512
+ */
3513
+ /**
3514
+ * **씨앗이 심지 못한 참조의 수** — 원본이 말했지만 그 물품이 스냅샷에 없었다.
3515
+ *
3516
+ * 0 이 아니면 이 트윈의 상태는 원본의 일부를 담지 못했다는 뜻이다. 예측·집계가 그 사실을 모르면
3517
+ * 부족한 씨앗 위에서 낸 답을 완전한 답으로 읽는다.
3518
+ */
3519
+ seedDanglingRefs = 0;
3520
+ transformInputsAbsent = 0;
3332
3521
  observedDirty = false;
3333
3522
  observeMode = false;
3334
3523
  /** 관측 구동이 투영기를 세울 때 필요한 원본 보드(구조는 이벤트가 아니라 마스터에서 온다). */
@@ -3533,6 +3722,24 @@ var FlowEngine = class {
3533
3722
  for (const id of snap?.acked ?? []) this._acked.add(id);
3534
3723
  for (const e of snap?.attentionSince ?? []) if (e?.id && e.since) this._attentionSince.set(e.id, e.since);
3535
3724
  }
3725
+ /**
3726
+ * 씨앗의 확보분 중 **이 트윈에 실제로 있는 것만** 남긴다 — 없는 것은 세고 버린다.
3727
+ *
3728
+ * 원본이 확보분을 말했는데 그 물품이 스냅샷에 없으면, 그 참조는 심는 순간 이미 사실이 아니다.
3729
+ * 심어 두면 나중에 틱에서 드러나고(시뮬은 없는 것을 소비하지 않으므로 멈춘다) 그 판정이 예측
3730
+ * 경로에서 터지면 원본의 빈틈 하나가 답 전체를 서버 오류로 죽인다.
3731
+ *
3732
+ * 그래서 심을 때 맞춘다. **없는 물품을 만들어 채우지 않는다** — 채우면 계보와 재고가 거짓 위에 선다.
3733
+ * 대신 몇 건이 맞지 않았는지 남겨 씨앗이 불완전했다는 사실을 답에 실을 수 있게 한다.
3734
+ */
3735
+ resolvedAllocated(allocated) {
3736
+ const all = allocated ?? [];
3737
+ if (!all.length) return { kept: [], dropped: 0 };
3738
+ const kept = all.filter((epc) => this.items.has(epc));
3739
+ const dropped = all.length - kept.length;
3740
+ this.seedDanglingRefs += dropped;
3741
+ return { kept, dropped };
3742
+ }
3536
3743
  hydrateObserved(snap, orders = []) {
3537
3744
  for (const id of snap.acked ?? []) this._acked.add(id);
3538
3745
  for (const e of snap.attentionSince ?? []) if (e?.id && e.since) this._attentionSince.set(e.id, e.since);
@@ -3645,6 +3852,7 @@ var FlowEngine = class {
3645
3852
  사라진다(같은 사실이 델타·스냅샷·이 변환 **세 길**을 지난다). */
3646
3853
  ...o.allocated?.length ? { allocated: o.allocated } : {},
3647
3854
  ...o.gtin ? { gtin: o.gtin } : {},
3855
+ ...o.recipeKey ? { recipeKey: o.recipeKey } : {},
3648
3856
  ...o.bizTransaction ? { bizTransaction: o.bizTransaction } : {},
3649
3857
  ...o.dockDoor ? { dockDoor: o.dockDoor } : {},
3650
3858
  ...o.windowStartMs !== void 0 ? { windowStartMs: o.windowStartMs } : {}
@@ -3653,6 +3861,7 @@ var FlowEngine = class {
3653
3861
  const lines = (o.lines ?? []).map((l) => ({ gtin: l.gtin, requested: l.requested - (l.fulfilled ?? 0) })).filter((l) => l.requested > 0);
3654
3862
  const remaining = lines.length ? lines.reduce((s, l) => s + l.requested, 0) : Math.max(0, (o.requested ?? 0) - (o.fulfilled ?? 0));
3655
3863
  if (remaining <= 0) continue;
3864
+ const resolved = this.resolvedAllocated(o.allocated);
3656
3865
  this.orders.set(o.orderId, {
3657
3866
  id: o.orderId,
3658
3867
  kind: o.kind,
@@ -3664,11 +3873,15 @@ var FlowEngine = class {
3664
3873
  않으면(옛 저널) 그때는 비는 것이 사실이다 — 없는 것을 지어내지 않는다. */
3665
3874
  /* 품목도 함께 — 씨앗이 흘리면 되살아난 오더가 무엇을 만드는지 모른다. */
3666
3875
  ...o.gtin ? { gtin: o.gtin } : {},
3876
+ /* 레시피도 함께 — 품목만 이어받으면 되살아난 오더가 「무엇으로」를 모르고, 그 오더의
3877
+ 남은 공정·소요가 다른 레시피 기준으로 계산된다(같은 품목에 대체 레시피가 있다). */
3878
+ ...o.recipeKey ? { recipeKey: o.recipeKey } : {},
3667
3879
  bizTransaction: o.bizTransaction ?? "",
3668
- allocated: (o.allocated ?? []).slice(),
3880
+ allocated: resolved.kept,
3669
3881
  picked: [],
3670
3882
  shipmentEpc: null,
3671
3883
  lines,
3884
+ ...resolved.dropped ? { seedIncomplete: true } : {},
3672
3885
  /* **보류를 이어받는다** — 잃으면 씨앗이 사람이 일부러 멈춘 오더를 다시 계획해 내보낸다.
3673
3886
  씨앗 왕복 대조가 이것을 잡았다(그 전에는 사람이 코드를 읽어야만 알 수 있었다). */
3674
3887
  ...o.held ? { held: true } : {},
@@ -3942,6 +4155,12 @@ var FlowEngine = class {
3942
4155
  simClockMs: this.clockMs,
3943
4156
  nowTime: this.now(),
3944
4157
  // 트윈의 "지금" — 관측 모드면 마지막으로 들은 시각(§nowMs)
4158
+ identityGrounding: this.identityGroundingView(),
4159
+ /* 원본과 어긋난 사실 — 0 이면 싣지 않는다(어긋난 적 없는 트윈에 빈 칸을 만들지 않는다). */
4160
+ ...this.transformInputsAbsent || this.seedDanglingRefs ? { conformance: {
4161
+ ...this.transformInputsAbsent ? { transformInputsAbsent: this.transformInputsAbsent } : {},
4162
+ ...this.seedDanglingRefs ? { seedDanglingRefs: this.seedDanglingRefs } : {}
4163
+ } } : {},
3945
4164
  /* 출처 표시 — 보드(마스터)에서 온 자리다. 미러는 관측으로 알게 된 자리를 'observed' 로 구별하는데,
3946
4165
  * 시뮬이 아무 표시도 안 하면 소비처가 두 스냅샷을 같은 규칙으로 읽지 못한다. */
3947
4166
  locations: [...this.locations.values()].map((n) => {
@@ -4027,6 +4246,10 @@ var FlowEngine = class {
4027
4246
  * 라인이 있는 오더는 `lines[].gtin` 이 품목을 말하고, 단일 품목 작업지시는 이 자리가 말한다.
4028
4247
  */
4029
4248
  ...o.gtin ? { gtin: o.gtin } : {},
4249
+ /* **어느 레시피로 만드나** — 품목만으로는 「무엇으로」가 안 남는다(같은 품목의 대체 레시피가 있다). */
4250
+ ...o.recipeKey ? { recipeKey: o.recipeKey } : {},
4251
+ /* 씨앗이 다 심지 못했다는 사실 — 조용히 자르지 않는다(그 오더의 답은 부족한 씨앗 위에 있다). */
4252
+ ...o.seedIncomplete ? { seedIncomplete: true } : {},
4030
4253
  ...o.lines?.length ? { lines: o.lines.map((l) => ({ gtin: l.gtin, requested: l.requested })) } : {},
4031
4254
  ...o.priority !== void 0 ? { priority: o.priority } : {},
4032
4255
  ...o.startTime ? { startTime: o.startTime } : {},
@@ -4138,7 +4361,8 @@ var FlowEngine = class {
4138
4361
  if (skip.has(key)) continue;
4139
4362
  const v = this[key];
4140
4363
  if (typeof v === "function") continue;
4141
- clone[key] = v instanceof Map ? new Map([...v.entries()].map(([k, o]) => [k, structuredClone(o)])) : structuredClone(v);
4364
+ const cloneable = v;
4365
+ clone[key] = v instanceof Map ? new Map([...v.entries()].map(([k, o]) => [k, structuredClone(o)])) : typeof cloneable?.clone === "function" ? cloneable.clone() : structuredClone(v);
4142
4366
  }
4143
4367
  clone.rng.state = this.rng.state;
4144
4368
  clone.durationEstimator = this.durationEstimator;
@@ -4360,7 +4584,7 @@ var FlowEngine = class {
4360
4584
  * 이긴다(원천 사본의 값이 낡았을 때 고칠 자리가 여기다). 무엇을 썼는지는 `specCoverage()` 가 밝힌다.
4361
4585
  *
4362
4586
  * ── 조용히 버리지 않는다 ────────────────────────────────────────────────────
4363
- * 읽을 수 없는 값은 던진다. 받아 두고 무시하면 화면은 「넣었습니다」라고 말하고 시뮬은 상수로 도는데,
4587
+ * 읽을 수 없는 값은 오류를 낸다. 받아 두고 무시하면 화면은 「넣었습니다」라고 말하고 시뮬은 상수로 도는데,
4364
4588
  * 그 어긋남을 아무도 볼 수 없다(이 시스템에서 가장 비싼 종류의 침묵이다).
4365
4589
  */
4366
4590
  declareDurations(durations) {
@@ -4388,7 +4612,7 @@ var FlowEngine = class {
4388
4612
  * · **현장 선언이 원천 명세를 이긴다**(ADR-0034) — 원천 사본이 낡았을 때 고칠 자리가 그것뿐이다.
4389
4613
  * · 명세 행이 없는 종류에도 얹힌다 — 행을 지어 만들면 지어낸 `intent` 가 능력 계산을 오염시킨다.
4390
4614
  * · 무엇을 썼는지는 `specCoverage()` 의 `parameters` 가 그대로 밝힌다.
4391
- * · 빈 이름·빈 값은 **던진다**: 받아 두고 무시하면 화면은 「넣었습니다」라고 말하고 시뮬은 상수로 돈다.
4615
+ * · 빈 이름·빈 값은 **오류를 낸다**: 받아 두고 무시하면 화면은 「넣었습니다」라고 말하고 시뮬은 상수로 돈다.
4392
4616
  */
4393
4617
  declareParameters(params) {
4394
4618
  for (const [kind, entries] of Object.entries(params ?? {})) {
@@ -4412,6 +4636,17 @@ var FlowEngine = class {
4412
4636
  routeKeys() {
4413
4637
  return void 0;
4414
4638
  }
4639
+ /**
4640
+ * **이 트윈이 굴리는 서로 다른 라우트들** — 용량이 답할 수 있는지를 가른다.
4641
+ *
4642
+ * 하나면 `routeKeys()` 가 그 순서를 답한다. 둘 이상이면 「하루 몇 대」의 답이 **제품 구성에 따라
4643
+ * 달라지므로**, 하나를 골라 답하지 않고 서로 다르다는 사실을 `capacity()` 가 함께 낸다. 조용히 고르면
4644
+ * 능력 숫자가 거짓이 되고, 그 위에 선 모든 계획이 거짓 위에 선다.
4645
+ */
4646
+ distinctRouteKeys() {
4647
+ const one = this.routeKeys();
4648
+ return one ? ["(single)"] : [];
4649
+ }
4415
4650
  /**
4416
4651
  * **이 공장이 하루 몇 대를 낼 수 있는가** — 실행해 보지 않고 답한다.
4417
4652
  *
@@ -4438,7 +4673,7 @@ var FlowEngine = class {
4438
4673
  sampleWeekStartMs: opts.sampleWeekStartMs,
4439
4674
  unitsPerDay: opts.unitsPerDay
4440
4675
  });
4441
- return { ...analysis, mixedCalendars: calendars.size > 1 };
4676
+ return { ...analysis, mixedCalendars: calendars.size > 1, mixedRoutes: this.distinctRouteKeys().length > 1 };
4442
4677
  }
4443
4678
  /**
4444
4679
  * **생산 능력 보고서** — ISA-95 Part 4 `OperationsCapability`(§operations-capability).
@@ -4664,7 +4899,7 @@ var FlowEngine = class {
4664
4899
  * `skuMix` 에서 weight 로 gtin 선택(rng) — 도착·오더 자극의 품목 결정.
4665
4900
  *
4666
4901
  * **빈 목록이면 고르지 않는다**(`undefined`). 예전에는 `mix[mix.length - 1].gtin` 으로 떨어져
4667
- * `mix[-1]` 이 undefined 가 되고 거기서 던졌다 — 그 예외가 서버까지 올라가 **정확도 추세 전체를
4902
+ * `mix[-1]` 이 undefined 가 되고 거기서 오류가 났다 — 그 예외가 서버까지 올라가 **정확도 추세 전체를
4668
4903
  * 죽였다**(품목 구성을 선언하지 않은 자극 하나가 예측 전체를 껐다).
4669
4904
  *
4670
4905
  * 없는 품목을 지어내지 않는다: 무엇을 만들지 모르면 **만들지 않는 것**이 맞고, 부르는 쪽이
@@ -4692,9 +4927,16 @@ var FlowEngine = class {
4692
4927
  * 도메인은 이 원시만 호출 — onTaskComplete 의 소비/생산/EPCIS 손코딩을 대체.
4693
4928
  */
4694
4929
  transform(inputs, outputs, opts) {
4695
- for (const epc of inputs) {
4930
+ const absent = inputs.filter((epc) => !this.items.get(epc));
4931
+ if (absent.length && !this.observeMode) {
4932
+ throw new Error(
4933
+ `transform: \uC785\uB825 ${absent.length}\uAC74\uC774 \uC0C1\uD0DC\uC5D0 \uC5C6\uC2B5\uB2C8\uB2E4 (${absent.slice(0, 3).join(", ")}${absent.length > 3 ? " \u2026" : ""}) \u2014 \uC5C6\uB294 \uAC83\uC744 \uC18C\uBE44\uD588\uB2E4\uACE0 \uAE30\uB85D\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uC774 \uD2B8\uC708\uC758 \uACC4\uC0B0\uC774 \uC5C6\uB294 \uBB3C\uAC74\uC744 \uC694\uAD6C\uD588\uC2B5\uB2C8\uB2E4.`
4934
+ );
4935
+ }
4936
+ if (absent.length) this.transformInputsAbsent += absent.length;
4937
+ const consumed = absent.length ? inputs.filter((epc) => this.items.has(epc)) : inputs;
4938
+ for (const epc of consumed) {
4696
4939
  const it = this.items.get(epc);
4697
- if (!it) continue;
4698
4940
  const n = this.locations.get(it.location);
4699
4941
  if (n) n.occupancy--;
4700
4942
  this.items.delete(epc);
@@ -4708,7 +4950,7 @@ var FlowEngine = class {
4708
4950
  eventTime: this.now(),
4709
4951
  bizStep: opts.bizStep,
4710
4952
  disposition: opts.disposition,
4711
- inputEPCList: inputs.length ? inputs.slice() : void 0,
4953
+ inputEPCList: consumed.length ? consumed.slice() : void 0,
4712
4954
  outputEPCList: outputs.length ? outputs.map((o) => o.epc) : void 0,
4713
4955
  transformationID: opts.transformationId,
4714
4956
  readPoint: opts.readPoint,
@@ -4911,6 +5153,9 @@ var FlowEngine = class {
4911
5153
  /* 진행 중 할당과 거래번호를 함께 싣는다 — 이것이 없으면 웜스타트가 잃고, 되살아난 오더의
4912
5154
  계보가 입력 없이 나간다(무엇이 무엇으로 바뀌었나의 절반이 사라진다). */
4913
5155
  ...o.gtin ? { gtin: o.gtin } : {},
5156
+ /* 어느 레시피로 만드는가 — 품목만 보내면 미러는 「무엇으로」를 알 수 없다(같은 품목에 대체
5157
+ 레시피가 있다). 시뮬만 아는 사실이 남으면 두 스냅샷을 같은 규칙으로 읽을 수 없다. */
5158
+ ...o.recipeKey ? { recipeKey: o.recipeKey } : {},
4914
5159
  ...o.allocated?.length ? { allocated: o.allocated.slice() } : {},
4915
5160
  ...o.bizTransaction ? { bizTransaction: o.bizTransaction } : {},
4916
5161
  /* 약속해 둔 자리·시각창 — 이것이 빠져서 야드 트윈이 재기동마다 첫 틱에 죽었다(§dockDoor). */
@@ -5056,9 +5301,8 @@ var FlowEngine = class {
5056
5301
  for (const req of need) {
5057
5302
  let remaining = Math.max(0, req.quantity ?? 0);
5058
5303
  if (!remaining) continue;
5059
- for (const it of this.items.values()) {
5304
+ for (const it of this.items.at(at)) {
5060
5305
  if (remaining <= 0) break;
5061
- if (it.location !== at) continue;
5062
5306
  if (!this.materialMatches(it, req)) continue;
5063
5307
  const already = takenSoFar.get(it.epc) ?? 0;
5064
5308
  const avail = Math.max(0, (it.qty ?? 1) - already);
@@ -5095,7 +5339,36 @@ var FlowEngine = class {
5095
5339
  * 같은 품목이 그 자리에 이미 있으면 **수량을 더한다** — 새 줄을 만들면 같은 자리의 같은 로트가
5096
5340
  * 둘로 갈려 재고가 부푼다(§MaterialSubLot 에서 겪은 것과 반대 방향의 같은 오류).
5097
5341
  */
5342
+ /**
5343
+ * **이 공정의 산출을 도메인이 직접 만드는가** — 기본은 아니다(코어가 만든다).
5344
+ *
5345
+ * ── 왜 이 이음새가 있나 (2026-08-20) ──────────────────────────────────────
5346
+ * 코어의 산출은 **비직렬 클래스+수량**이다(일련번호를 지어내지 않으므로). 그런데 어떤 도메인은
5347
+ * 산출물에 **개체 정체성**이 필요하다: MES 의 레시피 생산은 개체마다 직렬번호를 갖고, 수율이
5348
+ * 개체별로 양품/불량을 가르고, 계보(`TransformationEvent`)가 오더의 단계들을 잇는다.
5349
+ *
5350
+ * 그 도메인이 산출을 만들 때 코어도 같은 선언을 보고 만들면 **같은 산출이 두 벌** 된다 — 재고가
5351
+ * 조용히 두 배가 되고, 그 위의 모든 계산이 거짓 위에 선다. 그래서 소유를 **한쪽으로 정한다.**
5352
+ *
5353
+ * 코어는 여기서 **도메인 명사를 하나도 알지 않는다**: 「누가 만드는가」만 묻는다. 무엇을 만드는지는
5354
+ * 여전히 선언이 정하고, 코어는 그 선언을 읽을 뿐이다.
5355
+ */
5356
+ producesOwnOutputs(_opKey) {
5357
+ return false;
5358
+ }
5359
+ /**
5360
+ * **이 트윈의 정체성 근거** — 스냅샷에 실린다(§`StateSnapshot.identityGrounding`).
5361
+ *
5362
+ * 코어는 선언을 갖고 있지 않다(생산 선언은 도메인의 것이다). 그래서 기본은 **선언 없음**이고,
5363
+ * 선언을 든 커널이 override 해서 자기 것을 답한다 — `routeKeys()` 와 같은 모양이다.
5364
+ *
5365
+ * 코어는 여기서 **정체성의 값을 정하지 않는다**: 「어디서 왔나」만 묻는다.
5366
+ */
5367
+ identityGroundingView() {
5368
+ return identityGroundingOf(void 0);
5369
+ }
5098
5370
  produceMaterials(t) {
5371
+ if (this.producesOwnOutputs(t.kind)) return;
5099
5372
  const made = (this.operationSpecs.get(t.kind)?.materialSpecification ?? []).filter((m) => m.use === "produced");
5100
5373
  if (!made.length) return;
5101
5374
  for (const spec of made) {
@@ -5132,7 +5405,7 @@ var FlowEngine = class {
5132
5405
  * 작업은 혼자 서지 못한다: 옮길 **물품**과, (있다면) 그것을 시킨 **오더** 위에 선다. 진행 중에
5133
5406
  * 둘 중 하나가 사라질 수 있다 — 물품은 포장·출하·소비로, 오더는 이미 이행돼 씨앗이 주입하지 않아서.
5134
5407
  *
5135
- * 그때 도메인 훅은 없는 것을 딛으려다 던진다(`order.gtin` · `item.location`). 그 예외 하나가
5408
+ * 그때 도메인 훅은 없는 것을 딛으려다 오류를 낸다(`order.gtin` · `item.location`). 그 예외 하나가
5136
5409
  * **예측 전체를 죽였다** — 사용자에게는 기능이 통째로 사라진 것으로 보였다. 그래서 완료 **전에**
5137
5410
  * 여기서 묻고, 없으면 그 작업만 접는다.
5138
5411
  */
@@ -5146,7 +5419,7 @@ var FlowEngine = class {
5146
5419
  * 이 도메인이 이 작업을 **끝맺을 수 있나** — 코어가 모르는 조건은 도메인이 답한다.
5147
5420
  *
5148
5421
  * 코어는 물품과 오더까지만 안다. 그런데 도메인은 더 필요할 수 있다 — MES 는 완료 시점에 **오더와
5149
- * 제품 정의**를 딛고 서고, 그 중 하나만 없어도 던진다. 코어가 그 조건을 추측하면 도메인마다 다른
5422
+ * 제품 정의**를 딛고 서고, 그 중 하나만 없어도 오류를 낸다. 코어가 그 조건을 추측하면 도메인마다 다른
5150
5423
  * 가정을 코어에 박게 되므로(방언), **묻는다.**
5151
5424
  *
5152
5425
  * 기본은 `true` — 대부분의 작업은 코어가 확인한 것으로 충분하다.
@@ -5159,7 +5432,7 @@ var FlowEngine = class {
5159
5432
  *
5160
5433
  * 물품 맵의 키는 `itemKeyOf`(직렬 물품은 `epc`, 로트의 부분은 `subLotId`)인데, 작업은 로트 식별자
5161
5434
  * (`itemEpc`)로 가리킨다. 비직렬 로트에서는 둘이 **다르다** — 그래서 그냥 `get` 하면 못 찾는다.
5162
- * 실제로 그 회귀를 냈다: 예측(씨앗) 경로에서 `item.location = …` 이 `undefined` 위에서 던져
5435
+ * 실제로 그 회귀를 냈다: 예측(씨앗) 경로에서 `item.location = …` 이 `undefined` 위에서 오류를 내어
5163
5436
  * **정확도 추세 전체가 서버 오류로 죽었다.**
5164
5437
  *
5165
5438
  * **부분이 여럿이면 풀지 않는다** — 어느 부분을 가리키는지 알 수 없고, 아무거나 고르면 그 뒤
@@ -5168,6 +5441,7 @@ var FlowEngine = class {
5168
5441
  itemByRef(ref) {
5169
5442
  const exact = this.items.get(ref);
5170
5443
  if (exact) return exact;
5444
+ this.refScans++;
5171
5445
  let hit;
5172
5446
  for (const it of this.items.values()) {
5173
5447
  if (it.epc !== ref) continue;
@@ -5176,6 +5450,16 @@ var FlowEngine = class {
5176
5450
  }
5177
5451
  return hit;
5178
5452
  }
5453
+ /**
5454
+ * 참조가 키가 아니어서 전수 조회로 찾은 횟수 — **성능 판단의 근거**다.
5455
+ *
5456
+ * 규약대로면 0 이다. 0 이 아닌 것은 결함이 아니라 사실이다(원본이 준 참조는 우리 키를 모른다).
5457
+ * 다만 그 수가 규모와 함께 자라면 인덱스가 필요하다는 뜻이고, 그때 넣을 근거가 이 값이다.
5458
+ */
5459
+ refScans = 0;
5460
+ refScanCount() {
5461
+ return this.refScans;
5462
+ }
5179
5463
  /**
5180
5464
  * 실제 자재 이동을 작업에 적어 둔다 — ISA-95 `JobResponse.MaterialActual`.
5181
5465
  * 같은 품목·같은 쓰임은 **한 줄로 합친다**(줄을 늘리면 실적을 세는 쪽이 중복을 걷어내야 한다).
@@ -5692,7 +5976,7 @@ function matches(s, req) {
5692
5976
 
5693
5977
  // src/kernel.ts
5694
5978
  var TRAVEL_MS = 3e4;
5695
- var COMPANY_PREFIX = "0614141";
5979
+ var LEGACY_COMPANY_PREFIX = "0614141";
5696
5980
  var SHELF_MS = 30 * 24 * 36e5;
5697
5981
  var SHELF_JITTER_MS = 5 * 24 * 36e5;
5698
5982
  var WmsKernel = class extends FlowEngine {
@@ -5700,15 +5984,37 @@ var WmsKernel = class extends FlowEngine {
5700
5984
  constructor(tenantId, policy = firstFitPolicy) {
5701
5985
  super(tenantId, policy);
5702
5986
  }
5987
+ /**
5988
+ * 내장 시나리오가 요구하는 자리 — **없으면 말한다.**
5989
+ *
5990
+ * ── 왜 (2026-08-20) ────────────────────────────────────────────────────────
5991
+ * 이 커널은 입고·중간·보관 자리를 **자기 낱말로** 찾았고(`dock`·`staging`·`storage`), 없으면 조용히
5992
+ * `return` 했다. 그래서 그 낱말을 쓰지 않는 현장의 트윈은 **아무 일도 일어나지 않는 공장**이 됐다 —
5993
+ * 오류도 로그도 없이. 실제로 확인된 자리다: 첫 실 시스템(F&B)에는 `dock`·`gate`·`staging` 이 **한 건도
5994
+ * 없다**(자리 타입이 자유 문자열이고, 입출고가 도크 단계 없이 자리에 직접 일어난다).
5995
+ *
5996
+ * 여기서 이름을 여는 것이 답이 아니다 — 이 경로는 부품·라우트가 내장 상수인 레거시 시나리오라,
5997
+ * 이름만 열면 **지원하는 척**이 된다(MES 가 같은 이유로 같은 결론을 냈다). 답은 요구를 분명히 말하는
5998
+ * 것이고, 오류를 내면 호스트가 그 트윈만 멈추고 이유를 등록부에 적는다(`tick-failed`) — 다른 트윈은 계속 돈다.
5999
+ * 조용한 정지는 그 사실조차 남기지 않는다.
6000
+ */
6001
+ builtInLocation(type, what) {
6002
+ const loc = this.locationByType(type);
6003
+ if (!loc) {
6004
+ throw new Error(
6005
+ `built-in WMS scenario needs a location of type '${type}' (${what}) and this twin has none \u2014 rename it, or drive this twin from declarations instead of the built-in scenario`
6006
+ );
6007
+ }
6008
+ return loc;
6009
+ }
5703
6010
  /** 입고 도착 — §4 라이프사이클: ASN(PO) → 팔레트 조립 → 수령 → putaway task. */
5704
6011
  onArrival(spec) {
5705
- const dock = this.locationByType("dock");
5706
- if (!dock) return;
5707
- const epc = ssccUri(COMPANY_PREFIX, ++this.epcSeq);
6012
+ const dock = this.builtInLocation("dock", "inbound pallets land here");
6013
+ const epc = ssccUri(LEGACY_COMPANY_PREFIX, ++this.epcSeq);
5708
6014
  const gtin = this.pickGtin(spec.content.skuMix);
5709
6015
  if (!gtin) return;
5710
6016
  const qty = this.randInt(spec.content.qtyPerLine.min, spec.content.qtyPerLine.max);
5711
- const po = gdtiUri(COMPANY_PREFIX, "401", ++this.poSeq);
6017
+ const po = gdtiUri(LEGACY_COMPANY_PREFIX, "401", ++this.poSeq);
5712
6018
  const eventTime = this.now();
5713
6019
  const qtyList = [{ epcClass: gtin, quantity: qty }];
5714
6020
  const poTxn = [{ type: BTT.po, bizTransaction: po }];
@@ -5779,7 +6085,7 @@ var WmsKernel = class extends FlowEngine {
5779
6085
  /** 오더 생성 — 약속(납기·우선순위)은 base `promiseOf` 가 계산한다(도메인마다 다르게 재지 않는다). */
5780
6086
  createSalesOrder(lines, spec) {
5781
6087
  const id = `order-${++this.orderSeq}`;
5782
- const so = gdtiUri(COMPANY_PREFIX, "402", ++this.soSeq);
6088
+ const so = gdtiUri(LEGACY_COMPANY_PREFIX, "402", ++this.soSeq);
5783
6089
  const requested = lines.reduce((s, l) => s + l.qty, 0);
5784
6090
  const order = {
5785
6091
  id,
@@ -5803,8 +6109,8 @@ var WmsKernel = class extends FlowEngine {
5803
6109
  * 멀티SKU 는 여러 라인의 팔레트를 한 오더로 모아 단일 출하(finalizeOrder 통합 화물).
5804
6110
  */
5805
6111
  allocate(o) {
5806
- const staging = this.locationByType("staging");
5807
- if (!staging || !o.lines) return;
6112
+ if (!o.lines) return;
6113
+ const staging = this.builtInLocation("staging", "picked pallets wait here before shipping");
5808
6114
  const chosenAll = [];
5809
6115
  for (const line of o.lines) {
5810
6116
  const already = o.allocated.filter((e) => this.items.get(e)?.gtin === line.gtin).length;
@@ -5839,7 +6145,7 @@ var WmsKernel = class extends FlowEngine {
5839
6145
  const to = this.locations.get(t.toNode);
5840
6146
  from.occupancy--;
5841
6147
  to.occupancy++;
5842
- item.location = to.id;
6148
+ this.items.relocate(itemKeyOf(item), to.id);
5843
6149
  if (t.kind === "feed") {
5844
6150
  this.emit(objectEvent({ eventTime: this.now(), action: "OBSERVE", bizStep: BIZSTEP.storing, disposition: item.disposition, epcList: [item.epc], quantityList: [{ epcClass: item.gtin, quantity: item.qty }], readPoint: to.id, bizLocation: to.id }));
5845
6151
  return;
@@ -5952,21 +6258,21 @@ var WmsKernel = class extends FlowEngine {
5952
6258
  const qty = row.qty ?? 0;
5953
6259
  if (qty <= 0) continue;
5954
6260
  this.items.delete(key);
5955
- const pallet = ssccUri(COMPANY_PREFIX, ++this.epcSeq);
6261
+ const pallet = ssccUri(LEGACY_COMPANY_PREFIX, ++this.epcSeq);
5956
6262
  const qtyList = [{ epcClass: m.definitionId, quantity: qty }];
5957
6263
  this.items.set(pallet, { epc: pallet, gtin: m.definitionId, qty, location: at, disposition: DISP.in_progress });
5958
6264
  const eventTime = this.now();
5959
6265
  this.emit(aggregationEvent({ eventTime, action: "ADD", bizStep: BIZSTEP.packing, parentID: pallet, childQuantityList: qtyList, readPoint: at }));
5960
6266
  this.emit(objectEvent({ eventTime, action: "ADD", bizStep: BIZSTEP.packing, disposition: DISP.in_progress, epcList: [pallet], quantityList: qtyList, readPoint: at, bizLocation: at }));
5961
- const storage = this.locationByType("storage");
5962
- if (storage) this.pushTask(order, "putaway", pallet, at, storage.id);
6267
+ const storage = this.builtInLocation("storage", "received pallets are put away here");
6268
+ this.pushTask(order, "putaway", pallet, at, storage.id);
5963
6269
  }
5964
6270
  }
5965
6271
  /** 전량 피킹 → packing(조립)·staging·shipping 마감. 화물 사이트 이탈, 백오더 잔량 재할당. */
5966
6272
  finalizeOrder(order, staging) {
5967
6273
  const shipDock = this.locationByType("dock-ship") ?? staging;
5968
6274
  const eventTime = this.now();
5969
- const shipment = ssccUri(COMPANY_PREFIX, ++this.epcSeq);
6275
+ const shipment = ssccUri(LEGACY_COMPANY_PREFIX, ++this.epcSeq);
5970
6276
  order.shipmentEpc = shipment;
5971
6277
  const soTxn = [{ type: BTT.so, bizTransaction: order.bizTransaction }];
5972
6278
  this.aggregate(shipment, order.picked.slice(), { bizStep: BIZSTEP.packing, readPoint: staging.id, bizLocation: staging.id });
@@ -5999,7 +6305,7 @@ var DWELL_MS = 3e4;
5999
6305
  var WINDOW_DELAY_MS = 4e4;
6000
6306
  var WINDOW_LENGTH_MS = 20 * 6e4;
6001
6307
  var CARGO_PER_TRAILER = 2;
6002
- var CP = "0614141";
6308
+ var LEGACY_CP = "0614141";
6003
6309
  var YmsKernel = class extends FlowEngine {
6004
6310
  doorRR = 0;
6005
6311
  cargoSeq = 0;
@@ -6029,18 +6335,18 @@ var YmsKernel = class extends FlowEngine {
6029
6335
  const doors = [...this.locations.values()].filter((n) => n.type === "dock-door").sort((a, b) => a.id.localeCompare(b.id));
6030
6336
  if (!gate || doors.length === 0) return;
6031
6337
  const inbound = kind === "appointment";
6032
- const epc = graiUri(CP, "10", ++this.epcSeq);
6338
+ const epc = graiUri(LEGACY_CP, "10", ++this.epcSeq);
6033
6339
  this.items.set(epc, { epc, location: gate.id, disposition: DISP.in_progress });
6034
6340
  gate.occupancy++;
6035
6341
  this.emit(objectEvent({ eventTime: this.now(), action: "ADD", bizStep: YARD_BIZSTEP.arriving, disposition: DISP.in_progress, epcList: [epc], readPoint: gate.id, bizLocation: gate.id }));
6036
6342
  if (inbound) {
6037
- const cargo = Array.from({ length: CARGO_PER_TRAILER }, () => ssccUri(CP, ++this.cargoSeq));
6343
+ const cargo = Array.from({ length: CARGO_PER_TRAILER }, () => ssccUri(LEGACY_CP, ++this.cargoSeq));
6038
6344
  this.trailerCargo.set(epc, cargo);
6039
6345
  this.aggregate(epc, cargo, { bizStep: YARD_BIZSTEP.arriving, readPoint: gate.id });
6040
6346
  }
6041
6347
  const door = doors[this.doorRR++ % doors.length];
6042
6348
  const id = `order-${++this.orderSeq}`;
6043
- const appt = gdtiUri(CP, "404", ++this.soSeq);
6349
+ const appt = gdtiUri(LEGACY_CP, "404", ++this.soSeq);
6044
6350
  const order = {
6045
6351
  id,
6046
6352
  kind,
@@ -6120,7 +6426,7 @@ var YmsKernel = class extends FlowEngine {
6120
6426
  const to = this.locations.get(t.toNode);
6121
6427
  from.occupancy--;
6122
6428
  to.occupancy++;
6123
- trailer.location = to.id;
6429
+ this.items.relocate(itemKeyOf(trailer), to.id);
6124
6430
  if (t.kind === "dwell") {
6125
6431
  const gate = this.locationByType("gate");
6126
6432
  const dep = { id: `task-${++this.taskSeq}`, kind: "depart", status: "created", itemEpc: trailer.epc, fromNode: t.toNode, toNode: gate.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: "depart", fromNode: t.toNode, toNode: gate.id }, TRAVEL_MS2), orderId: t.orderId };
@@ -6174,25 +6480,7 @@ var YmsKernel = class extends FlowEngine {
6174
6480
  var DEFAULT_CYCLE_MS = 4e4;
6175
6481
  var DEFAULT_SETUP_MS = 15e3;
6176
6482
  var MES_CMD = { changeover: "mes.changeover" };
6177
- var CP2 = "0614141";
6178
- var WIP_ITEMREF = "066666";
6179
- var WIP_GTIN = sgtinClass(CP2, WIP_ITEMREF);
6180
6483
  var DEFAULT_YIELD = 0.8;
6181
- var PART_A = { itemRef: "055551", gtin: sgtinClass(CP2, "055551") };
6182
- var PART_B = { itemRef: "055552", gtin: sgtinClass(CP2, "055552") };
6183
- var PRODUCTS = [
6184
- { key: "P1", ref: "077777", gtin: sgtinClass(CP2, "077777"), bom: [{ part: PART_A, qty: 2 }, { part: PART_B, qty: 1 }] },
6185
- { key: "P2", ref: "077778", gtin: sgtinClass(CP2, "077778"), bom: [{ part: PART_A, qty: 1 }, { part: PART_B, qty: 2 }] }
6186
- ];
6187
- var MES_PART_GTINS = { partA: PART_A.gtin, partB: PART_B.gtin };
6188
- var MES_PRODUCT_GTINS = { p1: PRODUCTS[0].gtin, p2: PRODUCTS[1].gtin };
6189
- var MES_PRODUCTS = PRODUCTS.map((p) => ({ gtin: p.gtin, label: p.key }));
6190
- var ROUTE = [
6191
- { kind: "cut", locationType: "cut-station", resource: "cutter" },
6192
- { kind: "weld", locationType: "weld-station", resource: "welder" },
6193
- { kind: "paint", locationType: "paint-booth", resource: "painter" },
6194
- { kind: "assembly", locationType: "assembly-line", resource: "assembler" }
6195
- ];
6196
6484
  var MesKernel = class extends FlowEngine {
6197
6485
  wipSeq = 0;
6198
6486
  prodSeq = 0;
@@ -6203,13 +6491,15 @@ var MesKernel = class extends FlowEngine {
6203
6491
  this.productionSpec = productionSpec;
6204
6492
  if (productionSpec?.definition?.operations) {
6205
6493
  this.loadOperations(productionSpec.definition.operations);
6206
- this.assertNoDoubleProduction(productionSpec.definition.operations);
6207
6494
  }
6208
6495
  if (productionSpec?.definition?.recipes?.length && (!productionSpec.binding || !productionSpec.companyPrefix)) {
6209
6496
  throw new Error(
6210
6497
  "recipe-driven production needs `binding` and `companyPrefix` \u2014 the definition does not know GTINs, so material keys cannot be resolved to GS1 identifiers"
6211
6498
  );
6212
6499
  }
6500
+ if (productionSpec?.definition?.operations) {
6501
+ this.assertNoDoubleProduction(productionSpec.definition.operations);
6502
+ }
6213
6503
  }
6214
6504
  /**
6215
6505
  * **산출을 두 곳에서 만들지 않는다** — 기동 때 막는다.
@@ -6224,14 +6514,69 @@ var MesKernel = class extends FlowEngine {
6224
6514
  * 모든 계산이 거짓이 된다).
6225
6515
  */
6226
6516
  assertNoDoubleProduction(ops) {
6227
- const bad2 = ops.filter((o) => (o.materialSpecification ?? []).some((m) => m.use === "produced")).map((o) => o.key);
6517
+ const finals = new Set((this.productionSpec?.definition?.recipes ?? []).flatMap((r) => (r.outputs ?? []).map((o) => this.classOf(o.material))));
6518
+ const bad2 = ops.filter((o) => (o.materialSpecification ?? []).some((m) => m.use === "produced" && m.materialDefinition && finals.has(m.materialDefinition))).map((o) => o.key);
6228
6519
  if (!bad2.length) return;
6229
6520
  throw new Error(
6230
- `MES recipe already produces outputs \u2014 operations [${bad2.join(", ")}] must not also declare materialSpecification use:'produced' (that would create the same output twice). Consumption specs are fine; declare production in the recipe.`
6521
+ `MES recipe already produces its output \u2014 operations [${bad2.join(", ")}] must not also declare that same material as materialSpecification use:'produced' (that would create the same output twice). Declare intermediate outputs freely; the final output belongs to the recipe.`
6231
6522
  );
6232
6523
  }
6233
- productOf(gtin) {
6234
- return PRODUCTS.find((p) => p.gtin === gtin);
6524
+ /**
6525
+ * **레시피 모드에서는 MES 산출을 소유한다** — 코어는 비켜선다(§`producesOwnOutputs`).
6526
+ *
6527
+ * MES 의 산출은 개체 정체성(직렬번호) + 수율 + 오더 계보를 갖는다. 코어의 일반 산출은 비직렬
6528
+ * 클래스+수량이라 그 셋을 표현하지 못한다. 둘이 같은 선언을 보고 각자 만들면 재고가 두 배가 된다.
6529
+ *
6530
+ * 레시피가 없으면(유통가공 등) 소유를 주장하지 않는다 — 그때는 코어의 산출이 맞다.
6531
+ */
6532
+ producesOwnOutputs(_opKey) {
6533
+ return !!this.productionSpec?.definition?.recipes?.length;
6534
+ }
6535
+ /**
6536
+ * 이 트윈의 정체성 근거 — 선언에서 파생한다(§`identityGroundingOf`).
6537
+ *
6538
+ * 선언이 근거를 정한다: 이름공간을 선언했으면 `declared`, 발급을 주장했으면 `issued`(주장이지 사실이
6539
+ * 아니다). 선언이 없는 MES 트윈은 이제 만들 수 없으므로(`loadTwinModel` 이 거절한다) `fabricated` 로
6540
+ * 판정될 일이 없다.
6541
+ */
6542
+ identityGroundingView() {
6543
+ return identityGroundingOf(this.productionSpec);
6544
+ }
6545
+ /**
6546
+ * 선언이 말하는 자리 타입이 **이 트윈에 있나** — 로드 시점에 한 번, 모아서 말한다 (2026-08-20).
6547
+ *
6548
+ * ── 왜 로드 시점인가 ───────────────────────────────────────────────────────
6549
+ * 이것은 **선언의 흠**이다: 그 트윈의 생산 선언과 그 트윈의 모델이 서로 어긋났다. 틱에서 발견하면
6550
+ * 그 트윈은 「도는데 아무 일도 안 일어나는」 모습이 된다 — 실제로 `allocateDef` 가 첫 공정의 자리를
6551
+ * 못 찾으면 조용히 `return` 했고, 오더는 영원히 배정되지 않은 채 화면에는 「running」이라 적혔다.
6552
+ *
6553
+ * ── 좁히지 않는다 ─────────────────────────────────────────────────────────
6554
+ * 「이 트윈이 실제로 지나는 라우트만 본다」는 예외를 한 번 검토했다가 버렸다. 근거로 삼은 「여러
6555
+ * 트윈이 한 선언을 공유한다」가 **이 코드에 없는 이야기**였다: 호스트는 트윈마다 자기 모델에서
6556
+ * 선언을 만든다(`productionSpecOf(model)`). 없는 사정을 상상해 검사에 구멍을 내지 않는다.
6557
+ *
6558
+ * 모아서 한 번에 말한다 — 하나씩 실패하면 사람이 같은 기동을 열 번 반복한다.
6559
+ */
6560
+ loadTwinModel(def) {
6561
+ super.loadTwinModel(def);
6562
+ if (!this.productionSpec) {
6563
+ throw new Error(
6564
+ "MES twin requires a production declaration (ProductionSpec): materials, routes and recipes. The kernel no longer carries a built-in demo product \u2014 what is made is decided by the model."
6565
+ );
6566
+ }
6567
+ const have = new Set([...this.locations.values()].map((l) => l.type));
6568
+ const missing = [];
6569
+ for (const m of this.productionSpec.definition.materials ?? []) {
6570
+ if (m.locationType && !have.has(m.locationType)) missing.push(`material '${m.key}' \u2192 '${m.locationType}'`);
6571
+ }
6572
+ for (const op of this.productionSpec.definition.operations ?? []) {
6573
+ if (op.locationType && !have.has(op.locationType)) missing.push(`operation '${op.key}' \u2192 '${op.locationType}'`);
6574
+ }
6575
+ if (missing.length) {
6576
+ throw new Error(
6577
+ `production declaration names location types this twin does not have: ${missing.join(" \xB7 ")}. This twin has [${[...have].sort().join(", ")}]. Declare those locations in the model, or fix the declared locationType \u2014 the kernel does not invent a place for material to sit.`
6578
+ );
6579
+ }
6235
6580
  }
6236
6581
  /**
6237
6582
  * MES 도메인 커맨드(Tier 2) — mes.changeover: 설비를 제품 gtin 으로 강제 전환.
@@ -6254,107 +6599,55 @@ var MesKernel = class extends FlowEngine {
6254
6599
  }
6255
6600
  return super.handleCommand(cmd);
6256
6601
  }
6257
- /** 부품 수령(다품종)skuMix gtin 으로 부품 종류 결정. */
6602
+ /** 부품 수령 — 도착한 품목이 선언된 입력 자재면 자재가 선언한 자리에 생성. */
6258
6603
  onArrival(spec) {
6259
- if (this.productionSpec) return this.onArrivalDef(spec);
6260
- const rawStore = this.locationByType("raw-store");
6261
- if (!rawStore) return;
6262
- const gtin = this.pickGtin(spec.content.skuMix);
6263
- const part = [PART_A, PART_B].find((p) => p.gtin === gtin);
6264
- if (!part) return;
6265
- const epc = sgtinUri(CP2, part.itemRef, ++this.epcSeq);
6266
- this.items.set(epc, { epc, gtin: part.gtin, qty: 1, location: rawStore.id, disposition: DISP.sellable });
6267
- rawStore.occupancy++;
6268
- this.emit(objectEvent({ eventTime: this.now(), action: "ADD", bizStep: MES_BIZSTEP.receiving, disposition: DISP.sellable, epcList: [epc], quantityList: [{ epcClass: part.gtin, quantity: 1 }], readPoint: rawStore.id, bizLocation: rawStore.id }));
6269
- }
6270
- /** 작업지시 — 제품 2종 교대(체인지오버 유발). 제품 gtin 을 오더에 기록. */
6604
+ return this.onArrivalDef(spec);
6605
+ }
6606
+ /** 작업지시 — 오더가 자기 레시피를 든다(선언된 레시피 중에서). */
6271
6607
  onOrder(_spec) {
6272
- if (this.productionSpec) return this.onOrderDef(_spec);
6273
- const product = PRODUCTS[this.orderSeq % PRODUCTS.length];
6274
- const id = `order-${++this.orderSeq}`;
6275
- const wo = gdtiUri(CP2, "403", ++this.soSeq);
6276
- const order = { id, kind: "workorder", status: "created", gtin: product.gtin, requested: 1, fulfilled: 0, bizTransaction: wo, allocated: [], picked: [], ...this.promiseOf(_spec) };
6277
- this.orders.set(id, order);
6278
- this.emitOrder(order);
6608
+ return this.onOrderDef(_spec);
6279
6609
  }
6280
- /** 할당 — 제품 BOM 라인의 부품 예약(하나라도 부족하면 대기) + 라우트 스테이션(절단) 태스크. */
6610
+ /** 할당 — 레시피 입력 전량 확보 라우트 단계 태스크(부족하면 대기). */
6281
6611
  allocate(o) {
6282
- if (this.productionSpec) return this.allocateDef(o);
6283
- const s0 = ROUTE[0];
6284
- const first = this.locationByType(s0.locationType);
6285
- const product = this.productOf(o.gtin);
6286
- if (!first || !product) return;
6287
- const picks = [];
6288
- for (const line of product.bom) {
6289
- const available = [...this.items.values()].filter((i) => i.gtin === line.part.gtin && i.disposition === DISP.sellable && this.locations.get(i.location)?.type === "raw-store").map((i) => ({ epc: i.epc, location: i.location, qty: 1 }));
6290
- const chosen = this.policy.selectStock({ gtin: line.part.gtin, qty: line.qty, available });
6291
- if (chosen.length < line.qty) return;
6292
- picks.push(...chosen);
6293
- }
6294
- for (const epc of picks) o.allocated.push(epc);
6295
- this.reserve(picks, MES_BIZSTEP.producing);
6296
- this.emit(transactionEvent({ eventTime: this.now(), action: "ADD", bizStep: MES_BIZSTEP.producing, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: o.bizTransaction }], epcList: o.allocated.slice() }));
6297
- this.emitStation(o, s0, o.allocated[0], product.gtin);
6298
- o.status = "op-" + s0.kind;
6299
- this.emitOrder(o);
6300
- }
6301
- /** 라우트 스테이션 태스크 발행(공통) — 제자리 가공(process), 이종 자원, 제품 전환 셋업. */
6302
- emitStation(o, stage, itemEpc, changeoverKey) {
6303
- const loc = this.locationByType(stage.locationType);
6304
- const task = { id: `task-${++this.taskSeq}`, kind: stage.kind, status: "created", itemEpc, fromNode: loc.id, toNode: loc.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: stage.kind, fromNode: loc.id, toNode: loc.id, resourceKind: stage.resource }, DEFAULT_CYCLE_MS), orderId: o.id, resourceType: stage.resource, changeoverKey, setupMs: this.paramDuration(stage.kind, OP_PARAM.setupDuration) ?? DEFAULT_SETUP_MS, intent: "process" };
6305
- this.tasks.set(task.id, task);
6306
- this.emitTask(task);
6612
+ return this.allocateDef(o);
6307
6613
  }
6308
- /** op 완료 = 변환. 라우트 인덱스로 분기: 첫=BOM 소비→WIP, 중간=WIP→WIP, 마지막(조립)=WIP→완성차(수율→OEE 품질). */
6309
6614
  /**
6310
6615
  * MES 는 완료 시점에 **오더와 제품 정의**를 딛고 선다 — 둘 중 하나만 없어도 끝맺을 수 없다.
6311
6616
  *
6312
6617
  * 씨앗이 이미 이행된 오더를 주입하지 않으므로(남은 수량 0) 그 오더에 딸린 작업만 남을 수 있고,
6313
- * 관측이 오더 연결을 담지 못한 작업도 있다. 그때 예전에는 `order.gtin` 에서 던졌고 **그 예외
6618
+ * 관측이 오더 연결을 담지 못한 작업도 있다. 그때 예전에는 `order.gtin` 에서 오류가 났고 **그 예외
6314
6619
  * 하나가 정확도 추세 전체를 죽였다.** 이제 코어가 이 답을 보고 그 작업만 접는다.
6315
6620
  */
6316
6621
  canComplete(t) {
6317
- if (this.productionSpec) return !!(t.orderId && this.orders.get(t.orderId));
6318
- const order = t.orderId ? this.orders.get(t.orderId) : void 0;
6319
- return !!order && !!this.productOf(order.gtin);
6622
+ return !!(t.orderId && this.orders.get(t.orderId));
6320
6623
  }
6321
6624
  onTaskComplete(t) {
6322
- if (this.productionSpec) return this.onTaskCompleteDef(t);
6323
- const order = this.orders.get(t.orderId);
6324
- const product = order && this.productOf(order.gtin);
6325
- if (!order || !product) throw new Error(`task ${t.id}: order/product vanished between the core check and the domain hook`);
6326
- const i = ROUTE.findIndex((s) => s.kind === t.kind);
6327
- const loc = this.locations.get(t.toNode);
6328
- const isLast = i === ROUTE.length - 1;
6329
- if (!isLast) {
6330
- const inputs = order.allocated.slice();
6331
- const wip2 = sgtinUri(CP2, WIP_ITEMREF, ++this.wipSeq);
6332
- this.transform(inputs, [{ epc: wip2, gtin: WIP_GTIN, qty: 1, location: loc.id, disposition: DISP.in_progress }], { bizStep: MES_BIZSTEP.producing, disposition: DISP.in_progress, transformationId: order.bizTransaction, readPoint: loc.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
6333
- order.allocated = [wip2];
6334
- const next = ROUTE[i + 1];
6335
- this.emitStation(order, next, wip2, product.gtin);
6336
- order.status = "op-" + next.kind;
6337
- this.emitOrder(order);
6338
- return;
6339
- }
6340
- const fgStore = this.locationByType("fg-store");
6341
- const wip = order.allocated[0];
6342
- const good = this.rng() < (this.yieldOf(t.kind) ?? DEFAULT_YIELD);
6343
- t.outcome = good ? "good" : "scrap";
6344
- this.recordOutput(t.resource, good);
6345
- const disp = good ? DISP.sellable : DISP.non_sellable;
6346
- const outputEpc = sgtinUri(CP2, product.ref, ++this.prodSeq);
6347
- this.transform([wip], [{ epc: outputEpc, gtin: product.gtin, qty: 1, location: fgStore.id, disposition: disp }], { bizStep: MES_BIZSTEP.producing, disposition: disp, transformationId: order.bizTransaction, readPoint: loc.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
6348
- this.emit(objectEvent({ eventTime: this.now(), action: "OBSERVE", bizStep: MES_BIZSTEP.storing, disposition: disp, epcList: [outputEpc], quantityList: [{ epcClass: product.gtin, quantity: 1 }], readPoint: fgStore.id, bizLocation: fgStore.id }));
6349
- order.allocated = [];
6350
- order.fulfilled = 1;
6351
- order.status = good ? "produced" : "scrapped";
6352
- this.emitOrder(order);
6625
+ return this.onTaskCompleteDef(t);
6353
6626
  }
6354
6627
  // ── 정의-구동 모드 (도메인 정의 데이터로 실행 — 레거시와 분리, 하드코딩 대체) ──
6355
- recipeDef() {
6628
+ /** 선언된 레시피 전부 — 오더가 자기 것을 고르고, 수령이 전부의 소요를 본다. */
6629
+ recipesDef() {
6630
+ return this.productionSpec.definition.recipes ?? [];
6631
+ }
6632
+ /**
6633
+ * 이 오더의 레시피 — **오더가 들면 그것, 없으면 선언 수준의 기본**(§`FlowOrder.recipeKey`).
6634
+ *
6635
+ * 오더가 든 키가 선언에 없으면 **말한다**: 조용히 첫 레시피로 떨어지면 그 오더는 다른 물건을 만들고,
6636
+ * 그 뒤 계보·재고·수율이 전부 엉뚱한 품목에 붙는다.
6637
+ */
6638
+ recipeDef(order) {
6356
6639
  const d = this.productionSpec.definition;
6357
- return this.productionSpec.recipeKey ? d.recipes?.find((r) => r.key === this.productionSpec.recipeKey) : d.recipes?.[0];
6640
+ const key = order?.recipeKey ?? this.productionSpec.recipeKey;
6641
+ if (key) {
6642
+ const found = d.recipes?.find((r) => r.key === key);
6643
+ if (!found) {
6644
+ throw new Error(
6645
+ `recipe '${key}' is not declared. Declared: [${(d.recipes ?? []).map((r) => r.key).join(", ") || "(none)"}]. The kernel does not substitute another recipe \u2014 that would make this order produce a different item.`
6646
+ );
6647
+ }
6648
+ return found;
6649
+ }
6650
+ return d.recipes[0];
6358
6651
  }
6359
6652
  /** 자재 키 → 구체 gtin 클래스(idpat). 구체 식별은 바인딩+prefix 로 인스턴스가 주입. */
6360
6653
  classOf(materialKey) {
@@ -6363,54 +6656,191 @@ var MesKernel = class extends FlowEngine {
6363
6656
  serialOf(materialKey, serial) {
6364
6657
  return sgtinUri(this.productionSpec.companyPrefix, this.productionSpec.binding[materialKey], serial);
6365
6658
  }
6659
+ /**
6660
+ * 이 공정이 **만든다고 선언한 품목** — 표준 `OpMaterialSpecification` `MaterialUse='produced'`.
6661
+ *
6662
+ * ── 왜 이 함수가 생겼나 (2026-08-20) ──────────────────────────────────────
6663
+ * 예전에는 라우트의 중간 단계마다 커널이 `sgtinUri(prefix, 'WIP', ++seq)` 로 **품목을 지어냈다.**
6664
+ * 그러면 배합물·가열완료물·소분물이 전부 같은 상품코드가 된다 — 저널에 남는 것은 「무엇인지 아무도
6665
+ * 답할 수 없는 물건」이고, 그것이 영구 기록이 된다. 게다가 그 조립은 `binding` 을 **건너뛴다**:
6666
+ * 다른 모든 품목은 `classOf`/`serialOf` 로 인스턴스가 주입한 품번을 지나는데, `'WIP'` 만 커널이
6667
+ * itemRef 자리에 리터럴을 넣었다.
6668
+ *
6669
+ * ── 규율 ──────────────────────────────────────────────────────────────────
6670
+ * 만드는 것은 **선언이 정한다**. 선언이 없으면 만들지 않는다(§`produceMaterials` 와 같은 규율 —
6671
+ * 「무엇을 만드는지 말하지 않으면 만들지 않는다」). 있으면 그 품목으로 만든다. 어느 쪽도 지어내지 않는다.
6672
+ *
6673
+ * 둘 이상을 선언하면 **거부한다**: 어느 것이 이 단계의 산출인지는 모델이 말할 일이고, 커널이 첫 줄을
6674
+ * 고르면 나머지는 조용히 사라진다.
6675
+ */
6676
+ producedMaterialKeyOf(opKey) {
6677
+ const made = (this.operationSpecs.get(opKey)?.materialSpecification ?? []).filter((m) => m.use === "produced");
6678
+ if (!made.length) return void 0;
6679
+ if (made.length > 1) {
6680
+ throw new Error(
6681
+ `operation '${opKey}' declares ${made.length} produced materials \u2014 the kernel does not choose one. Declare exactly one 'produced' material for this step, or split the step.`
6682
+ );
6683
+ }
6684
+ const declared = made[0].materialDefinition;
6685
+ if (!declared) {
6686
+ return void 0;
6687
+ }
6688
+ return this.materialKeyOfClass(declared, opKey);
6689
+ }
6690
+ /**
6691
+ * 표준 GTIN 클래스 → **모델의 자재 키.**
6692
+ *
6693
+ * 공정 명세는 표준 식별자(`MaterialDefinitionID` = GTIN idpat)로 품목을 가리키고, MES 프로파일의
6694
+ * 나머지는 자재 키(`MaterialDef.key` + `binding`)로 가리킨다. **둘은 다른 층이고 `binding` 이 통로다.**
6695
+ * 그래서 선언된 식별자가 이 트윈의 `binding` 을 지나 나오는 것인지 되짚어 확인한다.
6696
+ *
6697
+ * 없으면 **모델 위반이므로 멈춘다** — 커널이 바인딩 없는 품목의 정체성을 만들어 낼 자리가 없다.
6698
+ */
6699
+ materialKeyOfClass(gtinClass, opKey) {
6700
+ const binding = this.productionSpec.binding;
6701
+ const key = Object.keys(binding).find((k) => this.classOf(k) === gtinClass);
6702
+ if (!key) {
6703
+ throw new Error(
6704
+ `operation '${opKey}' declares produced material '${gtinClass}', which no declared material binds to. Known: ${Object.keys(binding).map((k) => `${k}=${this.classOf(k)}`).join(", ") || "(none)"}. Declare that material in the model with a binding \u2014 the kernel does not invent an identity for it.`
6705
+ );
6706
+ }
6707
+ return key;
6708
+ }
6709
+ /**
6710
+ * 이 자재가 놓이는 **자리 타입** — 선언이 권위다(`MaterialDef.locationType`).
6711
+ *
6712
+ * 예전에는 이 자리에 `'raw-store'`·`'fg-store'` 가 박혀 있었다. 그래서 현장은 자기 창고를 그 이름으로
6713
+ * **개명해야** 트윈이 굴러갔다 — 커널이 현장의 낱말을 정하는 셈이었다. 선언이 없으면 **지어내지 않고
6714
+ * 말한다**: 예전 거동(조용한 return)에서는 아무도 왜 자재가 들어오지 않는지 알 수 없었다.
6715
+ *
6716
+ * 레거시(선언 없는 내장 프로파일) 경로는 이 함수를 쓰지 않는다 — 그쪽에는 대조할 선언이 없으므로
6717
+ * 커널 어휘 자체가 계약이다.
6718
+ */
6719
+ locationTypeOfMaterial(materialKey) {
6720
+ const type = this.productionSpec.definition.materials?.find((m) => m.key === materialKey)?.locationType;
6721
+ if (!type) throw new Error(`material '${materialKey}': MaterialDef.locationType \uBBF8\uC120\uC5B8 \u2014 \uC790\uC7AC\uAC00 \uB193\uC774\uB294 \uC790\uB9AC\uB97C \uCEE4\uB110\uC774 \uC9C0\uC5B4\uB0BC \uC218 \uC5C6\uB2E4`);
6722
+ return type;
6723
+ }
6724
+ /** 그 자리 타입의 자리 — 이 트윈에 없으면 말한다(선언과 모델이 어긋난 사실이다). */
6725
+ locationOfMaterial(materialKey) {
6726
+ const type = this.locationTypeOfMaterial(materialKey);
6727
+ const loc = this.locationByType(type);
6728
+ if (!loc) throw new Error(`material '${materialKey}': locationType '${type}' \uC778 \uC790\uB9AC\uAC00 \uC774 \uD2B8\uC708\uC5D0 \uC5C6\uB2E4`);
6729
+ return loc;
6730
+ }
6366
6731
  /**
6367
6732
  * 라우트를 용량 계산에 알려 준다 — 수율을 거슬러 올릴 때 순서가 곧 계산이다.
6368
6733
  *
6369
6734
  * 순서를 모르면 선언 순서대로 세는데, 그러면 하류에서 잃는 몫이 엉뚱한 공정에 얹힌다(조용히
6370
6735
  * 틀린다). 생산 정의를 가진 커널만 이 답을 안다.
6371
6736
  */
6737
+ /*
6738
+ * ── 레시피가 여럿이면 「그 라우트」가 하나가 아니다 (2026-08-20) ────────────
6739
+ * 용량은 트윈 수준의 질문이다(「이 공장이 하루 몇 대」). 레시피마다 라우트가 다르면 그 답이 제품
6740
+ * 구성에 따라 달라지는데, **하나를 골라 답하면 능력 숫자가 조용히 거짓이 된다.** 그래서 갈릴 때는
6741
+ * 답하지 않고(`undefined`) 서로 다르다는 사실을 `capacity()` 가 함께 낸다 — `mixedCalendars` 와 같은 규율.
6742
+ */
6372
6743
  routeKeys() {
6373
6744
  if (!this.productionSpec) return void 0;
6374
- const d = this.productionSpec.definition;
6375
- return d.routes?.find((r) => r.key === this.recipeDef().route)?.steps;
6745
+ const routes = this.distinctRouteKeys();
6746
+ if (routes.length !== 1) return void 0;
6747
+ return this.productionSpec.definition.routes?.find((r) => r.key === routes[0])?.steps;
6748
+ }
6749
+ /** 선언된 레시피들이 쓰는 **서로 다른** 라우트 키들. 하나면 용량을 답할 수 있다. */
6750
+ distinctRouteKeys() {
6751
+ if (!this.productionSpec) return [];
6752
+ return [...new Set(this.recipesDef().map((r) => r.route).filter((k) => !!k))];
6376
6753
  }
6377
- /** recipe.route → 오퍼레이션 시퀀스 해소. */
6378
- routeOps() {
6754
+ /** recipe.route → 오퍼레이션 시퀀스 해소. 오더가 자기 레시피를 들면 그 라우트다. */
6755
+ routeOps(order) {
6379
6756
  const d = this.productionSpec.definition;
6380
- const route = d.routes?.find((r) => r.key === this.recipeDef().route);
6757
+ const route = d.routes?.find((r) => r.key === this.recipeDef(order).route);
6381
6758
  return (route?.steps ?? []).map((sk) => d.operations?.find((o) => o.key === sk)).filter((o) => !!o);
6382
6759
  }
6383
- /** 정의 모드 수령 — skuMix gtin 이 레시피 입력 자재면 raw-store 생성. */
6760
+ /** 정의 모드 수령 — skuMix gtin 이 레시피 입력 자재면 **그 자재가 선언한 자리**에 생성. */
6384
6761
  onArrivalDef(spec) {
6385
- const rawStore = this.locationByType("raw-store");
6386
- if (!rawStore) return;
6387
6762
  const gtin = this.pickGtin(spec.content.skuMix);
6388
6763
  if (!gtin) return;
6389
- const inputKey = this.recipeDef().inputs.map((i) => i.material).find((k) => this.classOf(k) === gtin);
6764
+ const inputKey = this.recipesDef().flatMap((r) => r.inputs.map((i) => i.material)).find((k) => this.classOf(k) === gtin);
6390
6765
  if (!inputKey) return;
6766
+ const store = this.locationOfMaterial(inputKey);
6391
6767
  const epc = this.serialOf(inputKey, ++this.epcSeq);
6392
- this.items.set(epc, { epc, gtin, qty: 1, location: rawStore.id, disposition: DISP.sellable });
6393
- rawStore.occupancy++;
6394
- this.emit(objectEvent({ eventTime: this.now(), action: "ADD", bizStep: MES_BIZSTEP.receiving, disposition: DISP.sellable, epcList: [epc], quantityList: [{ epcClass: gtin, quantity: 1 }], readPoint: rawStore.id, bizLocation: rawStore.id }));
6768
+ this.items.set(epc, { epc, gtin, qty: 1, location: store.id, disposition: DISP.sellable });
6769
+ store.occupancy++;
6770
+ this.emit(objectEvent({ eventTime: this.now(), action: "ADD", bizStep: MES_BIZSTEP.receiving, disposition: DISP.sellable, epcList: [epc], quantityList: [{ epcClass: gtin, quantity: 1 }], readPoint: store.id, bizLocation: store.id }));
6771
+ }
6772
+ /**
6773
+ * 정의 모드 작업지시 — 레시피 산출물 1개. **오더가 자기 레시피를 든다**(§`FlowOrder.recipeKey`).
6774
+ *
6775
+ * 선언 수준의 `recipeKey` 가 있으면 그 하나만 만든다(현장이 한 품목만 돌린다고 말한 것이다). 없으면
6776
+ * 선언된 레시피를 **차례로** 돈다 — 그것이 제품 전환이고, 체인지오버 셋업은 `changeoverKey`(산출물
6777
+ * 클래스)가 달라지는 것으로 자동으로 따라온다. 커널 상수 제품 목록이 필요했던 이유가 이것이었다.
6778
+ *
6779
+ * 차례는 `orderSeq` 로 정한다 — 난수를 쓰면 fork 예측이 결정적이지 않게 된다.
6780
+ */
6781
+ /**
6782
+ * **작업지시의 거래 문서 식별자** — 선언이 정하고, 커널은 문서 타입을 정하지 않는다.
6783
+ *
6784
+ * ── 무엇이 틀렸나 (2026-08-21) ────────────────────────────────────────────
6785
+ * 여기는 `gdtiUri(prefix, '403', seq)` 였다. GDTI 는 `회사 프리픽스 + 문서 타입 + 일련번호`이고
6786
+ * **문서 타입은 GS1 이 공표하는 목록이 아니다** — 프리픽스를 배정받은 회사가 자기 번호 용량에서
6787
+ * 정한다. 그런데 커널이 `'403'` 을 스스로 정해 저널에 영구히 기록했다. 회사의 배정 권한을 커널이
6788
+ * 대신 행사한 것이고, 프리픽스 날조와 같은 종류다.
6789
+ *
6790
+ * ── 순서 ──────────────────────────────────────────────────────────────────
6791
+ * ① 선언된 이름공간이 있으면 그 아래 `bt` 형태로 만든다(CBV §8.5.4·§8.5.5). **문서 타입이 필요
6792
+ * 없는 길**이고, 회사가 도메인만 있으면 되므로 진입장벽이 가장 낮다.
6793
+ * ② 없고 문서 타입을 선언했으면 GDTI 로 만든다(그 현장이 GDTI 를 쓰는 것이다).
6794
+ * ③ 둘 다 없으면 **말한다.** 지어내면 그 값이 영구 기록이 되고, 어느 화면도 그것이 발명된
6795
+ * 번호라고 말해 주지 않는다.
6796
+ */
6797
+ workOrderId(serial) {
6798
+ const identity = this.productionSpec.identity;
6799
+ for (const ns of identity?.namespaces ?? []) {
6800
+ const uri = bizTransactionUri(ns, serial);
6801
+ if (uri) return uri;
6802
+ }
6803
+ const docType = identity?.documentTypes?.workorder;
6804
+ if (docType) return gdtiUri(this.productionSpec.companyPrefix, docType, serial);
6805
+ throw new Error(
6806
+ "work order needs an identifier the model declares: either `identity.namespaces` (CBV \xA78.5.4/\xA78.5.5 `.../bt/<id>`) or `identity.documentTypes.workorder` (the GDTI document type this site assigns). The kernel does not choose a document type \u2014 GS1 assigns the company prefix, the company assigns the type."
6807
+ );
6395
6808
  }
6396
- /** 정의 모드 작업지시 — 레시피 산출물 1개. */
6397
6809
  onOrderDef(_spec) {
6398
- const rc = this.recipeDef();
6399
6810
  const id = `order-${++this.orderSeq}`;
6400
- const wo = gdtiUri(this.productionSpec.companyPrefix, "403", ++this.soSeq);
6401
- const order = { id, kind: "workorder", status: "created", gtin: this.classOf(rc.outputs[0].material), requested: 1, fulfilled: 0, bizTransaction: wo, allocated: [], picked: [], ...this.promiseOf(_spec) };
6811
+ const recipes = this.recipesDef();
6812
+ const chosen = this.productionSpec.recipeKey ? void 0 : recipes[(this.orderSeq - 1) % recipes.length];
6813
+ const rc = this.recipeDef(chosen ? { recipeKey: chosen.key } : void 0);
6814
+ const wo = this.workOrderId(++this.soSeq);
6815
+ const order = { id, kind: "workorder", status: "created", ...chosen ? { recipeKey: chosen.key } : {}, gtin: this.classOf(rc.outputs[0].material), requested: 1, fulfilled: 0, bizTransaction: wo, allocated: [], picked: [], ...this.promiseOf(_spec) };
6402
6816
  this.orders.set(id, order);
6403
6817
  this.emitOrder(order);
6404
6818
  }
6405
6819
  /** 정의 모드 할당 — 레시피 입력 BOM 전량 확보 후 첫 라우트 스텝 태스크. */
6406
6820
  allocateDef(o) {
6407
- const ops = this.routeOps();
6408
- if (!ops.length || !ops[0].locationType || !this.locationByType(ops[0].locationType)) return;
6409
- const rc = this.recipeDef();
6821
+ const ops = this.routeOps(o);
6822
+ if (!ops.length) return;
6823
+ const firstType = ops[0].locationType;
6824
+ if (!firstType) {
6825
+ throw new Error(`route step '${ops[0].key}' declares no locationType \u2014 the kernel cannot choose where work happens`);
6826
+ }
6827
+ const rc = this.recipeDef(o);
6410
6828
  const picks = [];
6829
+ const locsOfType = /* @__PURE__ */ new Map();
6830
+ for (const n of this.locations.values()) {
6831
+ const bin = locsOfType.get(n.type);
6832
+ if (bin) bin.push(n);
6833
+ else locsOfType.set(n.type, [n]);
6834
+ }
6411
6835
  for (const line of rc.inputs) {
6412
6836
  const g = this.classOf(line.material);
6413
- const available = [...this.items.values()].filter((i) => i.gtin === g && i.disposition === DISP.sellable && this.locations.get(i.location)?.type === "raw-store").map((i) => ({ epc: i.epc, location: i.location, qty: 1 }));
6837
+ const fromType = this.locationTypeOfMaterial(line.material);
6838
+ const available = [];
6839
+ for (const n of locsOfType.get(fromType) ?? []) {
6840
+ for (const i of this.items.at(n.id)) {
6841
+ if (i.gtin === g && i.disposition === DISP.sellable) available.push({ epc: i.epc, location: i.location, qty: 1 });
6842
+ }
6843
+ }
6414
6844
  const chosen = this.policy.selectStock({ gtin: g, qty: line.qty, available });
6415
6845
  if (chosen.length < line.qty) return;
6416
6846
  picks.push(...chosen);
@@ -6432,33 +6862,51 @@ var MesKernel = class extends FlowEngine {
6432
6862
  onTaskCompleteDef(t) {
6433
6863
  const order = this.orders.get(t.orderId);
6434
6864
  if (!order) throw new Error(`task ${t.id}: order vanished between the core check and the domain hook`);
6435
- const rc = this.recipeDef();
6436
- const ops = this.routeOps();
6865
+ const rc = this.recipeDef(order);
6866
+ const ops = this.routeOps(order);
6437
6867
  const i = ops.findIndex((s) => s.key === t.kind);
6438
6868
  const loc = this.locations.get(t.toNode);
6439
6869
  const bizStep = ops[i]?.bizStep ?? MES_BIZSTEP.producing;
6440
6870
  const isLast = i === ops.length - 1;
6441
6871
  if (!isLast) {
6442
- const inputs = order.allocated.slice();
6443
- const wip2 = sgtinUri(this.productionSpec.companyPrefix, "WIP", ++this.wipSeq);
6444
- const wipGtin = sgtinClass(this.productionSpec.companyPrefix, "WIP");
6445
- this.transform(inputs, [{ epc: wip2, gtin: wipGtin, qty: 1, location: loc.id, disposition: DISP.in_progress }], { bizStep, disposition: DISP.in_progress, transformationId: order.bizTransaction, readPoint: loc.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
6446
- order.allocated = [wip2];
6872
+ const producedKey = this.producedMaterialKeyOf(t.kind);
6447
6873
  const next = ops[i + 1];
6448
- this.emitStationDef(order, next, wip2);
6874
+ if (producedKey) {
6875
+ const inputs = order.allocated.slice();
6876
+ const made = this.serialOf(producedKey, ++this.wipSeq);
6877
+ this.transform(inputs, [{ epc: made, gtin: this.classOf(producedKey), qty: 1, location: loc.id, disposition: DISP.in_progress }], { bizStep, disposition: DISP.in_progress, transformationId: order.bizTransaction, readPoint: loc.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
6878
+ order.allocated = [made];
6879
+ }
6880
+ const carried = order.allocated[0];
6881
+ if (!carried) {
6882
+ throw new Error(
6883
+ `order ${order.id} at step '${t.kind}': nothing to carry to '${next.key}' \u2014 the order holds no allocated material and this step declares no produced material. Declare the step's input or its output in the model.`
6884
+ );
6885
+ }
6886
+ this.emitStationDef(order, next, carried);
6449
6887
  order.status = "op-" + next.key;
6450
6888
  this.emitOrder(order);
6451
6889
  return;
6452
6890
  }
6453
- const fgStore = this.locationByType("fg-store");
6454
- const wip = order.allocated[0];
6891
+ const fgStore = this.locationOfMaterial(rc.outputs[0].material);
6892
+ const consumed = order.allocated.slice();
6893
+ if (!consumed.length) {
6894
+ if (order.seedIncomplete) {
6895
+ order.status = "blocked-seed-incomplete";
6896
+ this.emitOrder(order);
6897
+ return;
6898
+ }
6899
+ throw new Error(
6900
+ `order ${order.id} at final step '${t.kind}': nothing allocated to consume \u2014 the kernel does not make a product out of nothing. Declare the recipe inputs, or the step's produced material.`
6901
+ );
6902
+ }
6455
6903
  const good = this.rng() < (this.yieldOf(t.kind) ?? DEFAULT_YIELD);
6456
6904
  t.outcome = good ? "good" : "scrap";
6457
6905
  this.recordOutput(t.resource, good);
6458
6906
  const disp = good ? DISP.sellable : DISP.non_sellable;
6459
6907
  const outEpc = this.serialOf(rc.outputs[0].material, ++this.prodSeq);
6460
6908
  const outGtin = this.classOf(rc.outputs[0].material);
6461
- this.transform([wip], [{ epc: outEpc, gtin: outGtin, qty: 1, location: fgStore.id, disposition: disp }], { bizStep, disposition: disp, transformationId: order.bizTransaction, readPoint: loc.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
6909
+ this.transform(consumed, [{ epc: outEpc, gtin: outGtin, qty: 1, location: fgStore.id, disposition: disp }], { bizStep, disposition: disp, transformationId: order.bizTransaction, readPoint: loc.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
6462
6910
  this.emit(objectEvent({ eventTime: this.now(), action: "OBSERVE", bizStep: MES_BIZSTEP.storing, disposition: disp, epcList: [outEpc], quantityList: [{ epcClass: outGtin, quantity: 1 }], readPoint: fgStore.id, bizLocation: fgStore.id }));
6463
6911
  order.allocated = [];
6464
6912
  order.fulfilled = 1;
@@ -6752,6 +7200,18 @@ var EmsKernel = class extends FlowEngine {
6752
7200
  startMs: w.startMs,
6753
7201
  endMs: w.endMs,
6754
7202
  ...w.maxKW !== void 0 ? { maxKW: w.maxKW } : {},
7203
+ /*
7204
+ * ── 선언된 것만의 합도 **사실로 남긴다** (2026-08-20) ────────────────────
7205
+ *
7206
+ * 상태에만 두면 저널을 읽는 쪽(성과·타임라인·요금 화면)은 총합만 본다. 실 서버에서 확인한 모양이
7207
+ * 그랬다: 유령 계량기 300kW 가 섞인 구간이 「계약 1,000 초과 1,100」으로만 남고, 그중 얼마가
7208
+ * 정체 모를 계량기인지 되짚을 길이 없었다. 지나간 구간의 두 수는 나중에 계산할 수 없다 —
7209
+ * 그 순간의 지점별 kW 를 커널이 보관하지 않기 때문이다.
7210
+ *
7211
+ * `overContract` 는 여기서도 총합으로만 판정한다(요금이 그렇게 매겨진다). 선언된 것만으로는
7212
+ * 넘지 않았다는 판단은 **읽는 쪽이** 이 두 수로 한다 — 판정을 두 벌 만들지 않는다.
7213
+ */
7214
+ ...w.maxDeclaredKW !== void 0 ? { maxDeclaredKW: w.maxDeclaredKW } : {},
6755
7215
  /* 총부하도 사실로 남긴다 — 이것이 없으면 저널을 읽는 쪽은 「무엇이 깎였나」를 되짚을 수 없다
6756
7216
  (요금은 순수요로 매겨지지만, 그 수요를 만든 것은 설비 부하다). */
6757
7217
  ...w.grossMaxKW !== void 0 ? { grossMaxKW: w.grossMaxKW } : {},
@@ -6912,7 +7372,7 @@ var EmsKernel = class extends FlowEngine {
6912
7372
  * **아무 일도 하지 않는다** — 그런데 조용히 넘기지 않는다: 이 커널이 그런 요청을 받았다는 것은
6913
7373
  * **배선이 잘못됐다는 사실**이고(EMS 트윈에 물류 명령을 보낸 것), 조용히 넘기면 그 사실이 사라진다.
6914
7374
  *
6915
- * 던지지도 않는다: 저널 재생 중이라면 트윈 전체가 멈춘다. 그래서 커널이 담을 줄 모르는 사건을
7375
+ * 오류를 내지도 않는다: 저널 재생 중이라면 트윈 전체가 멈춘다. 그래서 커널이 담을 줄 모르는 사건을
6916
7376
  * 세는 자리(`ObservedReducer.unhandled`)와 같은 규율으로, **개수를 세어 상태로 낸다.**
6917
7377
  */
6918
7378
  flowRequests = /* @__PURE__ */ new Map();
@@ -7828,13 +8288,11 @@ function retiredVocabularyIn(line) {
7828
8288
  FlowEngine,
7829
8289
  GUARD_PRAGMA,
7830
8290
  ILMD_ATTR,
8291
+ ItemStore,
7831
8292
  LOCATION_SATURATION_NEAR,
7832
8293
  MATERIAL_PROPERTY,
7833
8294
  MES_BIZSTEP,
7834
8295
  MES_LOCATION_TYPES,
7835
- MES_PART_GTINS,
7836
- MES_PRODUCTS,
7837
- MES_PRODUCT_GTINS,
7838
8296
  MES_TYPES,
7839
8297
  MesKernel,
7840
8298
  OPERATION_PROPERTY,
@@ -7868,9 +8326,11 @@ function retiredVocabularyIn(line) {
7868
8326
  axisAppliesTo,
7869
8327
  axisInfo,
7870
8328
  axisSource,
8329
+ bizTransactionUri,
7871
8330
  capabilitiesForType,
7872
8331
  capabilityOf,
7873
8332
  classClosure,
8333
+ classIdentifierViolation,
7874
8334
  commandsOf,
7875
8335
  compareStates,
7876
8336
  computeOee,
@@ -7894,6 +8354,7 @@ function retiredVocabularyIn(line) {
7894
8354
  generationFractionAt,
7895
8355
  graiUri,
7896
8356
  hierarchyOf,
8357
+ identityGroundingOf,
7897
8358
  inWorkCalendar,
7898
8359
  inWorkCalendarAt,
7899
8360
  ingest,