@operato/twin-kernel 0.7.46 → 0.7.47

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.
@@ -2180,7 +2180,19 @@ export interface TwinModelDef {
2180
2180
  export interface ProductionSpec {
2181
2181
  /** 무엇이 있고 어떻게 흐르나 — `DomainDefinition`(자기 타입, zero-dep). */
2182
2182
  definition: import('./domain-definition.ts').DomainDefinition;
2183
- /** 자재 키 → GS1 item reference. */
2183
+ /**
2184
+ * 자재 키 → **그 자재의 클래스 식별자를 정하는 값.**
2185
+ *
2186
+ * 두 모양을 받는다. `companyPrefix` 가 있으면 값은 GS1 item reference 이고 클래스는
2187
+ * `urn:epc:idpat:sgtin:<prefix>.<ref>.*` 로 조립된다. 프리픽스가 없으면 값은 **이미 완성된 클래스
2188
+ * URI** 이고(CBV 2.0 §8.3.4 `.../class/ObjClassid`) 커널은 그것을 그대로 쓴다 — 조립하면 값을 망친다.
2189
+ *
2190
+ * ── 같은 자재를 가리키는 이름은 하나다 ──────────────────────────────────────
2191
+ * 공정 명세의 `materialSpecification.materialDefinition` 도 **이 값에서 나온 클래스 식별자**여야 한다.
2192
+ * 두 자리가 같은 자재를 다른 이름으로 부르면, 커널은 그것이 같은 자재인지 알 길이 없다 — 같은 산출을
2193
+ * 두 번 만드는 것을 막는 기동 검사가 그때 조용히 열린다(§`assertNoDoubleProduction`). 옮길 수 없는
2194
+ * 식별자는 기동에서 말한다.
2195
+ */
2184
2196
  binding?: Record<string, string>;
2185
2197
  companyPrefix?: string;
2186
2198
  /** 쓸 레시피 키(미지정 시 첫 레시피) — 직렬 생산 경로(MES)만 쓴다. */
@@ -89,9 +89,25 @@ export class MesKernel extends FlowEngine {
89
89
  * 모든 계산이 거짓이 된다).
90
90
  */
91
91
  assertNoDoubleProduction(ops) {
92
- const finals = new Set((this.productionSpec?.definition?.recipes ?? []).flatMap(r => (r.outputs ?? []).map(o => this.classOf(o.material))));
92
+ /*
93
+ * ── 두 어휘를 문자열로 견주지 않는다 (2026-08-21) ──────────────────────────
94
+ * 예전에는 `classOf(레시피 산출)` 과 `materialSpecification.materialDefinition` 을 **문자열로** 견주었다.
95
+ * 그 둘은 서로 다른 층의 이름이고(§`materialKeyOfClass`), 우연히 같은 커넥터에서는 맞았다.
96
+ * 커넥터가 둘을 다르게 실으면 — 공정 명세는 클래스 URI, `binding` 은 품번 — 이 검사가 **조용히
97
+ * 아무것도 잡지 못한다.** 같은 산출을 두 번 만드는 것을 막으려고 둔 검사인데, 바로 그때 열린다.
98
+ *
99
+ * 그래서 양쪽을 **자재 키**로 옮겨 놓고 견준다. 옮길 수 없는 식별자는 그 자리에서 말한다 —
100
+ * 런타임이 이미 같은 규칙으로 멈추므로(§`materialKeyOfClass`), 기동에서 말하는 편이 낫다.
101
+ */
102
+ const finals = new Set((this.productionSpec?.definition?.recipes ?? []).flatMap(r => (r.outputs ?? []).map(o => o.material)));
103
+ /*
104
+ * 레시피가 없으면 겹칠 것이 없다 — 그리고 그때는 `binding` 도 없다(유통가공은 산출을 자재 명세로만
105
+ * 말한다). 옮기기를 시도하면 없는 통로를 지나려 하므로 여기서 끝낸다.
106
+ */
107
+ if (!finals.size)
108
+ return;
93
109
  const bad = ops
94
- .filter(o => (o.materialSpecification ?? []).some(m => m.use === 'produced' && m.materialDefinition && finals.has(m.materialDefinition)))
110
+ .filter(o => (o.materialSpecification ?? []).some(m => m.use === 'produced' && m.materialDefinition && finals.has(this.materialKeyOfClass(m.materialDefinition, o.key))))
95
111
  .map(o => o.key);
96
112
  if (!bad.length)
97
113
  return;
@@ -6669,8 +6669,13 @@ var MesKernel = class extends FlowEngine {
6669
6669
  * 모든 계산이 거짓이 된다).
6670
6670
  */
6671
6671
  assertNoDoubleProduction(ops) {
6672
- const finals = new Set((this.productionSpec?.definition?.recipes ?? []).flatMap((r) => (r.outputs ?? []).map((o) => this.classOf(o.material))));
6673
- const bad2 = ops.filter((o) => (o.materialSpecification ?? []).some((m) => m.use === "produced" && m.materialDefinition && finals.has(m.materialDefinition))).map((o) => o.key);
6672
+ const finals = new Set((this.productionSpec?.definition?.recipes ?? []).flatMap((r) => (r.outputs ?? []).map((o) => o.material)));
6673
+ if (!finals.size) return;
6674
+ const bad2 = ops.filter(
6675
+ (o) => (o.materialSpecification ?? []).some(
6676
+ (m) => m.use === "produced" && m.materialDefinition && finals.has(this.materialKeyOfClass(m.materialDefinition, o.key))
6677
+ )
6678
+ ).map((o) => o.key);
6674
6679
  if (!bad2.length) return;
6675
6680
  throw new Error(
6676
6681
  `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.`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/twin-kernel",
3
- "version": "0.7.46",
3
+ "version": "0.7.47",
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": {