@operato/twin-kernel 0.10.1 → 0.11.0
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 -7
- package/dist/flow-engine.js +18 -6
- package/dist/job-response.d.ts +2 -11
- package/dist/mes-kernel.d.ts +12 -0
- package/dist/mes-kernel.js +47 -2
- package/dist-cjs/index.cjs +63 -7
- package/package.json +2 -2
package/dist/flow-engine.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { 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 } from '@operato/ops-contract';
|
|
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 } from '@operato/ops-contract';
|
|
2
2
|
import type { VocabularyElement } from '@operato/ops-contract';
|
|
3
3
|
import type { ReducerCheckpoint } from './observed-reducer.ts';
|
|
4
4
|
import type { EpcisEvent, BizTransactionElement } from '@operato/ops-contract';
|
|
@@ -159,12 +159,7 @@ export interface FlowTask {
|
|
|
159
159
|
* 명세(계획)가 아니라 **일어난 일**이다. 투입 인원·설비가 이미 작업 델타에 타고 있으니 자재도 같은
|
|
160
160
|
* 채널에 실어야 실적을 **한 곳에서** 읽는다(EPCIS 이벤트에서 다시 계산하려면 작업과 잇는 끈이 없다).
|
|
161
161
|
*/
|
|
162
|
-
materialActual?:
|
|
163
|
-
definitionId: string;
|
|
164
|
-
use: 'consumed' | 'produced';
|
|
165
|
-
quantity: number;
|
|
166
|
-
uom?: string;
|
|
167
|
-
}[];
|
|
162
|
+
materialActual?: MaterialActual[];
|
|
168
163
|
itemEpc: string;
|
|
169
164
|
fromNode: string;
|
|
170
165
|
toNode: string;
|
package/dist/flow-engine.js
CHANGED
|
@@ -3237,7 +3237,8 @@ export class FlowEngine {
|
|
|
3237
3237
|
if (n)
|
|
3238
3238
|
n.occupancy++;
|
|
3239
3239
|
}
|
|
3240
|
-
|
|
3240
|
+
/* 산출 로트의 식별자 — 비직렬 로트는 클래스 식별자가 곧 로트의 이름이다. */
|
|
3241
|
+
this.recordMaterialActual(t, gtin, 'produced', qty, spec.uom, gtin);
|
|
3241
3242
|
this.emit(objectEvent({
|
|
3242
3243
|
eventTime: this.now(), action: 'ADD', bizStep: CBV_BIZSTEP.commissioning, disposition: DISP.in_progress,
|
|
3243
3244
|
epcList: [], quantityList: [{ epcClass: gtin, quantity: total, ...(spec.uom ? { uom: spec.uom } : {}) }],
|
|
@@ -3327,15 +3328,26 @@ export class FlowEngine {
|
|
|
3327
3328
|
* 실제 자재 이동을 작업에 적어 둔다 — ISA-95 `JobResponse.MaterialActual`.
|
|
3328
3329
|
* 같은 품목·같은 쓰임은 **한 줄로 합친다**(줄을 늘리면 실적을 세는 쪽이 중복을 걷어내야 한다).
|
|
3329
3330
|
*/
|
|
3330
|
-
recordMaterialActual(t, definitionId, use, quantity, uom
|
|
3331
|
+
recordMaterialActual(t, definitionId, use, quantity, uom,
|
|
3332
|
+
/**
|
|
3333
|
+
* 어느 로트인가 — 표준 `MaterialLotID`. **식별자**이고 로트 번호 문자열이 아니다.
|
|
3334
|
+
*
|
|
3335
|
+
* 이 값이 있어야 「이 로트로 무엇을 만들었나」가 이어진다. 불합격 로트의 이동 제한과 계보가 그 위에
|
|
3336
|
+
* 선다. 모르면 넣지 않는다 — 지어내면 다른 로트의 실적이 된다.
|
|
3337
|
+
*/
|
|
3338
|
+
lotId) {
|
|
3331
3339
|
if (!definitionId || !(quantity > 0))
|
|
3332
3340
|
return;
|
|
3333
3341
|
const rows = (t.materialActual ??= []);
|
|
3334
|
-
|
|
3342
|
+
/*
|
|
3343
|
+
* **로트가 다르면 다른 줄이다.** 합치면 어느 로트를 얼마나 썼는지 사라지고, 그것이 이 필드를 만든
|
|
3344
|
+
* 이유 자체다. 같은 품목·같은 쓰임·같은 로트만 한 줄로 합친다.
|
|
3345
|
+
*/
|
|
3346
|
+
const hit = rows.find(r => r.definitionId === definitionId && r.use === use && r.uom === uom && r.lotId === lotId);
|
|
3335
3347
|
if (hit)
|
|
3336
3348
|
hit.quantity += quantity;
|
|
3337
3349
|
else
|
|
3338
|
-
rows.push({ definitionId, use, quantity, ...(uom ? { uom } : {}) });
|
|
3350
|
+
rows.push({ definitionId, use, quantity, ...(uom ? { uom } : {}), ...(lotId ? { lotId } : {}) });
|
|
3339
3351
|
}
|
|
3340
3352
|
/** 확보한 자재를 **작업 시작 시점에 소비**한다 — 수량이 0 이 되면 물품 자체가 사라진다. */
|
|
3341
3353
|
consumeMaterials(t, taken) {
|
|
@@ -3352,7 +3364,7 @@ export class FlowEngine {
|
|
|
3352
3364
|
const left = (it.qty ?? 1) - take;
|
|
3353
3365
|
if (left > 0) {
|
|
3354
3366
|
it.qty = left;
|
|
3355
|
-
this.recordMaterialActual(t, it.definitionId ?? it.gtin, 'consumed', take, it.uom);
|
|
3367
|
+
this.recordMaterialActual(t, it.definitionId ?? it.gtin, 'consumed', take, it.uom, it.epc);
|
|
3356
3368
|
/* 남은 수량을 그대로 알린다 — 관측 경로가 그 자리의 사실을 갱신한다(§quantityList). */
|
|
3357
3369
|
this.emit(objectEvent({
|
|
3358
3370
|
eventTime: this.now(), action: 'OBSERVE', bizStep: CBV_BIZSTEP.consuming, disposition: DISP.in_progress,
|
|
@@ -3361,7 +3373,7 @@ export class FlowEngine {
|
|
|
3361
3373
|
}));
|
|
3362
3374
|
continue;
|
|
3363
3375
|
}
|
|
3364
|
-
this.recordMaterialActual(t, it.definitionId ?? it.gtin, 'consumed', take, it.uom);
|
|
3376
|
+
this.recordMaterialActual(t, it.definitionId ?? it.gtin, 'consumed', take, it.uom, it.epc);
|
|
3365
3377
|
whole.push(epc);
|
|
3366
3378
|
}
|
|
3367
3379
|
if (!whole.length)
|
package/dist/job-response.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ISOTime } from '@operato/ops-contract';
|
|
1
|
+
import type { ISOTime, MaterialActual } from '@operato/ops-contract';
|
|
2
2
|
import { type TaskDeltaRow } from './task-fold.ts';
|
|
3
3
|
/** 표준 `JobState` — 우리 작업 상태를 표준 낱말로 옮긴다. */
|
|
4
4
|
export type JobState = 'ready' | 'running' | 'completed';
|
|
@@ -9,15 +9,7 @@ export interface ResourceActual {
|
|
|
9
9
|
/** 표준 `Quantity`. 우리는 개체를 지목하므로 언제나 1이다(등급 단위 집계는 소비처의 일). */
|
|
10
10
|
quantity: number;
|
|
11
11
|
}
|
|
12
|
-
|
|
13
|
-
export interface MaterialActual {
|
|
14
|
-
/** 표준 `MaterialDefinitionID`. */
|
|
15
|
-
definitionId: string;
|
|
16
|
-
/** 표준 `MaterialUse`. */
|
|
17
|
-
use: 'consumed' | 'produced';
|
|
18
|
-
quantity: number;
|
|
19
|
-
uom?: string;
|
|
20
|
-
}
|
|
12
|
+
export type { MaterialActual };
|
|
21
13
|
export interface JobResponse {
|
|
22
14
|
/** 표준 `ID` — 작업 식별자. */
|
|
23
15
|
id: string;
|
|
@@ -56,4 +48,3 @@ type Row = TaskDeltaRow;
|
|
|
56
48
|
* 자원·자재 실적은 전이가 실어 온 값에서 모은다. **마지막에 본 값**이 사실이다(재전송이면 나중 것).
|
|
57
49
|
*/
|
|
58
50
|
export declare function foldJobResponses(rows: readonly Row[]): JobResponse[];
|
|
59
|
-
export {};
|
package/dist/mes-kernel.d.ts
CHANGED
|
@@ -37,6 +37,18 @@ export declare class MesKernel extends FlowEngine {
|
|
|
37
37
|
*
|
|
38
38
|
* 런타임에 조용히 어긋나는 것보다 기동이 실패하는 편이 낫다.
|
|
39
39
|
*/
|
|
40
|
+
/**
|
|
41
|
+
* 라우트의 **절차 계층**이 단계와 어긋나지 않는가 (ISA-88).
|
|
42
|
+
*
|
|
43
|
+
* ── 왜 이제야 부르나 (2026-08-31) ─────────────────────────────────────────
|
|
44
|
+
* 계약이 `procedureViolations` 를 갖고 있었는데 **아무도 부르지 않았다** — 계약의 자기 시험만
|
|
45
|
+
* 불렀다. 그래서 절차를 선언해도 어긋난 것이 그냥 지나갔고, 화면은 잘못된 트리를 오류 없이 보였다.
|
|
46
|
+
*
|
|
47
|
+
* 선언을 읽는 곳이 있는지 보는 하네스가 이것을 잡았다(§`declaration-is-read.test.ts`).
|
|
48
|
+
*
|
|
49
|
+
* 판정은 계약이 한다 — 여기서 다시 만들면 규칙이 두 벌이 되고, 화면도 같은 판정을 해야 한다.
|
|
50
|
+
*/
|
|
51
|
+
private assertRouteProcedures;
|
|
40
52
|
private assertRecipeOperationTags;
|
|
41
53
|
/**
|
|
42
54
|
* **레시피 모드에서는 MES 가 산출을 소유한다** — 코어는 비켜선다(§`producesOwnOutputs`).
|
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 } from '@operato/ops-contract';
|
|
11
|
+
import { identityGroundingOf, procedureViolations } from '@operato/ops-contract';
|
|
12
12
|
import { firstFitPolicy } from "./allocation-policy.js";
|
|
13
13
|
import { FlowEngine } from "./flow-engine.js";
|
|
14
14
|
import { DISP, objectEvent, transactionEvent, gdtiUri, sgtinClass, bizTransactionUri } from '@operato/ops-contract';
|
|
@@ -76,6 +76,7 @@ export class MesKernel extends FlowEngine {
|
|
|
76
76
|
this.assertNoDoubleProduction(productionSpec.definition.operations);
|
|
77
77
|
}
|
|
78
78
|
this.assertRecipeOperationTags();
|
|
79
|
+
this.assertRouteProcedures();
|
|
79
80
|
}
|
|
80
81
|
/**
|
|
81
82
|
* **산출을 두 곳에서 만들지 않는다** — 기동 때 막는다.
|
|
@@ -131,6 +132,26 @@ export class MesKernel extends FlowEngine {
|
|
|
131
132
|
*
|
|
132
133
|
* 런타임에 조용히 어긋나는 것보다 기동이 실패하는 편이 낫다.
|
|
133
134
|
*/
|
|
135
|
+
/**
|
|
136
|
+
* 라우트의 **절차 계층**이 단계와 어긋나지 않는가 (ISA-88).
|
|
137
|
+
*
|
|
138
|
+
* ── 왜 이제야 부르나 (2026-08-31) ─────────────────────────────────────────
|
|
139
|
+
* 계약이 `procedureViolations` 를 갖고 있었는데 **아무도 부르지 않았다** — 계약의 자기 시험만
|
|
140
|
+
* 불렀다. 그래서 절차를 선언해도 어긋난 것이 그냥 지나갔고, 화면은 잘못된 트리를 오류 없이 보였다.
|
|
141
|
+
*
|
|
142
|
+
* 선언을 읽는 곳이 있는지 보는 하네스가 이것을 잡았다(§`declaration-is-read.test.ts`).
|
|
143
|
+
*
|
|
144
|
+
* 판정은 계약이 한다 — 여기서 다시 만들면 규칙이 두 벌이 되고, 화면도 같은 판정을 해야 한다.
|
|
145
|
+
*/
|
|
146
|
+
assertRouteProcedures() {
|
|
147
|
+
const routes = this.productionSpec?.definition?.routes ?? [];
|
|
148
|
+
const bad = routes.flatMap(r => procedureViolations(r).map(v => `route '${r.key}': ${v}`));
|
|
149
|
+
if (bad.length) {
|
|
150
|
+
throw new Error(`declared procedure hierarchy does not match the route steps:\n - ${bad.join('\n - ')}\n` +
|
|
151
|
+
'The steps are the source of truth for execution order; the hierarchy only names them. ' +
|
|
152
|
+
'A mismatch would show a tree on screen that the twin never executes.');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
134
155
|
assertRecipeOperationTags() {
|
|
135
156
|
const def = this.productionSpec?.definition;
|
|
136
157
|
if (!def?.recipes?.length)
|
|
@@ -458,9 +479,33 @@ export class MesKernel extends FlowEngine {
|
|
|
458
479
|
throw new Error(`recipe '${key}' is not declared. Declared: [${(d.recipes ?? []).map(r => r.key).join(', ') || '(none)'}]. ` +
|
|
459
480
|
`The kernel does not substitute another recipe — that would make this order produce a different item.`);
|
|
460
481
|
}
|
|
482
|
+
/*
|
|
483
|
+
* **지목한 것은 유효 기간으로 거르지 않는다** (2026-08-31).
|
|
484
|
+
*
|
|
485
|
+
* 지난 실적을 뒤늦게 올릴 때 **그때 쓰던 레시피**를 가리켜야 한다. 그 레시피는 이미 끝난 것일 수
|
|
486
|
+
* 있고, 그것이 사실이다. 「지금 무엇을 쓸 수 있나」와 「그때 무엇을 썼나」는 다른 물음이다.
|
|
487
|
+
*/
|
|
461
488
|
return found;
|
|
462
489
|
}
|
|
463
|
-
|
|
490
|
+
/*
|
|
491
|
+
* **지목이 없으면 지금 유효한 것 중에서 고른다** (2026-08-31).
|
|
492
|
+
*
|
|
493
|
+
* 예전에는 선언 순서의 첫 번째를 그대로 썼다. 그래서 첫 레시피가 개정으로 끝난 뒤에도 그것으로
|
|
494
|
+
* 만들었고, 그 산출이 무엇인지 계보에서 잃었다. 유효 기간이 계약에 열린 것은 이 판정을 하기 위한
|
|
495
|
+
* 것인데 읽는 쪽이 없었다.
|
|
496
|
+
*
|
|
497
|
+
* 여럿이 유효하면 첫 번째다 — 그 모호함은 예전부터 있었고 여기서 정할 일이 아니다(어느 것으로
|
|
498
|
+
* 만들지는 오더의 `recipeKey` 가 말한다). 하나도 유효하지 않으면 **던진다**: 끝난 레시피로 만드는
|
|
499
|
+
* 것이 멈추는 것보다 나쁘다.
|
|
500
|
+
*/
|
|
501
|
+
const usable = (d.recipes ?? []).filter(r => !this.effectivityOf(r));
|
|
502
|
+
if (!usable.length) {
|
|
503
|
+
const when = (d.recipes ?? []).map(r => `${r.key}(${this.effectivityOf(r)})`).join(', ');
|
|
504
|
+
throw new Error(`no declared recipe is in effect now. Declared: [${when || '(none)'}]. ` +
|
|
505
|
+
`The kernel does not fall back to a retired recipe — that would make this order produce an item whose ` +
|
|
506
|
+
`bill of materials no longer holds. Name a recipe on the order, or extend the effective period.`);
|
|
507
|
+
}
|
|
508
|
+
return usable[0];
|
|
464
509
|
}
|
|
465
510
|
/** 자재 키 → 구체 gtin 클래스(idpat). 구체 식별은 바인딩+prefix 로 인스턴스가 주입. */
|
|
466
511
|
/**
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -1354,6 +1354,32 @@ var OP_PARAM = {
|
|
|
1354
1354
|
* 현재 커널은 작업에 붙는 셋업으로 다루므로 모수로 받는다. */
|
|
1355
1355
|
setupDuration: "setupDuration"
|
|
1356
1356
|
};
|
|
1357
|
+
function procedureViolations(route) {
|
|
1358
|
+
const elements = route.procedure ?? [];
|
|
1359
|
+
if (!elements.length)
|
|
1360
|
+
return [];
|
|
1361
|
+
const errors = [];
|
|
1362
|
+
const byKey = new Map(elements.map((e) => [e.key, e]));
|
|
1363
|
+
const hasChild = new Set(elements.map((e) => e.parent).filter((k) => !!k));
|
|
1364
|
+
const stepSet = new Set(route.steps ?? []);
|
|
1365
|
+
const seenStep = /* @__PURE__ */ new Map();
|
|
1366
|
+
for (const n of elements) {
|
|
1367
|
+
if (n.parent && !byKey.has(n.parent))
|
|
1368
|
+
errors.push(`${n.key}: \uC0C1\uC704 '${n.parent}' \uAC00 \uC774 \uC808\uCC28\uC5D0 \uC5C6\uB2E4`);
|
|
1369
|
+
if (n.step === void 0)
|
|
1370
|
+
continue;
|
|
1371
|
+
if (hasChild.has(n.key))
|
|
1372
|
+
errors.push(`${n.key}: \uC544\uB798 \uC694\uC18C\uAC00 \uC788\uB294\uB370 \uB2E8\uACC4\uB97C \uB4E0\uB2E4 \u2014 \uC2E4\uD589\uB418\uB294 \uAC83\uC740 \uC78E\uBFD0\uC774\uB2E4`);
|
|
1373
|
+
if (!stepSet.has(n.step))
|
|
1374
|
+
errors.push(`${n.key}: \uBAA8\uB974\uB294 \uB2E8\uACC4 '${n.step}' \u2014 steps \uC5D0 \uC5C6\uB2E4`);
|
|
1375
|
+
const already = seenStep.get(n.step);
|
|
1376
|
+
if (already)
|
|
1377
|
+
errors.push(`\uB2E8\uACC4 '${n.step}' \uAC00 '${already}' \uC640 '${n.key}' \uB458\uC5D0 \uC2E4\uB838\uB2E4 \u2014 \uC808\uCC28 \uC5B4\uB514\uC778\uC9C0 \uB9D0\uD560 \uC218 \uC5C6\uB2E4`);
|
|
1378
|
+
else
|
|
1379
|
+
seenStep.set(n.step, n.key);
|
|
1380
|
+
}
|
|
1381
|
+
return errors;
|
|
1382
|
+
}
|
|
1357
1383
|
|
|
1358
1384
|
// ../ops-contract/dist/energy-ingest.js
|
|
1359
1385
|
function resolveSubject(kind, localId, opts) {
|
|
@@ -5999,7 +6025,7 @@ var FlowEngine = class {
|
|
|
5999
6025
|
const n = this.locations.get(at);
|
|
6000
6026
|
if (n) n.occupancy++;
|
|
6001
6027
|
}
|
|
6002
|
-
this.recordMaterialActual(t, gtin, "produced", qty, spec.uom);
|
|
6028
|
+
this.recordMaterialActual(t, gtin, "produced", qty, spec.uom, gtin);
|
|
6003
6029
|
this.emit(objectEvent({
|
|
6004
6030
|
eventTime: this.now(),
|
|
6005
6031
|
action: "ADD",
|
|
@@ -6077,12 +6103,12 @@ var FlowEngine = class {
|
|
|
6077
6103
|
* 실제 자재 이동을 작업에 적어 둔다 — ISA-95 `JobResponse.MaterialActual`.
|
|
6078
6104
|
* 같은 품목·같은 쓰임은 **한 줄로 합친다**(줄을 늘리면 실적을 세는 쪽이 중복을 걷어내야 한다).
|
|
6079
6105
|
*/
|
|
6080
|
-
recordMaterialActual(t, definitionId, use, quantity, uom) {
|
|
6106
|
+
recordMaterialActual(t, definitionId, use, quantity, uom, lotId) {
|
|
6081
6107
|
if (!definitionId || !(quantity > 0)) return;
|
|
6082
6108
|
const rows = t.materialActual ??= [];
|
|
6083
|
-
const hit = rows.find((r) => r.definitionId === definitionId && r.use === use && r.uom === uom);
|
|
6109
|
+
const hit = rows.find((r) => r.definitionId === definitionId && r.use === use && r.uom === uom && r.lotId === lotId);
|
|
6084
6110
|
if (hit) hit.quantity += quantity;
|
|
6085
|
-
else rows.push({ definitionId, use, quantity, ...uom ? { uom } : {} });
|
|
6111
|
+
else rows.push({ definitionId, use, quantity, ...uom ? { uom } : {}, ...lotId ? { lotId } : {} });
|
|
6086
6112
|
}
|
|
6087
6113
|
/** 확보한 자재를 **작업 시작 시점에 소비**한다 — 수량이 0 이 되면 물품 자체가 사라진다. */
|
|
6088
6114
|
consumeMaterials(t, taken) {
|
|
@@ -6094,7 +6120,7 @@ var FlowEngine = class {
|
|
|
6094
6120
|
const left = (it.qty ?? 1) - take;
|
|
6095
6121
|
if (left > 0) {
|
|
6096
6122
|
it.qty = left;
|
|
6097
|
-
this.recordMaterialActual(t, it.definitionId ?? it.gtin, "consumed", take, it.uom);
|
|
6123
|
+
this.recordMaterialActual(t, it.definitionId ?? it.gtin, "consumed", take, it.uom, it.epc);
|
|
6098
6124
|
this.emit(objectEvent({
|
|
6099
6125
|
eventTime: this.now(),
|
|
6100
6126
|
action: "OBSERVE",
|
|
@@ -6107,7 +6133,7 @@ var FlowEngine = class {
|
|
|
6107
6133
|
}));
|
|
6108
6134
|
continue;
|
|
6109
6135
|
}
|
|
6110
|
-
this.recordMaterialActual(t, it.definitionId ?? it.gtin, "consumed", take, it.uom);
|
|
6136
|
+
this.recordMaterialActual(t, it.definitionId ?? it.gtin, "consumed", take, it.uom, it.epc);
|
|
6111
6137
|
whole.push(epc);
|
|
6112
6138
|
}
|
|
6113
6139
|
if (!whole.length) return;
|
|
@@ -7228,6 +7254,7 @@ var MesKernel = class extends FlowEngine {
|
|
|
7228
7254
|
this.assertNoDoubleProduction(productionSpec.definition.operations);
|
|
7229
7255
|
}
|
|
7230
7256
|
this.assertRecipeOperationTags();
|
|
7257
|
+
this.assertRouteProcedures();
|
|
7231
7258
|
}
|
|
7232
7259
|
/**
|
|
7233
7260
|
* **산출을 두 곳에서 만들지 않는다** — 기동 때 막는다.
|
|
@@ -7269,6 +7296,28 @@ var MesKernel = class extends FlowEngine {
|
|
|
7269
7296
|
*
|
|
7270
7297
|
* 런타임에 조용히 어긋나는 것보다 기동이 실패하는 편이 낫다.
|
|
7271
7298
|
*/
|
|
7299
|
+
/**
|
|
7300
|
+
* 라우트의 **절차 계층**이 단계와 어긋나지 않는가 (ISA-88).
|
|
7301
|
+
*
|
|
7302
|
+
* ── 왜 이제야 부르나 (2026-08-31) ─────────────────────────────────────────
|
|
7303
|
+
* 계약이 `procedureViolations` 를 갖고 있었는데 **아무도 부르지 않았다** — 계약의 자기 시험만
|
|
7304
|
+
* 불렀다. 그래서 절차를 선언해도 어긋난 것이 그냥 지나갔고, 화면은 잘못된 트리를 오류 없이 보였다.
|
|
7305
|
+
*
|
|
7306
|
+
* 선언을 읽는 곳이 있는지 보는 하네스가 이것을 잡았다(§`declaration-is-read.test.ts`).
|
|
7307
|
+
*
|
|
7308
|
+
* 판정은 계약이 한다 — 여기서 다시 만들면 규칙이 두 벌이 되고, 화면도 같은 판정을 해야 한다.
|
|
7309
|
+
*/
|
|
7310
|
+
assertRouteProcedures() {
|
|
7311
|
+
const routes = this.productionSpec?.definition?.routes ?? [];
|
|
7312
|
+
const bad = routes.flatMap((r) => procedureViolations(r).map((v) => `route '${r.key}': ${v}`));
|
|
7313
|
+
if (bad.length) {
|
|
7314
|
+
throw new Error(
|
|
7315
|
+
`declared procedure hierarchy does not match the route steps:
|
|
7316
|
+
- ${bad.join("\n - ")}
|
|
7317
|
+
The steps are the source of truth for execution order; the hierarchy only names them. A mismatch would show a tree on screen that the twin never executes.`
|
|
7318
|
+
);
|
|
7319
|
+
}
|
|
7320
|
+
}
|
|
7272
7321
|
assertRecipeOperationTags() {
|
|
7273
7322
|
const def = this.productionSpec?.definition;
|
|
7274
7323
|
if (!def?.recipes?.length) return;
|
|
@@ -7564,7 +7613,14 @@ var MesKernel = class extends FlowEngine {
|
|
|
7564
7613
|
}
|
|
7565
7614
|
return found;
|
|
7566
7615
|
}
|
|
7567
|
-
|
|
7616
|
+
const usable = (d.recipes ?? []).filter((r) => !this.effectivityOf(r));
|
|
7617
|
+
if (!usable.length) {
|
|
7618
|
+
const when = (d.recipes ?? []).map((r) => `${r.key}(${this.effectivityOf(r)})`).join(", ");
|
|
7619
|
+
throw new Error(
|
|
7620
|
+
`no declared recipe is in effect now. Declared: [${when || "(none)"}]. The kernel does not fall back to a retired recipe \u2014 that would make this order produce an item whose bill of materials no longer holds. Name a recipe on the order, or extend the effective period.`
|
|
7621
|
+
);
|
|
7622
|
+
}
|
|
7623
|
+
return usable[0];
|
|
7568
7624
|
}
|
|
7569
7625
|
/** 자재 키 → 구체 gtin 클래스(idpat). 구체 식별은 바인딩+prefix 로 인스턴스가 주입. */
|
|
7570
7626
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@operato/twin-kernel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
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.
|
|
31
|
+
"@operato/ops-contract": "^0.9.0"
|
|
32
32
|
}
|
|
33
33
|
}
|