@operato/twin-kernel 0.11.21 → 0.11.22

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.
@@ -1,4 +1,4 @@
1
- import type { MaterialActual, TestResult, ISOTime, MaterialQuantity, WorkCalendarEntry, EffectivePeriod, Effectivity, OffCalendarReason, ResourceProperty, ResourceClassDef, MaterialDefinition, Attention, TwinModelDef, CanonicalEnvelope, Command, CommandAck, EventHandler, EquipmentMotion, AssetState, GeneratorSpec, InterventionOutcome, OrderState, PersonState, ScenarioControl, ScenarioOverride, StateSnapshot, TwinKernel, Unsubscribe, LocationState, ItemState, EquipmentState, OrderStatusDelta, TaskState, TaskStatus, StructureShift, IdentityGroundingView, IdentityDeclaration, TestSpecificationCriterion, LocationObservation, DispositionFact, AllocatedQuantity, MaterialLotUse } from '@operato/ops-contract';
1
+ import type { MaterialActual, RecipeExecutionBasis, TestResult, ISOTime, MaterialQuantity, WorkCalendarEntry, EffectivePeriod, Effectivity, OffCalendarReason, ResourceProperty, ResourceClassDef, MaterialDefinition, Attention, TwinModelDef, CanonicalEnvelope, Command, CommandAck, EventHandler, EquipmentMotion, AssetState, GeneratorSpec, InterventionOutcome, OrderState, PersonState, ScenarioControl, ScenarioOverride, StateSnapshot, TwinKernel, Unsubscribe, LocationState, ItemState, EquipmentState, OrderStatusDelta, TaskState, TaskStatus, StructureShift, IdentityGroundingView, IdentityDeclaration, TestSpecificationCriterion, LocationObservation, DispositionFact, AllocatedQuantity, MaterialLotUse } from '@operato/ops-contract';
2
2
  import { type RatedUsage, type UsedUsage } from '@operato/ops-contract';
3
3
  import type { VocabularyElement } from '@operato/ops-contract';
4
4
  import type { ReducerCheckpoint } from './observed-reducer.ts';
@@ -271,6 +271,7 @@ export declare function allocatedQty(rows: readonly AllocatedQuantity[] | undefi
271
271
  /** 잡아 둔 줄들의 식별자 — EPCIS 이벤트가 싣는 것은 열쇠뿐이다. */
272
272
  export declare function allocatedEpcs(rows: readonly AllocatedQuantity[] | undefined): string[];
273
273
  export interface FlowOrder {
274
+ recipeBasis?: RecipeExecutionBasis;
274
275
  id: string;
275
276
  kind: string;
276
277
  status: string;
@@ -1241,6 +1241,7 @@ export class FlowEngine {
1241
1241
  ...(o.allocated?.length ? { allocated: o.allocated } : {}),
1242
1242
  ...(o.gtin ? { gtin: o.gtin } : {}),
1243
1243
  ...(o.recipeKey ? { recipeKey: o.recipeKey } : {}),
1244
+ ...(o.recipeBasis ? { recipeBasis: structuredClone(o.recipeBasis) } : {}),
1244
1245
  ...(o.bizTransaction ? { bizTransaction: o.bizTransaction } : {}),
1245
1246
  ...(o.dockDoor ? { dockDoor: o.dockDoor } : {}),
1246
1247
  ...(o.windowStartMs !== undefined ? { windowStartMs: o.windowStartMs } : {})
@@ -1303,6 +1304,7 @@ export class FlowEngine {
1303
1304
  /* 레시피도 함께 — 품목만 이어받으면 되살아난 오더가 「무엇으로」를 모르고, 그 오더의
1304
1305
  남은 공정·소요가 다른 레시피 기준으로 계산된다(같은 품목에 대체 레시피가 있다). */
1305
1306
  ...(o.recipeKey ? { recipeKey: o.recipeKey } : {}),
1307
+ ...(o.recipeBasis ? { recipeBasis: structuredClone(o.recipeBasis) } : {}),
1306
1308
  bizTransaction: o.bizTransaction ?? '', allocated: resolved.kept, picked: [], shipmentEpc: null,
1307
1309
  /* 라인도 같다 — 씨앗은 남은 양만 남기고, 미러는 원본이 말한 요청·이행을 그대로 든다. */
1308
1310
  lines: observing ? (o.lines ?? []).map(l => ({ ...l })) : lines,
@@ -1820,6 +1822,7 @@ export class FlowEngine {
1820
1822
  ...(o.gtin ? { gtin: o.gtin } : {}),
1821
1823
  /* **어느 레시피로 만드나** — 품목만으로는 「무엇으로」가 안 남는다(같은 품목의 대체 레시피가 있다). */
1822
1824
  ...(o.recipeKey ? { recipeKey: o.recipeKey } : {}),
1825
+ ...(o.recipeBasis ? { recipeBasis: structuredClone(o.recipeBasis) } : {}),
1823
1826
  /* 씨앗이 다 심지 못했다는 사실 — 조용히 자르지 않는다(그 오더의 답은 부족한 씨앗 위에 있다). */
1824
1827
  ...(o.seedIncomplete ? { seedIncomplete: true } : {}),
1825
1828
  /* 왜 대기하는지 — 없으면 화면은 「running」만 보여 주고 사람은 원인을 찾을 자리가 없다. */
@@ -2989,6 +2992,7 @@ export class FlowEngine {
2989
2992
  /* 어느 레시피로 만드는가 — 품목만 보내면 미러는 「무엇으로」를 알 수 없다(같은 품목에 대체
2990
2993
  레시피가 있다). 시뮬만 아는 사실이 남으면 두 스냅샷을 같은 규칙으로 읽을 수 없다. */
2991
2994
  ...(o.recipeKey ? { recipeKey: o.recipeKey } : {}),
2995
+ ...(o.recipeBasis ? { recipeBasis: structuredClone(o.recipeBasis) } : {}),
2992
2996
  ...(o.allocated?.length ? { allocated: o.allocated.slice() } : {}),
2993
2997
  ...(o.bizTransaction ? { bizTransaction: o.bizTransaction } : {}),
2994
2998
  /* 약속해 둔 자리·시각창 — 이것이 빠져서 야드 트윈이 재기동마다 첫 틱에 죽었다(§dockDoor). */
@@ -8,7 +8,7 @@
8
8
  * ③ 셋업/체인지오버 = work-center 가 제품 전환 시 셋업(changeoverKey=제품 gtin) → OEE Availability.
9
9
  * ④ OEE = base 가 설비(설비)별 계측(가동/셋업/기아/품질). cut=cutter·weld=welder 이종 자원.
10
10
  */
11
- import { identityGroundingOf, procedureViolations } from '@operato/ops-contract';
11
+ import { identityGroundingOf, procedureViolations, isRecipeExecutionBasis } from '@operato/ops-contract';
12
12
  import { firstFitPolicy } from "./allocation-policy.js";
13
13
  import { FlowEngine, allocatedEpcs, allocatedQty } from "./flow-engine.js";
14
14
  import { DISP, objectEvent, transactionEvent, gdtiUri, sgtinClass, bizTransactionUri } from '@operato/ops-contract';
@@ -472,6 +472,11 @@ export class MesKernel extends FlowEngine {
472
472
  * 그 뒤 계보·재고·수율이 전부 엉뚱한 품목에 붙는다.
473
473
  */
474
474
  recipeDef(order) {
475
+ if (order?.recipeBasis !== undefined) {
476
+ if (!isRecipeExecutionBasis(order.recipeBasis) || (order.recipeKey && order.recipeKey !== order.recipeBasis.recipe.key))
477
+ throw new Error('Invalid pinned recipe evidence; refusing to substitute the current master.');
478
+ return order.recipeBasis.recipe;
479
+ }
475
480
  const d = this.productionSpec.definition;
476
481
  const key = order?.recipeKey ?? this.productionSpec.recipeKey;
477
482
  if (key) {
@@ -658,7 +663,10 @@ export class MesKernel extends FlowEngine {
658
663
  /** recipe.route → 오퍼레이션 시퀀스 해소. 오더가 자기 레시피를 들면 그 라우트다. */
659
664
  routeOps(order) {
660
665
  const d = this.productionSpec.definition;
661
- const route = d.routes?.find(r => r.key === this.recipeDef(order).route);
666
+ const recipe = this.recipeDef(order);
667
+ const route = order?.recipeBasis ? order.recipeBasis.route : d.routes?.find(r => r.key === recipe.route);
668
+ if (order?.recipeBasis && route?.steps.some(key => !d.operations?.some(operation => operation.key === key)))
669
+ throw new Error('A captured recipe route references an unavailable operation; refusing a shortened route.');
662
670
  return (route?.steps ?? []).map(sk => d.operations?.find(o => o.key === sk)).filter((o) => !!o);
663
671
  }
664
672
  /** 정의 모드 수령 — skuMix gtin 이 레시피 입력 자재면 **그 자재가 선언한 자리**에 생성. */
@@ -722,6 +722,7 @@ export class ObservedReducer {
722
722
  ...(d.gtin ? { gtin: d.gtin } : {}),
723
723
  /* 어느 레시피로 만드는가 — 품목만으로는 「무엇으로」가 남지 않는다(같은 품목에 대체 레시피). */
724
724
  ...(d.recipeKey ? { recipeKey: d.recipeKey } : {}),
725
+ ...(d.recipeBasis ? { recipeBasis: structuredClone(d.recipeBasis) } : {}),
725
726
  ...(d.allocated?.length ? { allocated: d.allocated.slice() } : {}),
726
727
  ...(d.bizTransaction ? { bizTransaction: d.bizTransaction } : {}),
727
728
  /* 이행하는 계획 — 「이 계획이 얼마나 됐나」를 이 값으로 묶어 답한다(§`OrderState.operationsRequestId`). */
@@ -635,6 +635,14 @@ function capabilityOf(r, ctx) {
635
635
  return { available: false, reason: "working" };
636
636
  return { available: true, reason: "available" };
637
637
  }
638
+ function isRecipeExecutionBasis(value) {
639
+ if (!value || typeof value !== "object")
640
+ return false;
641
+ const v = value;
642
+ const text = (x) => typeof x === "string" && x.trim().length > 0;
643
+ const lines = (x) => Array.isArray(x) && x.length > 0 && x.every((row) => row && text(row.material) && typeof row.qty === "number" && Number.isFinite(row.qty) && row.qty > 0 && (row.operation === void 0 || text(row.operation)));
644
+ return [v.basisId, v.recipeId, v.revisionId, v.planId, v.contentHash, v.businessTime].every(text) && /(?:Z|[+-]\d{2}:\d{2})$/.test(v.businessTime) && Number.isFinite(Date.parse(v.businessTime)) && !!v.recipe && text(v.recipe.key) && text(v.recipe.label) && lines(v.recipe.inputs) && lines(v.recipe.outputs) && (v.recipe.route === void 0 ? v.route === void 0 && v.recipe.inputs.every((line) => !line.operation) : text(v.recipe.route) && !!v.route && v.route.key === v.recipe.route && Array.isArray(v.route.steps) && v.route.steps.every(text) && v.recipe.inputs.every((line) => !line.operation || v.route.steps.includes(line.operation)));
645
+ }
638
646
  var OP_EVENT = {
639
647
  task: "task.status",
640
648
  equipment: "equipment.status",
@@ -2014,13 +2022,19 @@ var SPECS = {
2014
2022
  *
2015
2023
  * 새 어휘가 아니다 — 계약이 `recipeKey` 를 네 자리에서 이미 쓴다. 오더 관측에만 자리가 없었다.
2016
2024
  */
2017
- recipeKey: "string"
2025
+ recipeKey: "string",
2026
+ recipeBasis: "object"
2018
2027
  },
2019
2028
  /*
2020
2029
  * 확보분의 안쪽을 본다 — 열쇠와 수 둘이다(§`AllocatedQuantity`). 안 보면 틀린 이름이 조용히
2021
2030
  * 지나가고(`epcs`·`quantity` 따위), 미러는 잡힌 수를 0 으로 읽는다.
2022
2031
  */
2023
2032
  shapes: {
2033
+ recipeBasis: {
2034
+ required: ["basisId", "recipeId", "revisionId", "planId", "contentHash", "businessTime", "recipe"],
2035
+ fields: { basisId: "string", recipeId: "string", revisionId: "string", planId: "string", contentHash: "string", businessTime: "string" }
2036
+ // Owned composition and captured route are additionally checked by isRecipeExecutionBasis below.
2037
+ },
2024
2038
  allocated: {
2025
2039
  required: ["epc", "qty"],
2026
2040
  fields: { epc: "string", qty: "number" },
@@ -3097,6 +3111,7 @@ var ObservedReducer = class {
3097
3111
  ...d.gtin ? { gtin: d.gtin } : {},
3098
3112
  /* 어느 레시피로 만드는가 — 품목만으로는 「무엇으로」가 남지 않는다(같은 품목에 대체 레시피). */
3099
3113
  ...d.recipeKey ? { recipeKey: d.recipeKey } : {},
3114
+ ...d.recipeBasis ? { recipeBasis: structuredClone(d.recipeBasis) } : {},
3100
3115
  ...d.allocated?.length ? { allocated: d.allocated.slice() } : {},
3101
3116
  ...d.bizTransaction ? { bizTransaction: d.bizTransaction } : {},
3102
3117
  /* 이행하는 계획 — 「이 계획이 얼마나 됐나」를 이 값으로 묶어 답한다(§`OrderState.operationsRequestId`). */
@@ -4672,6 +4687,7 @@ var FlowEngine = class {
4672
4687
  ...o.allocated?.length ? { allocated: o.allocated } : {},
4673
4688
  ...o.gtin ? { gtin: o.gtin } : {},
4674
4689
  ...o.recipeKey ? { recipeKey: o.recipeKey } : {},
4690
+ ...o.recipeBasis ? { recipeBasis: structuredClone(o.recipeBasis) } : {},
4675
4691
  ...o.bizTransaction ? { bizTransaction: o.bizTransaction } : {},
4676
4692
  ...o.dockDoor ? { dockDoor: o.dockDoor } : {},
4677
4693
  ...o.windowStartMs !== void 0 ? { windowStartMs: o.windowStartMs } : {}
@@ -4696,6 +4712,7 @@ var FlowEngine = class {
4696
4712
  /* 레시피도 함께 — 품목만 이어받으면 되살아난 오더가 「무엇으로」를 모르고, 그 오더의
4697
4713
  남은 공정·소요가 다른 레시피 기준으로 계산된다(같은 품목에 대체 레시피가 있다). */
4698
4714
  ...o.recipeKey ? { recipeKey: o.recipeKey } : {},
4715
+ ...o.recipeBasis ? { recipeBasis: structuredClone(o.recipeBasis) } : {},
4699
4716
  bizTransaction: o.bizTransaction ?? "",
4700
4717
  allocated: resolved.kept,
4701
4718
  picked: [],
@@ -5129,6 +5146,7 @@ var FlowEngine = class {
5129
5146
  ...o.gtin ? { gtin: o.gtin } : {},
5130
5147
  /* **어느 레시피로 만드나** — 품목만으로는 「무엇으로」가 안 남는다(같은 품목의 대체 레시피가 있다). */
5131
5148
  ...o.recipeKey ? { recipeKey: o.recipeKey } : {},
5149
+ ...o.recipeBasis ? { recipeBasis: structuredClone(o.recipeBasis) } : {},
5132
5150
  /* 씨앗이 다 심지 못했다는 사실 — 조용히 자르지 않는다(그 오더의 답은 부족한 씨앗 위에 있다). */
5133
5151
  ...o.seedIncomplete ? { seedIncomplete: true } : {},
5134
5152
  /* 왜 대기하는지 — 없으면 화면은 「running」만 보여 주고 사람은 원인을 찾을 자리가 없다. */
@@ -6163,6 +6181,7 @@ var FlowEngine = class {
6163
6181
  /* 어느 레시피로 만드는가 — 품목만 보내면 미러는 「무엇으로」를 알 수 없다(같은 품목에 대체
6164
6182
  레시피가 있다). 시뮬만 아는 사실이 남으면 두 스냅샷을 같은 규칙으로 읽을 수 없다. */
6165
6183
  ...o.recipeKey ? { recipeKey: o.recipeKey } : {},
6184
+ ...o.recipeBasis ? { recipeBasis: structuredClone(o.recipeBasis) } : {},
6166
6185
  ...o.allocated?.length ? { allocated: o.allocated.slice() } : {},
6167
6186
  ...o.bizTransaction ? { bizTransaction: o.bizTransaction } : {},
6168
6187
  /* 약속해 둔 자리·시각창 — 이것이 빠져서 야드 트윈이 재기동마다 첫 틱에 죽었다(§dockDoor). */
@@ -8074,6 +8093,11 @@ The steps are the source of truth for execution order; the hierarchy only names
8074
8093
  * 그 뒤 계보·재고·수율이 전부 엉뚱한 품목에 붙는다.
8075
8094
  */
8076
8095
  recipeDef(order) {
8096
+ if (order?.recipeBasis !== void 0) {
8097
+ if (!isRecipeExecutionBasis(order.recipeBasis) || order.recipeKey && order.recipeKey !== order.recipeBasis.recipe.key)
8098
+ throw new Error("Invalid pinned recipe evidence; refusing to substitute the current master.");
8099
+ return order.recipeBasis.recipe;
8100
+ }
8077
8101
  const d = this.productionSpec.definition;
8078
8102
  const key = order?.recipeKey ?? this.productionSpec.recipeKey;
8079
8103
  if (key) {
@@ -8236,7 +8260,10 @@ The steps are the source of truth for execution order; the hierarchy only names
8236
8260
  /** recipe.route → 오퍼레이션 시퀀스 해소. 오더가 자기 레시피를 들면 그 라우트다. */
8237
8261
  routeOps(order) {
8238
8262
  const d = this.productionSpec.definition;
8239
- const route = d.routes?.find((r) => r.key === this.recipeDef(order).route);
8263
+ const recipe = this.recipeDef(order);
8264
+ const route = order?.recipeBasis ? order.recipeBasis.route : d.routes?.find((r) => r.key === recipe.route);
8265
+ if (order?.recipeBasis && route?.steps.some((key) => !d.operations?.some((operation) => operation.key === key)))
8266
+ throw new Error("A captured recipe route references an unavailable operation; refusing a shortened route.");
8240
8267
  return (route?.steps ?? []).map((sk) => d.operations?.find((o) => o.key === sk)).filter((o) => !!o);
8241
8268
  }
8242
8269
  /** 정의 모드 수령 — skuMix gtin 이 레시피 입력 자재면 **그 자재가 선언한 자리**에 생성. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/twin-kernel",
3
- "version": "0.11.21",
3
+ "version": "0.11.22",
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": {
@@ -28,6 +28,6 @@
28
28
  "test": "node --test test/*.test.ts"
29
29
  },
30
30
  "dependencies": {
31
- "@operato/ops-contract": "^0.9.31"
31
+ "@operato/ops-contract": "^0.9.32"
32
32
  }
33
33
  }