@operato/twin-kernel 0.11.22 → 0.11.23
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 +3 -1
- package/dist/flow-engine.js +16 -3
- package/dist/observed-reducer.js +14 -4
- package/dist-cjs/index.cjs +83 -9
- package/package.json +2 -2
package/dist/flow-engine.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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';
|
|
1
|
+
import type { MaterialActual, RecipeExecutionBasis, MaterialExecutionBasis, 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';
|
|
@@ -197,6 +197,7 @@ export interface FlowTask {
|
|
|
197
197
|
* 채널에 실어야 실적을 **한 곳에서** 읽는다(EPCIS 이벤트에서 다시 계산하려면 작업과 잇는 끈이 없다).
|
|
198
198
|
*/
|
|
199
199
|
materialActual?: MaterialActual[];
|
|
200
|
+
workShiftBasis?: import('@operato/ops-contract').WorkShiftExecutionBasis;
|
|
200
201
|
itemEpc: string;
|
|
201
202
|
fromNode: string;
|
|
202
203
|
toNode: string;
|
|
@@ -271,6 +272,7 @@ export declare function allocatedQty(rows: readonly AllocatedQuantity[] | undefi
|
|
|
271
272
|
/** 잡아 둔 줄들의 식별자 — EPCIS 이벤트가 싣는 것은 열쇠뿐이다. */
|
|
272
273
|
export declare function allocatedEpcs(rows: readonly AllocatedQuantity[] | undefined): string[];
|
|
273
274
|
export interface FlowOrder {
|
|
275
|
+
materialBasis?: MaterialExecutionBasis[];
|
|
274
276
|
recipeBasis?: RecipeExecutionBasis;
|
|
275
277
|
id: string;
|
|
276
278
|
kind: string;
|
package/dist/flow-engine.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* 통합 타입은 도메인 필드를 옵셔널로 넓혀(FlowItem.gtin?, FlowOrder.shipmentEpc? 등) 두 도메인을 담는다.
|
|
10
10
|
* (roadmap Phase5 발견 → 추출. [[project_flow_single_base_vision]] FlowLocation 단일 base 방향과 정합.)
|
|
11
11
|
*/
|
|
12
|
+
import { isWorkShiftExecutionBasis } from '@operato/ops-contract';
|
|
12
13
|
import { OP_EVENT, CMD, USE_UOM, locationStatusOf, readBoardEquipment, equipmentClassMembership, readBoardLocations, readBoardAssets, classClosure, capabilityOf, requiredTestsFor, priorityRank, dueStatusOf, isOrderTerminal, effectivityAt, offCalendarAt, offCalendarReasonAt, minuteOfDayAt, activeShiftAt, subLotIdOf, itemKeyOf, identityGroundingOf, outsideLimit, computeOee } from '@operato/ops-contract';
|
|
13
14
|
import { ObservedReducer } from "./observed-reducer.js";
|
|
14
15
|
/* 주체를 정하는 규칙은 유입 문과 한 벌이다 — 두 곳에 적으면 한쪽만 고쳐진다. */
|
|
@@ -1050,6 +1051,10 @@ export class FlowEngine {
|
|
|
1050
1051
|
* 그래서 규칙을 둘로 쪼개지 않고 **목적을 밝힌다.** 같은 함수, 같은 주입 경로, 다른 판정 하나다.
|
|
1051
1052
|
*/
|
|
1052
1053
|
purpose = 'seed') {
|
|
1054
|
+
for (const task of snap.tasks ?? []) {
|
|
1055
|
+
if (task.workShiftBasis !== undefined && !isWorkShiftExecutionBasis(task.workShiftBasis))
|
|
1056
|
+
throw new Error('Invalid pinned work shift evidence');
|
|
1057
|
+
}
|
|
1053
1058
|
/* 확인 처리를 먼저 이어받는다 — 아래에서 상태를 주입하면 곧바로 주목 신호가 계산되므로, 늦게
|
|
1054
1059
|
* 이어받으면 그 한 번은 확인 안 된 것으로 계산된다(화면이 잠깐 빨개진다). */
|
|
1055
1060
|
for (const id of snap.acked ?? [])
|
|
@@ -1242,6 +1247,7 @@ export class FlowEngine {
|
|
|
1242
1247
|
...(o.gtin ? { gtin: o.gtin } : {}),
|
|
1243
1248
|
...(o.recipeKey ? { recipeKey: o.recipeKey } : {}),
|
|
1244
1249
|
...(o.recipeBasis ? { recipeBasis: structuredClone(o.recipeBasis) } : {}),
|
|
1250
|
+
...(o.materialBasis ? { materialBasis: structuredClone(o.materialBasis) } : {}),
|
|
1245
1251
|
...(o.bizTransaction ? { bizTransaction: o.bizTransaction } : {}),
|
|
1246
1252
|
...(o.dockDoor ? { dockDoor: o.dockDoor } : {}),
|
|
1247
1253
|
...(o.windowStartMs !== undefined ? { windowStartMs: o.windowStartMs } : {})
|
|
@@ -1305,6 +1311,7 @@ export class FlowEngine {
|
|
|
1305
1311
|
남은 공정·소요가 다른 레시피 기준으로 계산된다(같은 품목에 대체 레시피가 있다). */
|
|
1306
1312
|
...(o.recipeKey ? { recipeKey: o.recipeKey } : {}),
|
|
1307
1313
|
...(o.recipeBasis ? { recipeBasis: structuredClone(o.recipeBasis) } : {}),
|
|
1314
|
+
...(o.materialBasis ? { materialBasis: structuredClone(o.materialBasis) } : {}),
|
|
1308
1315
|
bizTransaction: o.bizTransaction ?? '', allocated: resolved.kept, picked: [], shipmentEpc: null,
|
|
1309
1316
|
/* 라인도 같다 — 씨앗은 남은 양만 남기고, 미러는 원본이 말한 요청·이행을 그대로 든다. */
|
|
1310
1317
|
lines: observing ? (o.lines ?? []).map(l => ({ ...l })) : lines,
|
|
@@ -1368,6 +1375,7 @@ export class FlowEngine {
|
|
|
1368
1375
|
status: purpose === 'observe' ? t.status : known && t.status === 'in-progress' ? 'in-progress' : 'created',
|
|
1369
1376
|
/* 이미 일어난 자재 이동은 **씨앗에도 남는다** — 잃으면 실적이 재기동마다 지워진다. */
|
|
1370
1377
|
...(t.materialActual?.length ? { materialActual: t.materialActual.map(r => ({ ...r })) } : {}),
|
|
1378
|
+
...(t.workShiftBasis ? { workShiftBasis: structuredClone(t.workShiftBasis) } : {}),
|
|
1371
1379
|
itemEpc: t.itemRefs?.[0] ?? '',
|
|
1372
1380
|
fromNode: t.fromNode ?? '', toNode: t.toNode ?? '',
|
|
1373
1381
|
/*
|
|
@@ -1802,6 +1810,7 @@ export class FlowEngine {
|
|
|
1802
1810
|
...(t.assets?.length ? { assets: t.assets.slice() } : {}),
|
|
1803
1811
|
...(t.resources?.length ? { resources: t.resources.slice() } : {}),
|
|
1804
1812
|
...(t.materialActual?.length ? { materialActual: t.materialActual.map(r => ({ ...r })) } : {}),
|
|
1813
|
+
...(t.workShiftBasis ? { workShiftBasis: structuredClone(t.workShiftBasis) } : {}),
|
|
1805
1814
|
...(t.priority !== undefined ? { priority: t.priority } : {}),
|
|
1806
1815
|
...(t.startTime ? { startTime: t.startTime } : {}),
|
|
1807
1816
|
...(t.endTime ? { endTime: t.endTime } : {})
|
|
@@ -1823,6 +1832,7 @@ export class FlowEngine {
|
|
|
1823
1832
|
/* **어느 레시피로 만드나** — 품목만으로는 「무엇으로」가 안 남는다(같은 품목의 대체 레시피가 있다). */
|
|
1824
1833
|
...(o.recipeKey ? { recipeKey: o.recipeKey } : {}),
|
|
1825
1834
|
...(o.recipeBasis ? { recipeBasis: structuredClone(o.recipeBasis) } : {}),
|
|
1835
|
+
...(o.materialBasis ? { materialBasis: structuredClone(o.materialBasis) } : {}),
|
|
1826
1836
|
/* 씨앗이 다 심지 못했다는 사실 — 조용히 자르지 않는다(그 오더의 답은 부족한 씨앗 위에 있다). */
|
|
1827
1837
|
...(o.seedIncomplete ? { seedIncomplete: true } : {}),
|
|
1828
1838
|
/* 왜 대기하는지 — 없으면 화면은 「running」만 보여 주고 사람은 원인을 찾을 자리가 없다. */
|
|
@@ -2954,6 +2964,7 @@ export class FlowEngine {
|
|
|
2954
2964
|
...(t.resources?.length ? { resources: t.resources.slice() } : {}),
|
|
2955
2965
|
/* 실제 자재 이동 — 인원·설비와 같은 채널(실적을 한 곳에서 읽는다). */
|
|
2956
2966
|
...(t.materialActual?.length ? { materialActual: t.materialActual.map(r => ({ ...r })) } : {}),
|
|
2967
|
+
...(t.workShiftBasis ? { workShiftBasis: structuredClone(t.workShiftBasis) } : {}),
|
|
2957
2968
|
...(t.durationMs ? { durationMs: t.durationMs } : {}),
|
|
2958
2969
|
/* 품질 판정이 있었으면 **그 자리에서** 실어 보낸다 — 없으면 필드를 만들지 않는다(모름≠양품). */
|
|
2959
2970
|
...(t.outcome ? { outcome: t.outcome } : {}),
|
|
@@ -2993,6 +3004,7 @@ export class FlowEngine {
|
|
|
2993
3004
|
레시피가 있다). 시뮬만 아는 사실이 남으면 두 스냅샷을 같은 규칙으로 읽을 수 없다. */
|
|
2994
3005
|
...(o.recipeKey ? { recipeKey: o.recipeKey } : {}),
|
|
2995
3006
|
...(o.recipeBasis ? { recipeBasis: structuredClone(o.recipeBasis) } : {}),
|
|
3007
|
+
...(o.materialBasis ? { materialBasis: structuredClone(o.materialBasis) } : {}),
|
|
2996
3008
|
...(o.allocated?.length ? { allocated: o.allocated.slice() } : {}),
|
|
2997
3009
|
...(o.bizTransaction ? { bizTransaction: o.bizTransaction } : {}),
|
|
2998
3010
|
/* 약속해 둔 자리·시각창 — 이것이 빠져서 야드 트윈이 재기동마다 첫 틱에 죽었다(§dockDoor). */
|
|
@@ -3197,7 +3209,7 @@ export class FlowEngine {
|
|
|
3197
3209
|
for (const it of this.items.at(at)) {
|
|
3198
3210
|
if (remaining <= 0)
|
|
3199
3211
|
break;
|
|
3200
|
-
if (!this.materialMatches(it, req))
|
|
3212
|
+
if (!this.materialMatches(it, req, t.orderId))
|
|
3201
3213
|
continue;
|
|
3202
3214
|
/*
|
|
3203
3215
|
* 변환 중인 것은 잡지 않는다 — 어떤 공정이 이미 먹어 그 단계의 산출로 바뀔 물품이다
|
|
@@ -3221,12 +3233,13 @@ export class FlowEngine {
|
|
|
3221
3233
|
return picked;
|
|
3222
3234
|
}
|
|
3223
3235
|
/** 이 물품이 명세를 만족하나 — 품목 지목이 우선, 없으면 등급(상속을 타고 닫는다). */
|
|
3224
|
-
materialMatches(it, req) {
|
|
3236
|
+
materialMatches(it, req, orderId) {
|
|
3225
3237
|
const defId = it.definitionId ?? it.gtin;
|
|
3226
3238
|
if (req.materialDefinition)
|
|
3227
3239
|
return defId === req.materialDefinition;
|
|
3228
3240
|
if (req.materialClass) {
|
|
3229
|
-
const
|
|
3241
|
+
const basis = orderId ? this.orders.get(orderId)?.materialBasis : undefined;
|
|
3242
|
+
const def = basis !== undefined ? basis.find(b => b.material.id === defId)?.material : defId ? this.materialDefs.get(defId) : undefined;
|
|
3230
3243
|
return classClosure(def?.materialClassIds, this.classDefs.material, this.now()).has(req.materialClass);
|
|
3231
3244
|
}
|
|
3232
3245
|
return true; // 지목도 등급도 없으면 아무 자재나(명세가 그렇게 느슨하면 그대로 따른다)
|
package/dist/observed-reducer.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* - 운영 델타(task/equipment/order.status) → tasks·equipment·orders (EPCIS 로 재구성 불가한 절반)
|
|
20
20
|
* 마스터(로케이션)는 board 초기화 + applyMaster 로 갱신(마스터 동기).
|
|
21
21
|
*/
|
|
22
|
-
import { computeOee } from '@operato/ops-contract';
|
|
22
|
+
import { computeOee, isWorkShiftExecutionBasis } from '@operato/ops-contract';
|
|
23
23
|
import { OP_EVENT, capabilityOf, itemKeyOf, judgeAgainstSpec, requiredTestsFor, locationStatusOf, readBoardEquipment, readBoardLocations, readBoardAssets, effectivityAt, offCalendarAt, offCalendarReasonAt, activeShiftAt } from '@operato/ops-contract';
|
|
24
24
|
import { ILMD_ATTR, parseEpc } from '@operato/ops-contract';
|
|
25
25
|
import { expiryFromAttributes, lotFromAttributes } from '@operato/ops-contract';
|
|
@@ -345,6 +345,8 @@ export class ObservedReducer {
|
|
|
345
345
|
switch (e.eventType) {
|
|
346
346
|
case OP_EVENT.task: {
|
|
347
347
|
const d = e.data;
|
|
348
|
+
if (d.workShiftBasis !== undefined && !isWorkShiftExecutionBasis(d.workShiftBasis))
|
|
349
|
+
throw new Error('Invalid pinned work shift evidence');
|
|
348
350
|
if (this.stale(`task:${d.taskId}`, e))
|
|
349
351
|
return;
|
|
350
352
|
/* 커널이 실어 보낸 것을 그대로 담는다 — 진척·남은 시간·의도가 있어야 이 상태를 씨앗으로
|
|
@@ -387,6 +389,7 @@ export class ObservedReducer {
|
|
|
387
389
|
...(d.resources?.length ? { resources: d.resources.slice() } : {}),
|
|
388
390
|
/* 실제 자재 이동 — 인원·설비와 같은 채널로 온다(실적을 한 곳에서 읽는다).
|
|
389
391
|
말하지 않았으면 **앞의 것을 지킨다**(위 주석) — 빈 배열은 「지웠다」로 말한 것이다. */
|
|
392
|
+
...(d.workShiftBasis ? { workShiftBasis: structuredClone(d.workShiftBasis) } : prevTask?.workShiftBasis ? { workShiftBasis: structuredClone(prevTask.workShiftBasis) } : {}),
|
|
390
393
|
...(d.materialActual?.length
|
|
391
394
|
? { materialActual: d.materialActual.map(r => ({ ...r })) }
|
|
392
395
|
: keptActual?.length
|
|
@@ -509,6 +512,8 @@ export class ObservedReducer {
|
|
|
509
512
|
* **지금 상태를 바꾸지 않는다.** 어제의 구간이 오늘의 상태가 되면 안 된다.
|
|
510
513
|
*/
|
|
511
514
|
const d = e.data;
|
|
515
|
+
if (d.workShiftBasis !== undefined && !isWorkShiftExecutionBasis(d.workShiftBasis))
|
|
516
|
+
throw new Error('Invalid pinned work shift evidence');
|
|
512
517
|
if (!d?.moverId || !d?.from || !d?.to)
|
|
513
518
|
break;
|
|
514
519
|
const from = Date.parse(d.from);
|
|
@@ -723,6 +728,7 @@ export class ObservedReducer {
|
|
|
723
728
|
/* 어느 레시피로 만드는가 — 품목만으로는 「무엇으로」가 남지 않는다(같은 품목에 대체 레시피). */
|
|
724
729
|
...(d.recipeKey ? { recipeKey: d.recipeKey } : {}),
|
|
725
730
|
...(d.recipeBasis ? { recipeBasis: structuredClone(d.recipeBasis) } : {}),
|
|
731
|
+
...(d.materialBasis ? { materialBasis: structuredClone(d.materialBasis) } : {}),
|
|
726
732
|
...(d.allocated?.length ? { allocated: d.allocated.slice() } : {}),
|
|
727
733
|
...(d.bizTransaction ? { bizTransaction: d.bizTransaction } : {}),
|
|
728
734
|
/* 이행하는 계획 — 「이 계획이 얼마나 됐나」를 이 값으로 묶어 답한다(§`OrderState.operationsRequestId`). */
|
|
@@ -1321,7 +1327,7 @@ export class ObservedReducer {
|
|
|
1321
1327
|
/* 수량으로 담긴 것도 이어받는다 — 되풀어 주지 않는 축이다(원천은 담김을 다시 말해 주지 않는다). */
|
|
1322
1328
|
qtyAggregation: [...this.qtyAggregation.entries()].map(([parent, quantities]) => ({ parent, quantities })),
|
|
1323
1329
|
pendingQuantities: [...this.pendingQuantities.entries()].map(([parent, quantities]) => ({ parent, quantities })),
|
|
1324
|
-
tasks: [...this.tasks.values()].map(t => (
|
|
1330
|
+
tasks: [...this.tasks.values()].map(t => structuredClone(t)),
|
|
1325
1331
|
equipment: [...this.equipment.values()].map(m => ({ ...m })),
|
|
1326
1332
|
persons: [...this.persons.values()].map(x => ({ ...x })),
|
|
1327
1333
|
assets: [...this.assets.values()].map(x => ({ ...x })),
|
|
@@ -1349,6 +1355,10 @@ export class ObservedReducer {
|
|
|
1349
1355
|
* 모델에서 오는 판정 입력(교대·등급·시간대)은 생성자가 이미 세웠으므로 건드리지 않는다.
|
|
1350
1356
|
*/
|
|
1351
1357
|
restore(cp) {
|
|
1358
|
+
for (const task of cp?.tasks ?? []) {
|
|
1359
|
+
if (task.workShiftBasis !== undefined && !isWorkShiftExecutionBasis(task.workShiftBasis))
|
|
1360
|
+
throw new Error('Invalid pinned work shift evidence');
|
|
1361
|
+
}
|
|
1352
1362
|
this.observations = new Map((cp?.observations ?? []).map(o => [o.id, new Map((o.values ?? []).map(v => [v.propertyId, { ...v }]))]));
|
|
1353
1363
|
this.revision = cp?.revision ?? 0;
|
|
1354
1364
|
this.master = new Map((cp?.master ?? []).map(n => [n.id, { ...n }]));
|
|
@@ -1359,7 +1369,7 @@ export class ObservedReducer {
|
|
|
1359
1369
|
this.pendingParent = new Map((cp?.pendingParent ?? []).map(x => [x.child, x.parent]));
|
|
1360
1370
|
this.qtyAggregation = new Map((cp?.qtyAggregation ?? []).map(x => [x.parent, x.quantities]));
|
|
1361
1371
|
this.pendingQuantities = new Map((cp?.pendingQuantities ?? []).map(x => [x.parent, x.quantities]));
|
|
1362
|
-
this.tasks = new Map((cp?.tasks ?? []).map(t => [t.id,
|
|
1372
|
+
this.tasks = new Map((cp?.tasks ?? []).map(t => [t.id, structuredClone(t)]));
|
|
1363
1373
|
this.equipment = new Map((cp?.equipment ?? []).map(m => [m.id, { ...m }]));
|
|
1364
1374
|
this.oeeCounters = new Map((cp?.oeeCounters ?? []).map((c) => [String(c.id), { ...emptyOeeCounters(), ...c, id: undefined }]));
|
|
1365
1375
|
this.persons = new Map((cp?.persons ?? []).map(x => [x.id, { ...x }]));
|
|
@@ -1412,7 +1422,7 @@ export class ObservedReducer {
|
|
|
1412
1422
|
만료는 이벤트 없이 시각만으로 일어나므로, 델타로 받아 두면 유휴 자원이 영원히 유효하게 남는다. */
|
|
1413
1423
|
persons: [...this.persons.values()].map(p => ({ ...p, ...this.effectivityPart(p), ...this.offShiftPart(`person:${p.id}`), ...this.capabilityPart(p, `person:${p.id}`, p.personnelClassIds, this.classDefs.personnel) })),
|
|
1414
1424
|
assets: [...this.assets.values()].map(a => ({ ...a, ...this.effectivityPart(a), ...this.capabilityPart(a, `asset:${a.id}`, a.assetClassIds, this.classDefs.asset) })),
|
|
1415
|
-
tasks: [...this.tasks.values()].map(t => (
|
|
1425
|
+
tasks: [...this.tasks.values()].map(t => structuredClone(t)),
|
|
1416
1426
|
equipment: [...this.equipment.values()].map(m => ({ ...m, ...this.oeePart(m.id), ...this.effectivityPart(m), ...this.offShiftPart(`eq:${m.id}`), ...this.capabilityPart(m, `eq:${m.id}`, m.kind ? [m.kind] : [], this.classDefs.equipment) })),
|
|
1417
1427
|
orders: [...this.orders.values()].map(o => ({ ...o })),
|
|
1418
1428
|
acked: [...this.acked]
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -1846,6 +1846,43 @@ function readEpochMs(raw) {
|
|
|
1846
1846
|
return Number.isNaN(at) ? void 0 : at;
|
|
1847
1847
|
}
|
|
1848
1848
|
|
|
1849
|
+
// ../ops-contract/dist/work-shift-basis.js
|
|
1850
|
+
var instant = (value) => typeof value === "string" && /(?:Z|[+-]\d{2}:\d{2})$/.test(value) && Number.isFinite(Date.parse(value));
|
|
1851
|
+
var nonempty = (value) => typeof value === "string" && !!value.trim();
|
|
1852
|
+
var clock = (value) => typeof value === "string" && /^(?:[01]\d|2[0-3]):[0-5]\d$/.test(value);
|
|
1853
|
+
function isWorkShiftExecutionBasis(value) {
|
|
1854
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1855
|
+
return false;
|
|
1856
|
+
const b = value;
|
|
1857
|
+
if (![b.basisId, b.arrangementId, b.revisionId, b.planId, b.contentHash].every(nonempty) || !Number.isSafeInteger(b.revisionNumber) || b.revisionNumber < 1 || !instant(b.businessTime) || !nonempty(b.timezone))
|
|
1858
|
+
return false;
|
|
1859
|
+
try {
|
|
1860
|
+
new Intl.DateTimeFormat("en", { timeZone: b.timezone }).format(0);
|
|
1861
|
+
} catch {
|
|
1862
|
+
return false;
|
|
1863
|
+
}
|
|
1864
|
+
if (typeof b.workDate !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(b.workDate) || !Number.isFinite(Date.parse(b.workDate)) || new Date(b.workDate).toISOString().slice(0, 10) !== b.workDate)
|
|
1865
|
+
return false;
|
|
1866
|
+
if (!Array.isArray(b.shifts) || !b.shifts.length || b.shifts.length > 64)
|
|
1867
|
+
return false;
|
|
1868
|
+
const names = /* @__PURE__ */ new Set();
|
|
1869
|
+
for (const s of b.shifts) {
|
|
1870
|
+
if (!s || typeof s !== "object" || !nonempty(s.name) || names.has(s.name) || s.description !== void 0 && typeof s.description !== "string" || ![-1, 0, 1].includes(s.fromDate) || ![-1, 0, 1].includes(s.toDate) || !clock(s.fromTime) || !clock(s.toTime) || typeof s.daysOfWeek !== "string" || !/^(?:[0-6](?:,[0-6])*)?$/.test(s.daysOfWeek))
|
|
1871
|
+
return false;
|
|
1872
|
+
const minutes = (time) => Number(time.slice(0, 2)) * 60 + Number(time.slice(3));
|
|
1873
|
+
const duration = (s.toDate - s.fromDate) * 1440 + minutes(s.toTime) - minutes(s.fromTime);
|
|
1874
|
+
if (duration <= 0 || duration > 1440)
|
|
1875
|
+
return false;
|
|
1876
|
+
names.add(s.name);
|
|
1877
|
+
}
|
|
1878
|
+
if (b.shiftName === void 0)
|
|
1879
|
+
return b.occurrence === void 0;
|
|
1880
|
+
if (!nonempty(b.shiftName) || !names.has(b.shiftName) || !b.occurrence || !instant(b.occurrence.from) || !instant(b.occurrence.to))
|
|
1881
|
+
return false;
|
|
1882
|
+
const at = Date.parse(b.businessTime);
|
|
1883
|
+
return Date.parse(b.occurrence.from) <= at && at < Date.parse(b.occurrence.to);
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1849
1886
|
// ../ops-contract/dist/operational-ingest.js
|
|
1850
1887
|
var TASK_STATUS = ["created", "assigned", "in-progress", "completed"];
|
|
1851
1888
|
var EQUIPMENT_STATUS = ["idle", "busy", "down", "setup", "planned-stop"];
|
|
@@ -1881,7 +1918,8 @@ var SPECS = {
|
|
|
1881
1918
|
startTime: "string",
|
|
1882
1919
|
endTime: "string",
|
|
1883
1920
|
materialActual: "object[]",
|
|
1884
|
-
recordTime: "string"
|
|
1921
|
+
recordTime: "string",
|
|
1922
|
+
workShiftBasis: "object"
|
|
1885
1923
|
},
|
|
1886
1924
|
/*
|
|
1887
1925
|
* **자재 줄의 안쪽** — 계약의 `MaterialActual` 그대로다.
|
|
@@ -1897,6 +1935,11 @@ var SPECS = {
|
|
|
1897
1935
|
* 물려받으면 단위를 모르는 현장의 사실이 통째로 거절된다.
|
|
1898
1936
|
*/
|
|
1899
1937
|
shapes: {
|
|
1938
|
+
workShiftBasis: {
|
|
1939
|
+
required: ["basisId", "arrangementId", "revisionId", "revisionNumber", "planId", "contentHash", "businessTime", "timezone", "shifts", "workDate"],
|
|
1940
|
+
fields: { basisId: "string", arrangementId: "string", revisionId: "string", revisionNumber: "number", planId: "string", contentHash: "string", businessTime: "string", timezone: "string", workDate: "string" }
|
|
1941
|
+
// Full nested content and selected occurrence are checked by isWorkShiftExecutionBasis below.
|
|
1942
|
+
},
|
|
1900
1943
|
materialActual: {
|
|
1901
1944
|
required: ["definitionId", "use"],
|
|
1902
1945
|
fields: { definitionId: "string", lotId: "string", quantity: "number", uom: "string" },
|
|
@@ -2023,13 +2066,19 @@ var SPECS = {
|
|
|
2023
2066
|
* 새 어휘가 아니다 — 계약이 `recipeKey` 를 네 자리에서 이미 쓴다. 오더 관측에만 자리가 없었다.
|
|
2024
2067
|
*/
|
|
2025
2068
|
recipeKey: "string",
|
|
2026
|
-
recipeBasis: "object"
|
|
2069
|
+
recipeBasis: "object",
|
|
2070
|
+
materialBasis: "object[]"
|
|
2027
2071
|
},
|
|
2028
2072
|
/*
|
|
2029
2073
|
* 확보분의 안쪽을 본다 — 열쇠와 수 둘이다(§`AllocatedQuantity`). 안 보면 틀린 이름이 조용히
|
|
2030
2074
|
* 지나가고(`epcs`·`quantity` 따위), 미러는 잡힌 수를 0 으로 읽는다.
|
|
2031
2075
|
*/
|
|
2032
2076
|
shapes: {
|
|
2077
|
+
materialBasis: {
|
|
2078
|
+
required: ["basisId", "materialId", "revisionId", "revisionNumber", "planId", "contentHash", "businessTime", "material"],
|
|
2079
|
+
fields: { basisId: "string", materialId: "string", revisionId: "string", revisionNumber: "number", planId: "string", contentHash: "string", businessTime: "string" }
|
|
2080
|
+
// Recursive material properties and reference lists are checked by isMaterialExecutionBasis.
|
|
2081
|
+
},
|
|
2033
2082
|
recipeBasis: {
|
|
2034
2083
|
required: ["basisId", "recipeId", "revisionId", "planId", "contentHash", "businessTime", "recipe"],
|
|
2035
2084
|
fields: { basisId: "string", recipeId: "string", revisionId: "string", planId: "string", contentHash: "string", businessTime: "string" }
|
|
@@ -2115,7 +2164,14 @@ var SPECS = {
|
|
|
2115
2164
|
/* 사람이 정한 사유 — 없을 수 있다(적지 않은 것과 사유가 없는 것은 다르므로 지어내지 않는다). */
|
|
2116
2165
|
reasonCode: "string",
|
|
2117
2166
|
decidedBy: "string",
|
|
2118
|
-
recordTime: "string"
|
|
2167
|
+
recordTime: "string",
|
|
2168
|
+
workShiftBasis: "object"
|
|
2169
|
+
},
|
|
2170
|
+
shapes: {
|
|
2171
|
+
workShiftBasis: {
|
|
2172
|
+
required: ["basisId", "arrangementId", "revisionId", "revisionNumber", "planId", "contentHash", "businessTime", "timezone", "shifts", "workDate"],
|
|
2173
|
+
fields: { basisId: "string", arrangementId: "string", revisionId: "string", revisionNumber: "number", planId: "string", contentHash: "string", businessTime: "string", timezone: "string", workDate: "string" }
|
|
2174
|
+
}
|
|
2119
2175
|
},
|
|
2120
2176
|
enums: { status: EQUIPMENT_STATUS }
|
|
2121
2177
|
},
|
|
@@ -2898,6 +2954,7 @@ var ObservedReducer = class {
|
|
|
2898
2954
|
switch (e.eventType) {
|
|
2899
2955
|
case OP_EVENT.task: {
|
|
2900
2956
|
const d = e.data;
|
|
2957
|
+
if (d.workShiftBasis !== void 0 && !isWorkShiftExecutionBasis(d.workShiftBasis)) throw new Error("Invalid pinned work shift evidence");
|
|
2901
2958
|
if (this.stale(`task:${d.taskId}`, e)) return;
|
|
2902
2959
|
this.touchLocation(d.fromNode);
|
|
2903
2960
|
this.touchLocation(d.toNode);
|
|
@@ -2922,6 +2979,7 @@ var ObservedReducer = class {
|
|
|
2922
2979
|
...d.resources?.length ? { resources: d.resources.slice() } : {},
|
|
2923
2980
|
/* 실제 자재 이동 — 인원·설비와 같은 채널로 온다(실적을 한 곳에서 읽는다).
|
|
2924
2981
|
말하지 않았으면 **앞의 것을 지킨다**(위 주석) — 빈 배열은 「지웠다」로 말한 것이다. */
|
|
2982
|
+
...d.workShiftBasis ? { workShiftBasis: structuredClone(d.workShiftBasis) } : prevTask?.workShiftBasis ? { workShiftBasis: structuredClone(prevTask.workShiftBasis) } : {},
|
|
2925
2983
|
...d.materialActual?.length ? { materialActual: d.materialActual.map((r) => ({ ...r })) } : keptActual?.length ? { materialActual: keptActual.map((r) => ({ ...r })) } : {},
|
|
2926
2984
|
...d.priority !== void 0 ? { priority: d.priority } : {},
|
|
2927
2985
|
...d.startTime ? { startTime: d.startTime } : {},
|
|
@@ -3003,6 +3061,7 @@ var ObservedReducer = class {
|
|
|
3003
3061
|
}
|
|
3004
3062
|
case OP_EVENT.equipmentPeriod: {
|
|
3005
3063
|
const d = e.data;
|
|
3064
|
+
if (d.workShiftBasis !== void 0 && !isWorkShiftExecutionBasis(d.workShiftBasis)) throw new Error("Invalid pinned work shift evidence");
|
|
3006
3065
|
if (!d?.moverId || !d?.from || !d?.to) break;
|
|
3007
3066
|
const from = Date.parse(d.from);
|
|
3008
3067
|
const to = Date.parse(d.to);
|
|
@@ -3112,6 +3171,7 @@ var ObservedReducer = class {
|
|
|
3112
3171
|
/* 어느 레시피로 만드는가 — 품목만으로는 「무엇으로」가 남지 않는다(같은 품목에 대체 레시피). */
|
|
3113
3172
|
...d.recipeKey ? { recipeKey: d.recipeKey } : {},
|
|
3114
3173
|
...d.recipeBasis ? { recipeBasis: structuredClone(d.recipeBasis) } : {},
|
|
3174
|
+
...d.materialBasis ? { materialBasis: structuredClone(d.materialBasis) } : {},
|
|
3115
3175
|
...d.allocated?.length ? { allocated: d.allocated.slice() } : {},
|
|
3116
3176
|
...d.bizTransaction ? { bizTransaction: d.bizTransaction } : {},
|
|
3117
3177
|
/* 이행하는 계획 — 「이 계획이 얼마나 됐나」를 이 값으로 묶어 답한다(§`OrderState.operationsRequestId`). */
|
|
@@ -3536,7 +3596,7 @@ var ObservedReducer = class {
|
|
|
3536
3596
|
/* 수량으로 담긴 것도 이어받는다 — 되풀어 주지 않는 축이다(원천은 담김을 다시 말해 주지 않는다). */
|
|
3537
3597
|
qtyAggregation: [...this.qtyAggregation.entries()].map(([parent, quantities]) => ({ parent, quantities })),
|
|
3538
3598
|
pendingQuantities: [...this.pendingQuantities.entries()].map(([parent, quantities]) => ({ parent, quantities })),
|
|
3539
|
-
tasks: [...this.tasks.values()].map((t) => (
|
|
3599
|
+
tasks: [...this.tasks.values()].map((t) => structuredClone(t)),
|
|
3540
3600
|
equipment: [...this.equipment.values()].map((m) => ({ ...m })),
|
|
3541
3601
|
persons: [...this.persons.values()].map((x) => ({ ...x })),
|
|
3542
3602
|
assets: [...this.assets.values()].map((x) => ({ ...x })),
|
|
@@ -3564,6 +3624,9 @@ var ObservedReducer = class {
|
|
|
3564
3624
|
* 모델에서 오는 판정 입력(교대·등급·시간대)은 생성자가 이미 세웠으므로 건드리지 않는다.
|
|
3565
3625
|
*/
|
|
3566
3626
|
restore(cp) {
|
|
3627
|
+
for (const task of cp?.tasks ?? []) {
|
|
3628
|
+
if (task.workShiftBasis !== void 0 && !isWorkShiftExecutionBasis(task.workShiftBasis)) throw new Error("Invalid pinned work shift evidence");
|
|
3629
|
+
}
|
|
3567
3630
|
this.observations = new Map(
|
|
3568
3631
|
(cp?.observations ?? []).map((o) => [o.id, new Map((o.values ?? []).map((v) => [v.propertyId, { ...v }]))])
|
|
3569
3632
|
);
|
|
@@ -3576,7 +3639,7 @@ var ObservedReducer = class {
|
|
|
3576
3639
|
this.pendingParent = new Map((cp?.pendingParent ?? []).map((x) => [x.child, x.parent]));
|
|
3577
3640
|
this.qtyAggregation = new Map((cp?.qtyAggregation ?? []).map((x) => [x.parent, x.quantities]));
|
|
3578
3641
|
this.pendingQuantities = new Map((cp?.pendingQuantities ?? []).map((x) => [x.parent, x.quantities]));
|
|
3579
|
-
this.tasks = new Map((cp?.tasks ?? []).map((t) => [t.id,
|
|
3642
|
+
this.tasks = new Map((cp?.tasks ?? []).map((t) => [t.id, structuredClone(t)]));
|
|
3580
3643
|
this.equipment = new Map((cp?.equipment ?? []).map((m) => [m.id, { ...m }]));
|
|
3581
3644
|
this.oeeCounters = new Map(
|
|
3582
3645
|
(cp?.oeeCounters ?? []).map((c) => [String(c.id), { ...emptyOeeCounters(), ...c, id: void 0 }])
|
|
@@ -3630,7 +3693,7 @@ var ObservedReducer = class {
|
|
|
3630
3693
|
만료는 이벤트 없이 시각만으로 일어나므로, 델타로 받아 두면 유휴 자원이 영원히 유효하게 남는다. */
|
|
3631
3694
|
persons: [...this.persons.values()].map((p) => ({ ...p, ...this.effectivityPart(p), ...this.offShiftPart(`person:${p.id}`), ...this.capabilityPart(p, `person:${p.id}`, p.personnelClassIds, this.classDefs.personnel) })),
|
|
3632
3695
|
assets: [...this.assets.values()].map((a) => ({ ...a, ...this.effectivityPart(a), ...this.capabilityPart(a, `asset:${a.id}`, a.assetClassIds, this.classDefs.asset) })),
|
|
3633
|
-
tasks: [...this.tasks.values()].map((t) => (
|
|
3696
|
+
tasks: [...this.tasks.values()].map((t) => structuredClone(t)),
|
|
3634
3697
|
equipment: [...this.equipment.values()].map((m) => ({ ...m, ...this.oeePart(m.id), ...this.effectivityPart(m), ...this.offShiftPart(`eq:${m.id}`), ...this.capabilityPart(m, `eq:${m.id}`, m.kind ? [m.kind] : [], this.classDefs.equipment) })),
|
|
3635
3698
|
orders: [...this.orders.values()].map((o) => ({ ...o })),
|
|
3636
3699
|
acked: [...this.acked]
|
|
@@ -4527,6 +4590,9 @@ var FlowEngine = class {
|
|
|
4527
4590
|
return { kept, dropped };
|
|
4528
4591
|
}
|
|
4529
4592
|
hydrateObserved(snap, orders = [], purpose = "seed") {
|
|
4593
|
+
for (const task of snap.tasks ?? []) {
|
|
4594
|
+
if (task.workShiftBasis !== void 0 && !isWorkShiftExecutionBasis(task.workShiftBasis)) throw new Error("Invalid pinned work shift evidence");
|
|
4595
|
+
}
|
|
4530
4596
|
for (const id of snap.acked ?? []) this._acked.add(id);
|
|
4531
4597
|
for (const e of snap.attentionSince ?? []) if (e?.id && e.since) this._attentionSince.set(e.id, e.since);
|
|
4532
4598
|
for (const n of snap.locations) {
|
|
@@ -4688,6 +4754,7 @@ var FlowEngine = class {
|
|
|
4688
4754
|
...o.gtin ? { gtin: o.gtin } : {},
|
|
4689
4755
|
...o.recipeKey ? { recipeKey: o.recipeKey } : {},
|
|
4690
4756
|
...o.recipeBasis ? { recipeBasis: structuredClone(o.recipeBasis) } : {},
|
|
4757
|
+
...o.materialBasis ? { materialBasis: structuredClone(o.materialBasis) } : {},
|
|
4691
4758
|
...o.bizTransaction ? { bizTransaction: o.bizTransaction } : {},
|
|
4692
4759
|
...o.dockDoor ? { dockDoor: o.dockDoor } : {},
|
|
4693
4760
|
...o.windowStartMs !== void 0 ? { windowStartMs: o.windowStartMs } : {}
|
|
@@ -4713,6 +4780,7 @@ var FlowEngine = class {
|
|
|
4713
4780
|
남은 공정·소요가 다른 레시피 기준으로 계산된다(같은 품목에 대체 레시피가 있다). */
|
|
4714
4781
|
...o.recipeKey ? { recipeKey: o.recipeKey } : {},
|
|
4715
4782
|
...o.recipeBasis ? { recipeBasis: structuredClone(o.recipeBasis) } : {},
|
|
4783
|
+
...o.materialBasis ? { materialBasis: structuredClone(o.materialBasis) } : {},
|
|
4716
4784
|
bizTransaction: o.bizTransaction ?? "",
|
|
4717
4785
|
allocated: resolved.kept,
|
|
4718
4786
|
picked: [],
|
|
@@ -4766,6 +4834,7 @@ var FlowEngine = class {
|
|
|
4766
4834
|
status: purpose === "observe" ? t.status : known && t.status === "in-progress" ? "in-progress" : "created",
|
|
4767
4835
|
/* 이미 일어난 자재 이동은 **씨앗에도 남는다** — 잃으면 실적이 재기동마다 지워진다. */
|
|
4768
4836
|
...t.materialActual?.length ? { materialActual: t.materialActual.map((r) => ({ ...r })) } : {},
|
|
4837
|
+
...t.workShiftBasis ? { workShiftBasis: structuredClone(t.workShiftBasis) } : {},
|
|
4769
4838
|
itemEpc: t.itemRefs?.[0] ?? "",
|
|
4770
4839
|
fromNode: t.fromNode ?? "",
|
|
4771
4840
|
toNode: t.toNode ?? "",
|
|
@@ -5123,6 +5192,7 @@ var FlowEngine = class {
|
|
|
5123
5192
|
...t.assets?.length ? { assets: t.assets.slice() } : {},
|
|
5124
5193
|
...t.resources?.length ? { resources: t.resources.slice() } : {},
|
|
5125
5194
|
...t.materialActual?.length ? { materialActual: t.materialActual.map((r) => ({ ...r })) } : {},
|
|
5195
|
+
...t.workShiftBasis ? { workShiftBasis: structuredClone(t.workShiftBasis) } : {},
|
|
5126
5196
|
...t.priority !== void 0 ? { priority: t.priority } : {},
|
|
5127
5197
|
...t.startTime ? { startTime: t.startTime } : {},
|
|
5128
5198
|
...t.endTime ? { endTime: t.endTime } : {}
|
|
@@ -5147,6 +5217,7 @@ var FlowEngine = class {
|
|
|
5147
5217
|
/* **어느 레시피로 만드나** — 품목만으로는 「무엇으로」가 안 남는다(같은 품목의 대체 레시피가 있다). */
|
|
5148
5218
|
...o.recipeKey ? { recipeKey: o.recipeKey } : {},
|
|
5149
5219
|
...o.recipeBasis ? { recipeBasis: structuredClone(o.recipeBasis) } : {},
|
|
5220
|
+
...o.materialBasis ? { materialBasis: structuredClone(o.materialBasis) } : {},
|
|
5150
5221
|
/* 씨앗이 다 심지 못했다는 사실 — 조용히 자르지 않는다(그 오더의 답은 부족한 씨앗 위에 있다). */
|
|
5151
5222
|
...o.seedIncomplete ? { seedIncomplete: true } : {},
|
|
5152
5223
|
/* 왜 대기하는지 — 없으면 화면은 「running」만 보여 주고 사람은 원인을 찾을 자리가 없다. */
|
|
@@ -6141,6 +6212,7 @@ var FlowEngine = class {
|
|
|
6141
6212
|
...t.resources?.length ? { resources: t.resources.slice() } : {},
|
|
6142
6213
|
/* 실제 자재 이동 — 인원·설비와 같은 채널(실적을 한 곳에서 읽는다). */
|
|
6143
6214
|
...t.materialActual?.length ? { materialActual: t.materialActual.map((r) => ({ ...r })) } : {},
|
|
6215
|
+
...t.workShiftBasis ? { workShiftBasis: structuredClone(t.workShiftBasis) } : {},
|
|
6144
6216
|
...t.durationMs ? { durationMs: t.durationMs } : {},
|
|
6145
6217
|
/* 품질 판정이 있었으면 **그 자리에서** 실어 보낸다 — 없으면 필드를 만들지 않는다(모름≠양품). */
|
|
6146
6218
|
...t.outcome ? { outcome: t.outcome } : {},
|
|
@@ -6182,6 +6254,7 @@ var FlowEngine = class {
|
|
|
6182
6254
|
레시피가 있다). 시뮬만 아는 사실이 남으면 두 스냅샷을 같은 규칙으로 읽을 수 없다. */
|
|
6183
6255
|
...o.recipeKey ? { recipeKey: o.recipeKey } : {},
|
|
6184
6256
|
...o.recipeBasis ? { recipeBasis: structuredClone(o.recipeBasis) } : {},
|
|
6257
|
+
...o.materialBasis ? { materialBasis: structuredClone(o.materialBasis) } : {},
|
|
6185
6258
|
...o.allocated?.length ? { allocated: o.allocated.slice() } : {},
|
|
6186
6259
|
...o.bizTransaction ? { bizTransaction: o.bizTransaction } : {},
|
|
6187
6260
|
/* 약속해 둔 자리·시각창 — 이것이 빠져서 야드 트윈이 재기동마다 첫 틱에 죽었다(§dockDoor). */
|
|
@@ -6356,7 +6429,7 @@ var FlowEngine = class {
|
|
|
6356
6429
|
if (!remaining) continue;
|
|
6357
6430
|
for (const it of this.items.at(at)) {
|
|
6358
6431
|
if (remaining <= 0) break;
|
|
6359
|
-
if (!this.materialMatches(it, req)) continue;
|
|
6432
|
+
if (!this.materialMatches(it, req, t.orderId)) continue;
|
|
6360
6433
|
if (it.disposition === DISP.in_progress) continue;
|
|
6361
6434
|
const already = takenSoFar.get(it.epc) ?? 0;
|
|
6362
6435
|
const avail = Math.max(0, (it.qty ?? 1) - already);
|
|
@@ -6371,11 +6444,12 @@ var FlowEngine = class {
|
|
|
6371
6444
|
return picked;
|
|
6372
6445
|
}
|
|
6373
6446
|
/** 이 물품이 명세를 만족하나 — 품목 지목이 우선, 없으면 등급(상속을 타고 닫는다). */
|
|
6374
|
-
materialMatches(it, req) {
|
|
6447
|
+
materialMatches(it, req, orderId) {
|
|
6375
6448
|
const defId = it.definitionId ?? it.gtin;
|
|
6376
6449
|
if (req.materialDefinition) return defId === req.materialDefinition;
|
|
6377
6450
|
if (req.materialClass) {
|
|
6378
|
-
const
|
|
6451
|
+
const basis = orderId ? this.orders.get(orderId)?.materialBasis : void 0;
|
|
6452
|
+
const def = basis !== void 0 ? basis.find((b) => b.material.id === defId)?.material : defId ? this.materialDefs.get(defId) : void 0;
|
|
6379
6453
|
return classClosure(def?.materialClassIds, this.classDefs.material, this.now()).has(req.materialClass);
|
|
6380
6454
|
}
|
|
6381
6455
|
return true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@operato/twin-kernel",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.23",
|
|
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.33"
|
|
32
32
|
}
|
|
33
33
|
}
|