@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.
- package/dist/flow-engine.d.ts +2 -1
- package/dist/flow-engine.js +4 -0
- package/dist/mes-kernel.js +10 -2
- package/dist/observed-reducer.js +1 -0
- package/dist-cjs/index.cjs +29 -2
- package/package.json +2 -2
package/dist/flow-engine.d.ts
CHANGED
|
@@ -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;
|
package/dist/flow-engine.js
CHANGED
|
@@ -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). */
|
package/dist/mes-kernel.js
CHANGED
|
@@ -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
|
|
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 이 레시피 입력 자재면 **그 자재가 선언한 자리**에 생성. */
|
package/dist/observed-reducer.js
CHANGED
|
@@ -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`). */
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
+
"@operato/ops-contract": "^0.9.32"
|
|
32
32
|
}
|
|
33
33
|
}
|