@operato/twin-kernel 0.7.44 → 0.7.46
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/contract.d.ts +13 -0
- package/dist/domain-catalog.d.ts +1 -1
- package/dist/ems-kernel.d.ts +1 -1
- package/dist/ems-kernel.js +1 -1
- package/dist/epcis.js +13 -0
- package/dist/flow-engine.d.ts +12 -0
- package/dist/flow-engine.js +35 -2
- package/dist/kernel.js +6 -18
- package/dist/mes-kernel.d.ts +17 -0
- package/dist/mes-kernel.js +79 -7
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1 -1
- package/dist/yms-kernel.js +5 -11
- package/dist-cjs/index.cjs +90 -20
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -2242,6 +2242,19 @@ export interface IdentityDeclaration {
|
|
|
2242
2242
|
* 키는 문서의 종류(예: `workorder`), 값은 그 현장이 배정한 문서 타입이다.
|
|
2243
2243
|
*/
|
|
2244
2244
|
documentTypes?: Record<string, string>;
|
|
2245
|
+
/**
|
|
2246
|
+
* **이 현장이 배정받은 GS1 회사 프리픽스** — GS1 키(SSCC·GRAI·GDTI)를 쓰는 현장이 선언한다.
|
|
2247
|
+
*
|
|
2248
|
+
* 이 자리가 없으면 GS1 키를 만들 수 없다. 그것이 막힘이 아니라 **정직**이다: 프리픽스는 GS1 이
|
|
2249
|
+
* 회사에 배정하는 것이고, 커널이 값을 고르면 저널에 남의 번호가 영구히 남는다.
|
|
2250
|
+
*
|
|
2251
|
+
* 프리픽스가 없는 현장은 `namespaces` 로 간다 — 도메인만 있으면 표준 정합이다(CBV §8.2.4·§8.5.5).
|
|
2252
|
+
* 그쪽이 진입장벽이 낮다.
|
|
2253
|
+
*
|
|
2254
|
+
* 여기 있다는 것이 **발급받았다는 증명은 아니다** — 그 주장은 `issuedClaim` 이 따로 말하고, 우리는
|
|
2255
|
+
* 확인하지 않는다(§`IdentityGroundingView`).
|
|
2256
|
+
*/
|
|
2257
|
+
companyPrefix?: string;
|
|
2245
2258
|
}
|
|
2246
2259
|
/**
|
|
2247
2260
|
* 정체성의 **근거** — 값이 어디서 왔나. 「우리가 구현했나」와 **다른 축**이다.
|
package/dist/domain-catalog.d.ts
CHANGED
|
@@ -100,7 +100,7 @@ export interface TwinAxisInfo {
|
|
|
100
100
|
*
|
|
101
101
|
* **`source` 가 가리키는 자료 안의 경로다** — 예전에는 「`document` 일 때만 있다」고 못 박았는데,
|
|
102
102
|
* 상태 축도 중첩될 수 있다는 것이 에너지에서 드러났다(수요 구간은 `state.energy.closed` 에 산다).
|
|
103
|
-
* 그때 축 이름을 상태의 최상위 키로 맞추려면 같은 배열을 두 자리에 실어야 했다(
|
|
103
|
+
* 그때 축 이름을 상태의 최상위 키로 맞추려면 같은 배열을 두 자리에 실어야 했다(브로드캐스팅이 그만큼 커진다).
|
|
104
104
|
*
|
|
105
105
|
* 규칙은 하나다: **경로가 있으면 그 경로로 읽고, 없으면 축 이름으로 읽는다.** 빈 문자열로 두지
|
|
106
106
|
* 않는다 — 소비처가 자료의 뿌리를 읽고 통째로 잘못된 답을 만든다.
|
package/dist/ems-kernel.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export declare class EmsKernel extends FlowEngine {
|
|
|
11
11
|
private closed;
|
|
12
12
|
private closedTotal;
|
|
13
13
|
private peak?;
|
|
14
|
-
/** 이 구간에서 이미 제안을 냈나 — 같은 사실을 되풀어
|
|
14
|
+
/** 이 구간에서 이미 제안을 냈나 — 같은 사실을 되풀어 브로드캐스팅하지 않는다(라이브 브리지의 교훈). */
|
|
15
15
|
private suggestedFor?;
|
|
16
16
|
private windowMs;
|
|
17
17
|
/**
|
package/dist/ems-kernel.js
CHANGED
package/dist/epcis.js
CHANGED
|
@@ -77,6 +77,19 @@ export function lgtinClass(companyPrefix, itemRefAndIndicator, lot) {
|
|
|
77
77
|
*/
|
|
78
78
|
export function parseEpc(uri) {
|
|
79
79
|
const raw = String(uri ?? '');
|
|
80
|
+
/*
|
|
81
|
+
* ── CBV 형식을 먼저 읽는다 (2026-08-21) ──────────────────────────────────
|
|
82
|
+
* GS1 프리픽스가 없는 현장은 선언된 이름공간 아래에서 식별자를 만든다(CBV 2.0 §8.2.3·§8.2.4 개체
|
|
83
|
+
* `obj` · §8.3.3·§8.3.4 클래스 `class`). 그 형식을 모르면 `scheme: 'unknown'` · `instance: false` 가
|
|
84
|
+
* 되고, 소비처가 **개체를 클래스로 읽는다** — 미러가 트레일러의 개체 URI 를 그 물품의 품번으로 적었다
|
|
85
|
+
* (적합성 대조가 「미러만 아는 것=[gtin]」으로 잡았다).
|
|
86
|
+
*
|
|
87
|
+
* 표지가 종류를 말한다. 그것이 이 형식의 설계이고(그래서 표지가 필수다) 여기서 그대로 읽는다.
|
|
88
|
+
*/
|
|
89
|
+
if (/(?:\/obj\/|:obj:)[^/:\s]+$/.test(raw))
|
|
90
|
+
return { scheme: 'unknown', instance: true, uri: raw };
|
|
91
|
+
if (/(?:\/class\/|:class:)[^/:\s]+$/.test(raw))
|
|
92
|
+
return { scheme: 'unknown', instance: false, uri: raw };
|
|
80
93
|
const cls = raw.match(/^urn:epc:class:lgtin:(.+)$/);
|
|
81
94
|
if (cls) {
|
|
82
95
|
const seg = cls[1].split('.');
|
package/dist/flow-engine.d.ts
CHANGED
|
@@ -1271,6 +1271,18 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
1271
1271
|
* 호출부가 무엇을 할지 정한다(레거시 경로는 아직 상수를 쓴다).
|
|
1272
1272
|
*/
|
|
1273
1273
|
protected declaredObjectId(id: string | number): string | undefined;
|
|
1274
|
+
/**
|
|
1275
|
+
* 개체 식별자 — **선언이 없으면 오류를 낸다.**
|
|
1276
|
+
*
|
|
1277
|
+
* 예전에는 선언이 없으면 커널 상수 프리픽스로 만들었다(`0614141` — GS1 이 규격 예제로 쓰다 버린
|
|
1278
|
+
* 값이고 `06…` 은 실 배정 대역이다). 저널에 남는 것은 가짜 데이터가 아니라 **남의 번호일 수 있는
|
|
1279
|
+
* 데이터**이고, 저널은 지우지 않는다. 그래서 지어내지 않고 멈춘다 — 조용히 도는 것보다 낫다.
|
|
1280
|
+
*/
|
|
1281
|
+
protected requireObjectId(id: string | number): string;
|
|
1282
|
+
/** 거래 문서 식별자 — 선언된 이름공간 또는 선언된 GDTI 문서 타입. 둘 다 없으면 오류를 낸다. */
|
|
1283
|
+
protected requireBizTransactionId(id: string | number, docKind: string): string;
|
|
1284
|
+
/** 같은 문장을 두 곳에 적지 않는다 — 고치는 자리가 하나여야 한다. */
|
|
1285
|
+
private identityMissing;
|
|
1274
1286
|
/** 선언된 이름공간 아래의 **거래 문서 식별자**(발주·주문·어포인트먼트) — 없으면 답하지 않는다. */
|
|
1275
1287
|
protected declaredBizTransactionId(id: string | number): string | undefined;
|
|
1276
1288
|
private produceMaterials;
|
package/dist/flow-engine.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { OP_EVENT, CMD, locationStatusOf, readBoardEquipment, readBoardLocations, readBoardAssets, classClosure, capabilityOf, requiredTestsFor, priorityRank, dueStatusOf, effectivityAt, offCalendarAt, offCalendarReasonAt, minuteOfDayAt, activeShiftAt, subLotIdOf, itemKeyOf, identityGroundingOf } from "./contract.js";
|
|
13
13
|
import { ObservedReducer } from "./observed-reducer.js";
|
|
14
|
-
import { transformationEvent, aggregationEvent, objectEvent, parseEpc, DISP, ILMD_ATTR, CBV_BIZSTEP, objectUri, bizTransactionUri } from "./epcis.js";
|
|
14
|
+
import { transformationEvent, aggregationEvent, objectEvent, parseEpc, DISP, ILMD_ATTR, CBV_BIZSTEP, objectUri, bizTransactionUri, gdtiUri } from "./epcis.js";
|
|
15
15
|
import { OP_PARAM } from "./domain-definition.js";
|
|
16
16
|
import { parseIsoDuration } from "./iso-duration.js";
|
|
17
17
|
import { analyzeCapacity } from "./capacity.js";
|
|
@@ -1622,7 +1622,7 @@ export class FlowEngine {
|
|
|
1622
1622
|
}
|
|
1623
1623
|
this.observer.apply(envelope);
|
|
1624
1624
|
/* **구독자에게 그대로 흘린다** — 호스트가 시뮬·관측 두 모드에서 같은 배선을 쓰게 하기 위해서다
|
|
1625
|
-
* (`onEvent` 하나로
|
|
1625
|
+
* (`onEvent` 하나로 저널·브로드캐스팅이 붙는다). 관측 모드에서 이것은 **재방출**이지 새 사실이 아니다:
|
|
1626
1626
|
* 원천이 이미 그 이벤트를 갖고 있으므로, 호스트가 인입과 재방출을 **둘 다 저널에 적으면 중복**이
|
|
1627
1627
|
* 된다. 저널은 인입에서 한 번만 적는다. */
|
|
1628
1628
|
for (const h of this.observedHandlers())
|
|
@@ -2605,6 +2605,39 @@ export class FlowEngine {
|
|
|
2605
2605
|
}
|
|
2606
2606
|
return undefined;
|
|
2607
2607
|
}
|
|
2608
|
+
/**
|
|
2609
|
+
* 개체 식별자 — **선언이 없으면 오류를 낸다.**
|
|
2610
|
+
*
|
|
2611
|
+
* 예전에는 선언이 없으면 커널 상수 프리픽스로 만들었다(`0614141` — GS1 이 규격 예제로 쓰다 버린
|
|
2612
|
+
* 값이고 `06…` 은 실 배정 대역이다). 저널에 남는 것은 가짜 데이터가 아니라 **남의 번호일 수 있는
|
|
2613
|
+
* 데이터**이고, 저널은 지우지 않는다. 그래서 지어내지 않고 멈춘다 — 조용히 도는 것보다 낫다.
|
|
2614
|
+
*/
|
|
2615
|
+
requireObjectId(id) {
|
|
2616
|
+
const uri = this.declaredObjectId(id);
|
|
2617
|
+
if (uri)
|
|
2618
|
+
return uri;
|
|
2619
|
+
throw new Error(this.identityMissing('object'));
|
|
2620
|
+
}
|
|
2621
|
+
/** 거래 문서 식별자 — 선언된 이름공간 또는 선언된 GDTI 문서 타입. 둘 다 없으면 오류를 낸다. */
|
|
2622
|
+
requireBizTransactionId(id, docKind) {
|
|
2623
|
+
const uri = this.declaredBizTransactionId(id);
|
|
2624
|
+
if (uri)
|
|
2625
|
+
return uri;
|
|
2626
|
+
const decl = this.identityDeclaration();
|
|
2627
|
+
const docType = decl?.documentTypes?.[docKind];
|
|
2628
|
+
const prefix = decl?.companyPrefix;
|
|
2629
|
+
if (docType && prefix)
|
|
2630
|
+
return gdtiUri(prefix, docType, Number(id) || 0);
|
|
2631
|
+
throw new Error(this.identityMissing(`business transaction '${docKind}'`));
|
|
2632
|
+
}
|
|
2633
|
+
/** 같은 문장을 두 곳에 적지 않는다 — 고치는 자리가 하나여야 한다. */
|
|
2634
|
+
identityMissing(what) {
|
|
2635
|
+
return (`this twin has no identity declaration, so the kernel cannot name a ${what}. ` +
|
|
2636
|
+
'Declare `identity.namespaces` on the model (CBV 2.0 §8.2.4 `.../obj/<id>` · §8.5.5 `.../bt/<id>` — ' +
|
|
2637
|
+
'assigned by the owner of that internet domain), or `identity.documentTypes` with `identity.companyPrefix` ' +
|
|
2638
|
+
'for GS1 keys. The kernel does not invent a company prefix: GS1 assigns it to a company, and a value the ' +
|
|
2639
|
+
'kernel picked would sit in the journal forever with nothing saying it was invented.');
|
|
2640
|
+
}
|
|
2608
2641
|
/** 선언된 이름공간 아래의 **거래 문서 식별자**(발주·주문·어포인트먼트) — 없으면 답하지 않는다. */
|
|
2609
2642
|
declaredBizTransactionId(id) {
|
|
2610
2643
|
for (const ns of this.identityDeclaration()?.namespaces ?? []) {
|
package/dist/kernel.js
CHANGED
|
@@ -10,21 +10,9 @@ import { firstFitPolicy } from "./allocation-policy.js";
|
|
|
10
10
|
import { FlowEngine } from "./flow-engine.js";
|
|
11
11
|
import { planMakeToOrder } from "./make-to-order.js";
|
|
12
12
|
import { BIZSTEP, BTT } from "./wms-profile.js";
|
|
13
|
-
import { DISP, ILMD_ATTR, aggregationEvent,
|
|
13
|
+
import { DISP, ILMD_ATTR, aggregationEvent, objectEvent, transactionEvent } from "./epcis.js";
|
|
14
14
|
import { subLotIdOf, itemKeyOf } from "./contract.js";
|
|
15
15
|
const TRAVEL_MS = 30_000;
|
|
16
|
-
/**
|
|
17
|
-
* 이 내장 시나리오가 쓰는 **날조된 회사 프리픽스** — 선언이 없는 트윈만 이 길로 온다.
|
|
18
|
-
*
|
|
19
|
-
* 값이 실 배정 대역(`06…`)이라 저널에 남는 것은 **남의 번호일 수 있는 데이터**다. GS1 은 2022 년에
|
|
20
|
-
* 모든 예제를 `952` 로 옮겼으므로(CBV 2.0 변경 이력), 이 값은 「GS1 이 버린 옛 예제」이기도 하다.
|
|
21
|
-
*
|
|
22
|
-
* 지우는 길은 이 상수를 다른 값으로 바꾸는 것이 **아니다** — 이 경로는 부품·라우트까지 상수여서
|
|
23
|
-
* 정체성만 고치면 「지원하는 척」이 된다. 선언(`productionSpec`)으로 도는 트윈은 이 상수를 지나지
|
|
24
|
-
* 않으며, 지나는 트윈은 `identityGroundingOf` 가 `fabricated` 로 판정한다 — 릴리즈 게이트가 그 값으로
|
|
25
|
-
* 막는다. 이름에 `LEGACY_` 를 둔 이유는 **새 코드가 무심코 집어 들지 못하게** 하는 것이다.
|
|
26
|
-
*/
|
|
27
|
-
const LEGACY_COMPANY_PREFIX = '0614141';
|
|
28
16
|
const SHELF_MS = 30 * 24 * 3_600_000; // 기본 유통기한(30일)
|
|
29
17
|
const SHELF_JITTER_MS = 5 * 24 * 3_600_000; // 로트별 만료 편차(FEFO 가 FIFO 와 갈리게)
|
|
30
18
|
export class WmsKernel extends FlowEngine {
|
|
@@ -59,7 +47,7 @@ export class WmsKernel extends FlowEngine {
|
|
|
59
47
|
const dock = this.builtInLocation('dock', 'inbound pallets land here');
|
|
60
48
|
/* 선언이 있으면 선언에서(CBV §8.2.4) — 없으면 레거시 상수. 가드가 그 상수를 계속 센다. */
|
|
61
49
|
const seq = ++this.epcSeq;
|
|
62
|
-
const epc = this.
|
|
50
|
+
const epc = this.requireObjectId(`PAL-${seq}`); // 팔레트
|
|
63
51
|
const gtin = this.pickGtin(spec.content.skuMix); // SGTIN idpat = epcClass
|
|
64
52
|
/* 품목 구성이 비어 있으면 **도착을 만들지 않는다** — 무엇이 왔는지 말할 수 없는 입고는 사실이 아니다. */
|
|
65
53
|
if (!gtin)
|
|
@@ -67,7 +55,7 @@ export class WmsKernel extends FlowEngine {
|
|
|
67
55
|
const qty = this.randInt(spec.content.qtyPerLine.min, spec.content.qtyPerLine.max); // 케이스 수(비직렬)
|
|
68
56
|
const poSeq = ++this.poSeq;
|
|
69
57
|
/* 발주 문서 식별자 — 선언된 이름공간 아래(CBV §8.5.5) 또는 선언된 GDTI 문서 타입. */
|
|
70
|
-
const po = this.
|
|
58
|
+
const po = this.requireBizTransactionId(`PO-${poSeq}`, 'purchaseorder');
|
|
71
59
|
const eventTime = this.now();
|
|
72
60
|
const qtyList = [{ epcClass: gtin, quantity: qty }];
|
|
73
61
|
const poTxn = [{ type: BTT.po, bizTransaction: po }];
|
|
@@ -142,7 +130,7 @@ export class WmsKernel extends FlowEngine {
|
|
|
142
130
|
createSalesOrder(lines, spec) {
|
|
143
131
|
const id = `order-${++this.orderSeq}`;
|
|
144
132
|
const soSeq = ++this.soSeq;
|
|
145
|
-
const so = this.
|
|
133
|
+
const so = this.requireBizTransactionId(`SO-${soSeq}`, 'salesorder');
|
|
146
134
|
const requested = lines.reduce((s, l) => s + l.qty, 0);
|
|
147
135
|
const order = {
|
|
148
136
|
id, kind: 'outbound', status: 'created', requested, fulfilled: 0, bizTransaction: so,
|
|
@@ -355,7 +343,7 @@ export class WmsKernel extends FlowEngine {
|
|
|
355
343
|
/* 클래스 줄을 팔레트로 바꾼다 — 자리 점유는 줄 하나에서 줄 하나로(늘지 않는다). */
|
|
356
344
|
this.items.delete(key);
|
|
357
345
|
const palSeq = ++this.epcSeq;
|
|
358
|
-
const pallet = this.
|
|
346
|
+
const pallet = this.requireObjectId(`PAL-${palSeq}`);
|
|
359
347
|
const qtyList = [{ epcClass: m.definitionId, quantity: qty }];
|
|
360
348
|
this.items.set(pallet, { epc: pallet, gtin: m.definitionId, qty, location: at, disposition: DISP.in_progress });
|
|
361
349
|
const eventTime = this.now();
|
|
@@ -373,7 +361,7 @@ export class WmsKernel extends FlowEngine {
|
|
|
373
361
|
const shipDock = this.locationByType('dock-ship') ?? staging;
|
|
374
362
|
const eventTime = this.now();
|
|
375
363
|
const shpSeq = ++this.epcSeq;
|
|
376
|
-
const shipment = this.
|
|
364
|
+
const shipment = this.requireObjectId(`SHP-${shpSeq}`);
|
|
377
365
|
order.shipmentEpc = shipment;
|
|
378
366
|
const soTxn = [{ type: BTT.so, bizTransaction: order.bizTransaction }];
|
|
379
367
|
// 패킹: 화물(shipment) ← 팔레트 조립(merge). 팔레트는 출하 DELETE 까지 독립 유지 → consume 없음.
|
package/dist/mes-kernel.d.ts
CHANGED
|
@@ -107,7 +107,24 @@ export declare class MesKernel extends FlowEngine {
|
|
|
107
107
|
*/
|
|
108
108
|
private recipeDef;
|
|
109
109
|
/** 자재 키 → 구체 gtin 클래스(idpat). 구체 식별은 바인딩+prefix 로 인스턴스가 주입. */
|
|
110
|
+
/**
|
|
111
|
+
* 자재 키 → **품목 클래스 식별자.** 선언된 스킴이 모양을 정한다.
|
|
112
|
+
*
|
|
113
|
+
* ── 두 길 (2026-08-21) ─────────────────────────────────────────────────────
|
|
114
|
+
* **프리픽스가 있으면** `binding[key]` 를 품번(itemRef)으로 보고 SGTIN 을 조립한다(예전 거동).
|
|
115
|
+
* **없으면** `binding[key]` 가 **이미 완성된 클래스 식별자**다 — 커넥터가 선언된 도메인 아래에서
|
|
116
|
+
* 만든 CBV 형식(`https://<도메인>/class/<id>`)이고, 조립하면 그 값을 망친다.
|
|
117
|
+
*
|
|
118
|
+
* 실 연동에서 이것이 드러났다: 커넥터가 `binding` 에 완성된 URI 를 실었는데 커널이 그것을 품번으로
|
|
119
|
+
* 보고 프리픽스와 이어 붙이려 했다. 선언이 권위이므로 선언된 모양을 그대로 쓴다.
|
|
120
|
+
*/
|
|
110
121
|
private classOf;
|
|
122
|
+
/**
|
|
123
|
+
* 자재 키 + 일련번호 → **개체 식별자.**
|
|
124
|
+
*
|
|
125
|
+
* 프리픽스가 있으면 SGTIN(직렬), 없으면 선언된 이름공간 아래 CBV §8.2.4 `.../obj/<Objid>`.
|
|
126
|
+
* `Objid` 는 경로 성분 하나여야 하므로 구분자를 넣지 않는다 — 자재 키와 일련번호를 `-` 로 잇는다.
|
|
127
|
+
*/
|
|
111
128
|
private serialOf;
|
|
112
129
|
/**
|
|
113
130
|
* 이 공정이 **만든다고 선언한 품목** — 표준 `OpMaterialSpecification` `MaterialUse='produced'`.
|
package/dist/mes-kernel.js
CHANGED
|
@@ -49,9 +49,27 @@ export class MesKernel extends FlowEngine {
|
|
|
49
49
|
* 계약에서 `binding`·`companyPrefix` 는 선택이다 — 유통가공(비직렬 클래스+수량)은 자재 명세에
|
|
50
50
|
* gtin 을 직접 적으므로 필요 없다. 그래서 **필요한 경로에서만** 요구하고, 없으면 기동을 막는다.
|
|
51
51
|
*/
|
|
52
|
-
if (productionSpec?.definition?.recipes?.length
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
if (productionSpec?.definition?.recipes?.length) {
|
|
53
|
+
/*
|
|
54
|
+
* ── 프리픽스가 없는 현장도 세운다 (2026-08-21) ──────────────────────────
|
|
55
|
+
* 예전에는 `companyPrefix` 가 없으면 무조건 거절했다. 그래서 **GS1 회사 프리픽스가 없는 실
|
|
56
|
+
* 시스템이 기동조차 못 했다** — 첫 실 연동(F&B MES)이 정확히 그 자리에서 막혔다.
|
|
57
|
+
*
|
|
58
|
+
* 표준은 GS1 키만 인정하지 않는다. 선언된 도메인 아래의 클래스 식별자
|
|
59
|
+
* (CBV 2.0 §8.3.4 `.../class/<id>`)도 정합이고 소유 권한자는 그 도메인의 주인이다
|
|
60
|
+
* (EPCIS 2.0 §6.4). 그러면 프리픽스를 요구할 근거가 없다.
|
|
61
|
+
*
|
|
62
|
+
* 두 길을 섞지 않는다: 프리픽스가 있으면 SGTIN, 없고 이름공간이 있으면 도메인 기반.
|
|
63
|
+
* 둘 다 없으면 자재 키를 식별자로 옮길 방법이 없으므로 그때만 거절한다.
|
|
64
|
+
*/
|
|
65
|
+
const hasPrefix = !!productionSpec.companyPrefix;
|
|
66
|
+
const hasNamespace = !!productionSpec.identity?.namespaces?.length;
|
|
67
|
+
if (!productionSpec.binding || !(hasPrefix || hasNamespace)) {
|
|
68
|
+
throw new Error('recipe-driven production needs `binding`, and either `companyPrefix` (GS1 keys) or ' +
|
|
69
|
+
'`identity.namespaces` (domain-based identifiers — CBV 2.0 §8.3.4 `.../class/<id>`, assigned by the ' +
|
|
70
|
+
'owner of that internet domain). The definition does not know identifiers, so material keys cannot ' +
|
|
71
|
+
'be resolved without one of the two. The kernel does not invent a company prefix.');
|
|
72
|
+
}
|
|
55
73
|
}
|
|
56
74
|
/* 겹침 검사는 **바인딩 검증 뒤**다 — 검사 자체가 자재 키를 식별자로 옮겨 견주기 때문이다. */
|
|
57
75
|
if (productionSpec?.definition?.operations) {
|
|
@@ -106,7 +124,27 @@ export class MesKernel extends FlowEngine {
|
|
|
106
124
|
* 아니라 **하나를 고르는** 것이므로 규칙이 갈리지 않는다.
|
|
107
125
|
*/
|
|
108
126
|
identityDeclaration() {
|
|
109
|
-
|
|
127
|
+
/*
|
|
128
|
+
* ── 가리지 않고 **합친다** (2026-08-21) ────────────────────────────────────
|
|
129
|
+
* 예전에는 생산 선언이 있으면 모델 선언을 **가렸다**(`?? super`). 그래서 생산 선언이 프리픽스만
|
|
130
|
+
* 말하고 이름공간은 모델에만 있는 현장에서, 커널이 모델의 이름공간을 보지 못해 작업지시 식별자를
|
|
131
|
+
* 만들 수 없었다 — 시험이 그것을 잡았다.
|
|
132
|
+
*
|
|
133
|
+
* 두 선언은 경쟁이 아니라 같은 사실의 두 자리다(생산 선언은 먼저 생긴 자리다). 항목별로 있는 것을
|
|
134
|
+
* 쓴다: 생산 선언이 말한 것이 이기고, 말하지 않은 것은 모델에서 온다.
|
|
135
|
+
*/
|
|
136
|
+
const own = this.productionSpec?.identity;
|
|
137
|
+
const model = super.identityDeclaration();
|
|
138
|
+
if (!own)
|
|
139
|
+
return model;
|
|
140
|
+
if (!model)
|
|
141
|
+
return own;
|
|
142
|
+
return {
|
|
143
|
+
namespaces: own.namespaces?.length ? own.namespaces : model.namespaces,
|
|
144
|
+
documentTypes: { ...model.documentTypes, ...own.documentTypes },
|
|
145
|
+
...(own.companyPrefix ?? model.companyPrefix ? { companyPrefix: own.companyPrefix ?? model.companyPrefix } : {}),
|
|
146
|
+
...(own.issuedClaim ?? model.issuedClaim ? { issuedClaim: own.issuedClaim ?? model.issuedClaim } : {})
|
|
147
|
+
};
|
|
110
148
|
}
|
|
111
149
|
identityGroundingView() {
|
|
112
150
|
return identityGroundingOf(this.productionSpec);
|
|
@@ -264,11 +302,44 @@ export class MesKernel extends FlowEngine {
|
|
|
264
302
|
return d.recipes[0];
|
|
265
303
|
}
|
|
266
304
|
/** 자재 키 → 구체 gtin 클래스(idpat). 구체 식별은 바인딩+prefix 로 인스턴스가 주입. */
|
|
305
|
+
/**
|
|
306
|
+
* 자재 키 → **품목 클래스 식별자.** 선언된 스킴이 모양을 정한다.
|
|
307
|
+
*
|
|
308
|
+
* ── 두 길 (2026-08-21) ─────────────────────────────────────────────────────
|
|
309
|
+
* **프리픽스가 있으면** `binding[key]` 를 품번(itemRef)으로 보고 SGTIN 을 조립한다(예전 거동).
|
|
310
|
+
* **없으면** `binding[key]` 가 **이미 완성된 클래스 식별자**다 — 커넥터가 선언된 도메인 아래에서
|
|
311
|
+
* 만든 CBV 형식(`https://<도메인>/class/<id>`)이고, 조립하면 그 값을 망친다.
|
|
312
|
+
*
|
|
313
|
+
* 실 연동에서 이것이 드러났다: 커넥터가 `binding` 에 완성된 URI 를 실었는데 커널이 그것을 품번으로
|
|
314
|
+
* 보고 프리픽스와 이어 붙이려 했다. 선언이 권위이므로 선언된 모양을 그대로 쓴다.
|
|
315
|
+
*/
|
|
267
316
|
classOf(materialKey) {
|
|
268
|
-
|
|
317
|
+
const spec = this.productionSpec;
|
|
318
|
+
const bound = spec.binding[materialKey];
|
|
319
|
+
if (spec.companyPrefix)
|
|
320
|
+
return sgtinClass(spec.companyPrefix, bound);
|
|
321
|
+
if (!bound) {
|
|
322
|
+
throw new Error(`material '${materialKey}' has no binding — the model must say which item this key names. ` +
|
|
323
|
+
'The kernel does not invent an identifier for it.');
|
|
324
|
+
}
|
|
325
|
+
/* 이미 완성된 식별자다. 모양이 표준에 맞는지는 판정기가 답한다(§classIdentifierViolation). */
|
|
326
|
+
return bound;
|
|
269
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* 자재 키 + 일련번호 → **개체 식별자.**
|
|
330
|
+
*
|
|
331
|
+
* 프리픽스가 있으면 SGTIN(직렬), 없으면 선언된 이름공간 아래 CBV §8.2.4 `.../obj/<Objid>`.
|
|
332
|
+
* `Objid` 는 경로 성분 하나여야 하므로 구분자를 넣지 않는다 — 자재 키와 일련번호를 `-` 로 잇는다.
|
|
333
|
+
*/
|
|
270
334
|
serialOf(materialKey, serial) {
|
|
271
|
-
|
|
335
|
+
const spec = this.productionSpec;
|
|
336
|
+
if (spec.companyPrefix)
|
|
337
|
+
return sgtinUri(spec.companyPrefix, spec.binding[materialKey], serial);
|
|
338
|
+
const uri = this.declaredObjectId(`${materialKey}-${serial}`);
|
|
339
|
+
if (uri)
|
|
340
|
+
return uri;
|
|
341
|
+
throw new Error(`cannot name an instance of '${materialKey}': the model declares neither \`companyPrefix\` (GS1 keys) nor a ` +
|
|
342
|
+
'usable `identity.namespaces` entry (CBV 2.0 §8.2.4 `.../obj/<id>`). The kernel does not invent one.');
|
|
272
343
|
}
|
|
273
344
|
/**
|
|
274
345
|
* 이 공정이 **만든다고 선언한 품목** — 표준 `OpMaterialSpecification` `MaterialUse='produced'`.
|
|
@@ -424,7 +495,8 @@ export class MesKernel extends FlowEngine {
|
|
|
424
495
|
* 번호라고 말해 주지 않는다.
|
|
425
496
|
*/
|
|
426
497
|
workOrderId(serial) {
|
|
427
|
-
|
|
498
|
+
/* 선언을 읽는 곳은 하나다(§`FlowEngine.identityDeclaration`) — 직접 읽으면 모델 자리를 놓친다. */
|
|
499
|
+
const identity = this.identityDeclaration();
|
|
428
500
|
for (const ns of identity?.namespaces ?? []) {
|
|
429
501
|
const uri = bizTransactionUri(ns, serial);
|
|
430
502
|
if (uri)
|
package/dist/runtime.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export declare class TwinRuntime {
|
|
|
28
28
|
revision: number;
|
|
29
29
|
state: StateSnapshot;
|
|
30
30
|
};
|
|
31
|
-
/** 클록-싱크 하트비트 메시지 — host 가 자기 send-rate 로
|
|
31
|
+
/** 클록-싱크 하트비트 메시지 — host 가 자기 send-rate 로 브로드캐스팅(⊥ tick). 클라 보간의 nowSim. */
|
|
32
32
|
nowClock(): Extract<SubscriptionMessage, {
|
|
33
33
|
kind: 'clock';
|
|
34
34
|
}>;
|
package/dist/runtime.js
CHANGED
|
@@ -37,7 +37,7 @@ export class TwinRuntime {
|
|
|
37
37
|
resync() {
|
|
38
38
|
return { revision: this.currentRevision, state: this.kernel.getSnapshot() };
|
|
39
39
|
}
|
|
40
|
-
/** 클록-싱크 하트비트 메시지 — host 가 자기 send-rate 로
|
|
40
|
+
/** 클록-싱크 하트비트 메시지 — host 가 자기 send-rate 로 브로드캐스팅(⊥ tick). 클라 보간의 nowSim. */
|
|
41
41
|
nowClock() {
|
|
42
42
|
return { kind: 'clock', revision: this.currentRevision, simClockMs: this.kernel.getSnapshot().simClockMs };
|
|
43
43
|
}
|
package/dist/yms-kernel.js
CHANGED
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
import { itemKeyOf } from "./contract.js";
|
|
19
19
|
import { firstFitPolicy } from "./allocation-policy.js";
|
|
20
20
|
import { FlowEngine } from "./flow-engine.js";
|
|
21
|
-
import { DISP, objectEvent, transactionEvent
|
|
22
|
-
import { YARD_BIZSTEP, BTT_DELIVERY
|
|
21
|
+
import { DISP, objectEvent, transactionEvent } from "./epcis.js";
|
|
22
|
+
import { YARD_BIZSTEP, BTT_DELIVERY } from "./yms-profile.js";
|
|
23
23
|
const TRAVEL_MS = 20_000; // 야드 이동
|
|
24
24
|
const DWELL_MS = 30_000; // 도크 체류(상·하차) — depart 이동에 folded
|
|
25
25
|
const WINDOW_DELAY_MS = 40_000; // 도착 후 어포인트먼트 창까지(early arrival → 대기)
|
|
@@ -34,12 +34,6 @@ const WINDOW_DELAY_MS = 40_000; // 도착 후 어포인트먼트 창까지(early
|
|
|
34
34
|
*/
|
|
35
35
|
const WINDOW_LENGTH_MS = 20 * 60_000;
|
|
36
36
|
const CARGO_PER_TRAILER = 2; // 트레일러 적재/하역 화물(SSCC) 수
|
|
37
|
-
/**
|
|
38
|
-
* 이 내장 시나리오가 쓰는 **날조된 회사 프리픽스** — 선언이 없는 트윈만 이 길로 온다.
|
|
39
|
-
* 근거·처분은 WMS 커널의 같은 상수 주석에 있다(`kernel.ts`). 이름의 `LEGACY_` 는 새 코드가 무심코
|
|
40
|
-
* 집어 들지 못하게 하는 표시다 — 이 상수를 지나는 트윈은 `fabricated` 로 판정된다.
|
|
41
|
-
*/
|
|
42
|
-
const LEGACY_CP = '0614141';
|
|
43
37
|
export class YmsKernel extends FlowEngine {
|
|
44
38
|
doorRR = 0;
|
|
45
39
|
cargoSeq = 0;
|
|
@@ -66,7 +60,7 @@ export class YmsKernel extends FlowEngine {
|
|
|
66
60
|
const inbound = kind === 'appointment';
|
|
67
61
|
const trSeq = ++this.epcSeq;
|
|
68
62
|
/* 선언이 있으면 선언에서(CBV §8.2.4) — 없으면 레거시 상수. */
|
|
69
|
-
const epc = this.
|
|
63
|
+
const epc = this.requireObjectId(`TRL-${trSeq}`);
|
|
70
64
|
this.items.set(epc, { epc, location: gate.id, disposition: DISP.in_progress });
|
|
71
65
|
gate.occupancy++;
|
|
72
66
|
this.emit(objectEvent({ eventTime: this.now(), action: 'ADD', bizStep: YARD_BIZSTEP.arriving, disposition: DISP.in_progress, epcList: [epc], readPoint: gate.id, bizLocation: gate.id }));
|
|
@@ -74,7 +68,7 @@ export class YmsKernel extends FlowEngine {
|
|
|
74
68
|
if (inbound) {
|
|
75
69
|
const cargo = Array.from({ length: CARGO_PER_TRAILER }, () => {
|
|
76
70
|
const c = ++this.cargoSeq;
|
|
77
|
-
return this.
|
|
71
|
+
return this.requireObjectId(`CGO-${c}`);
|
|
78
72
|
});
|
|
79
73
|
this.trailerCargo.set(epc, cargo);
|
|
80
74
|
this.aggregate(epc, cargo, { bizStep: YARD_BIZSTEP.arriving, readPoint: gate.id }); // 적재된 채 도착(자식 opaque, 미materialize)
|
|
@@ -82,7 +76,7 @@ export class YmsKernel extends FlowEngine {
|
|
|
82
76
|
const door = doors[this.doorRR++ % doors.length];
|
|
83
77
|
const id = `order-${++this.orderSeq}`;
|
|
84
78
|
const apSeq = ++this.soSeq;
|
|
85
|
-
const appt = this.
|
|
79
|
+
const appt = this.requireBizTransactionId(`APPT-${apSeq}`, 'appointment');
|
|
86
80
|
const order = { id, kind, status: 'created', requested: 1, fulfilled: 0, bizTransaction: appt, allocated: [epc], picked: [], dockDoor: door.id,
|
|
87
81
|
/* 내부 스케줄 게이트(시뮬 ms) — 표준 `EarliestStartTime` 과 같은 개념의 우리 단위. */
|
|
88
82
|
windowStartMs: this.clockMs + WINDOW_DELAY_MS,
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -914,6 +914,8 @@ function lgtinClass(companyPrefix, itemRefAndIndicator, lot) {
|
|
|
914
914
|
}
|
|
915
915
|
function parseEpc(uri) {
|
|
916
916
|
const raw = String(uri ?? "");
|
|
917
|
+
if (/(?:\/obj\/|:obj:)[^/:\s]+$/.test(raw)) return { scheme: "unknown", instance: true, uri: raw };
|
|
918
|
+
if (/(?:\/class\/|:class:)[^/:\s]+$/.test(raw)) return { scheme: "unknown", instance: false, uri: raw };
|
|
917
919
|
const cls = raw.match(/^urn:epc:class:lgtin:(.+)$/);
|
|
918
920
|
if (cls) {
|
|
919
921
|
const seg = cls[1].split(".");
|
|
@@ -3020,7 +3022,7 @@ var TwinRuntime = class {
|
|
|
3020
3022
|
resync() {
|
|
3021
3023
|
return { revision: this.currentRevision, state: this.kernel.getSnapshot() };
|
|
3022
3024
|
}
|
|
3023
|
-
/** 클록-싱크 하트비트 메시지 — host 가 자기 send-rate 로
|
|
3025
|
+
/** 클록-싱크 하트비트 메시지 — host 가 자기 send-rate 로 브로드캐스팅(⊥ tick). 클라 보간의 nowSim. */
|
|
3024
3026
|
nowClock() {
|
|
3025
3027
|
return { kind: "clock", revision: this.currentRevision, simClockMs: this.kernel.getSnapshot().simClockMs };
|
|
3026
3028
|
}
|
|
@@ -5474,6 +5476,32 @@ var FlowEngine = class {
|
|
|
5474
5476
|
}
|
|
5475
5477
|
return void 0;
|
|
5476
5478
|
}
|
|
5479
|
+
/**
|
|
5480
|
+
* 개체 식별자 — **선언이 없으면 오류를 낸다.**
|
|
5481
|
+
*
|
|
5482
|
+
* 예전에는 선언이 없으면 커널 상수 프리픽스로 만들었다(`0614141` — GS1 이 규격 예제로 쓰다 버린
|
|
5483
|
+
* 값이고 `06…` 은 실 배정 대역이다). 저널에 남는 것은 가짜 데이터가 아니라 **남의 번호일 수 있는
|
|
5484
|
+
* 데이터**이고, 저널은 지우지 않는다. 그래서 지어내지 않고 멈춘다 — 조용히 도는 것보다 낫다.
|
|
5485
|
+
*/
|
|
5486
|
+
requireObjectId(id) {
|
|
5487
|
+
const uri = this.declaredObjectId(id);
|
|
5488
|
+
if (uri) return uri;
|
|
5489
|
+
throw new Error(this.identityMissing("object"));
|
|
5490
|
+
}
|
|
5491
|
+
/** 거래 문서 식별자 — 선언된 이름공간 또는 선언된 GDTI 문서 타입. 둘 다 없으면 오류를 낸다. */
|
|
5492
|
+
requireBizTransactionId(id, docKind) {
|
|
5493
|
+
const uri = this.declaredBizTransactionId(id);
|
|
5494
|
+
if (uri) return uri;
|
|
5495
|
+
const decl = this.identityDeclaration();
|
|
5496
|
+
const docType = decl?.documentTypes?.[docKind];
|
|
5497
|
+
const prefix = decl?.companyPrefix;
|
|
5498
|
+
if (docType && prefix) return gdtiUri(prefix, docType, Number(id) || 0);
|
|
5499
|
+
throw new Error(this.identityMissing(`business transaction '${docKind}'`));
|
|
5500
|
+
}
|
|
5501
|
+
/** 같은 문장을 두 곳에 적지 않는다 — 고치는 자리가 하나여야 한다. */
|
|
5502
|
+
identityMissing(what) {
|
|
5503
|
+
return `this twin has no identity declaration, so the kernel cannot name a ${what}. Declare \`identity.namespaces\` on the model (CBV 2.0 \xA78.2.4 \`.../obj/<id>\` \xB7 \xA78.5.5 \`.../bt/<id>\` \u2014 assigned by the owner of that internet domain), or \`identity.documentTypes\` with \`identity.companyPrefix\` for GS1 keys. The kernel does not invent a company prefix: GS1 assigns it to a company, and a value the kernel picked would sit in the journal forever with nothing saying it was invented.`;
|
|
5504
|
+
}
|
|
5477
5505
|
/** 선언된 이름공간 아래의 **거래 문서 식별자**(발주·주문·어포인트먼트) — 없으면 답하지 않는다. */
|
|
5478
5506
|
declaredBizTransactionId(id) {
|
|
5479
5507
|
for (const ns of this.identityDeclaration()?.namespaces ?? []) {
|
|
@@ -6091,7 +6119,6 @@ function matches(s, req) {
|
|
|
6091
6119
|
|
|
6092
6120
|
// src/kernel.ts
|
|
6093
6121
|
var TRAVEL_MS = 3e4;
|
|
6094
|
-
var LEGACY_COMPANY_PREFIX = "0614141";
|
|
6095
6122
|
var SHELF_MS = 30 * 24 * 36e5;
|
|
6096
6123
|
var SHELF_JITTER_MS = 5 * 24 * 36e5;
|
|
6097
6124
|
var WmsKernel = class extends FlowEngine {
|
|
@@ -6126,12 +6153,12 @@ var WmsKernel = class extends FlowEngine {
|
|
|
6126
6153
|
onArrival(spec) {
|
|
6127
6154
|
const dock = this.builtInLocation("dock", "inbound pallets land here");
|
|
6128
6155
|
const seq = ++this.epcSeq;
|
|
6129
|
-
const epc = this.
|
|
6156
|
+
const epc = this.requireObjectId(`PAL-${seq}`);
|
|
6130
6157
|
const gtin = this.pickGtin(spec.content.skuMix);
|
|
6131
6158
|
if (!gtin) return;
|
|
6132
6159
|
const qty = this.randInt(spec.content.qtyPerLine.min, spec.content.qtyPerLine.max);
|
|
6133
6160
|
const poSeq = ++this.poSeq;
|
|
6134
|
-
const po = this.
|
|
6161
|
+
const po = this.requireBizTransactionId(`PO-${poSeq}`, "purchaseorder");
|
|
6135
6162
|
const eventTime = this.now();
|
|
6136
6163
|
const qtyList = [{ epcClass: gtin, quantity: qty }];
|
|
6137
6164
|
const poTxn = [{ type: BTT.po, bizTransaction: po }];
|
|
@@ -6203,7 +6230,7 @@ var WmsKernel = class extends FlowEngine {
|
|
|
6203
6230
|
createSalesOrder(lines, spec) {
|
|
6204
6231
|
const id = `order-${++this.orderSeq}`;
|
|
6205
6232
|
const soSeq = ++this.soSeq;
|
|
6206
|
-
const so = this.
|
|
6233
|
+
const so = this.requireBizTransactionId(`SO-${soSeq}`, "salesorder");
|
|
6207
6234
|
const requested = lines.reduce((s, l) => s + l.qty, 0);
|
|
6208
6235
|
const order = {
|
|
6209
6236
|
id,
|
|
@@ -6377,7 +6404,7 @@ var WmsKernel = class extends FlowEngine {
|
|
|
6377
6404
|
if (qty <= 0) continue;
|
|
6378
6405
|
this.items.delete(key);
|
|
6379
6406
|
const palSeq = ++this.epcSeq;
|
|
6380
|
-
const pallet = this.
|
|
6407
|
+
const pallet = this.requireObjectId(`PAL-${palSeq}`);
|
|
6381
6408
|
const qtyList = [{ epcClass: m.definitionId, quantity: qty }];
|
|
6382
6409
|
this.items.set(pallet, { epc: pallet, gtin: m.definitionId, qty, location: at, disposition: DISP.in_progress });
|
|
6383
6410
|
const eventTime = this.now();
|
|
@@ -6392,7 +6419,7 @@ var WmsKernel = class extends FlowEngine {
|
|
|
6392
6419
|
const shipDock = this.locationByType("dock-ship") ?? staging;
|
|
6393
6420
|
const eventTime = this.now();
|
|
6394
6421
|
const shpSeq = ++this.epcSeq;
|
|
6395
|
-
const shipment = this.
|
|
6422
|
+
const shipment = this.requireObjectId(`SHP-${shpSeq}`);
|
|
6396
6423
|
order.shipmentEpc = shipment;
|
|
6397
6424
|
const soTxn = [{ type: BTT.so, bizTransaction: order.bizTransaction }];
|
|
6398
6425
|
this.aggregate(shipment, order.picked.slice(), { bizStep: BIZSTEP.packing, readPoint: staging.id, bizLocation: staging.id });
|
|
@@ -6425,7 +6452,6 @@ var DWELL_MS = 3e4;
|
|
|
6425
6452
|
var WINDOW_DELAY_MS = 4e4;
|
|
6426
6453
|
var WINDOW_LENGTH_MS = 20 * 6e4;
|
|
6427
6454
|
var CARGO_PER_TRAILER = 2;
|
|
6428
|
-
var LEGACY_CP = "0614141";
|
|
6429
6455
|
var YmsKernel = class extends FlowEngine {
|
|
6430
6456
|
doorRR = 0;
|
|
6431
6457
|
cargoSeq = 0;
|
|
@@ -6456,14 +6482,14 @@ var YmsKernel = class extends FlowEngine {
|
|
|
6456
6482
|
if (!gate || doors.length === 0) return;
|
|
6457
6483
|
const inbound = kind === "appointment";
|
|
6458
6484
|
const trSeq = ++this.epcSeq;
|
|
6459
|
-
const epc = this.
|
|
6485
|
+
const epc = this.requireObjectId(`TRL-${trSeq}`);
|
|
6460
6486
|
this.items.set(epc, { epc, location: gate.id, disposition: DISP.in_progress });
|
|
6461
6487
|
gate.occupancy++;
|
|
6462
6488
|
this.emit(objectEvent({ eventTime: this.now(), action: "ADD", bizStep: YARD_BIZSTEP.arriving, disposition: DISP.in_progress, epcList: [epc], readPoint: gate.id, bizLocation: gate.id }));
|
|
6463
6489
|
if (inbound) {
|
|
6464
6490
|
const cargo = Array.from({ length: CARGO_PER_TRAILER }, () => {
|
|
6465
6491
|
const c = ++this.cargoSeq;
|
|
6466
|
-
return this.
|
|
6492
|
+
return this.requireObjectId(`CGO-${c}`);
|
|
6467
6493
|
});
|
|
6468
6494
|
this.trailerCargo.set(epc, cargo);
|
|
6469
6495
|
this.aggregate(epc, cargo, { bizStep: YARD_BIZSTEP.arriving, readPoint: gate.id });
|
|
@@ -6471,7 +6497,7 @@ var YmsKernel = class extends FlowEngine {
|
|
|
6471
6497
|
const door = doors[this.doorRR++ % doors.length];
|
|
6472
6498
|
const id = `order-${++this.orderSeq}`;
|
|
6473
6499
|
const apSeq = ++this.soSeq;
|
|
6474
|
-
const appt = this.
|
|
6500
|
+
const appt = this.requireBizTransactionId(`APPT-${apSeq}`, "appointment");
|
|
6475
6501
|
const order = {
|
|
6476
6502
|
id,
|
|
6477
6503
|
kind,
|
|
@@ -6617,10 +6643,14 @@ var MesKernel = class extends FlowEngine {
|
|
|
6617
6643
|
if (productionSpec?.definition?.operations) {
|
|
6618
6644
|
this.loadOperations(productionSpec.definition.operations);
|
|
6619
6645
|
}
|
|
6620
|
-
if (productionSpec?.definition?.recipes?.length
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
)
|
|
6646
|
+
if (productionSpec?.definition?.recipes?.length) {
|
|
6647
|
+
const hasPrefix = !!productionSpec.companyPrefix;
|
|
6648
|
+
const hasNamespace = !!productionSpec.identity?.namespaces?.length;
|
|
6649
|
+
if (!productionSpec.binding || !(hasPrefix || hasNamespace)) {
|
|
6650
|
+
throw new Error(
|
|
6651
|
+
"recipe-driven production needs `binding`, and either `companyPrefix` (GS1 keys) or `identity.namespaces` (domain-based identifiers \u2014 CBV 2.0 \xA78.3.4 `.../class/<id>`, assigned by the owner of that internet domain). The definition does not know identifiers, so material keys cannot be resolved without one of the two. The kernel does not invent a company prefix."
|
|
6652
|
+
);
|
|
6653
|
+
}
|
|
6624
6654
|
}
|
|
6625
6655
|
if (productionSpec?.definition?.operations) {
|
|
6626
6656
|
this.assertNoDoubleProduction(productionSpec.definition.operations);
|
|
@@ -6671,7 +6701,16 @@ var MesKernel = class extends FlowEngine {
|
|
|
6671
6701
|
* 아니라 **하나를 고르는** 것이므로 규칙이 갈리지 않는다.
|
|
6672
6702
|
*/
|
|
6673
6703
|
identityDeclaration() {
|
|
6674
|
-
|
|
6704
|
+
const own = this.productionSpec?.identity;
|
|
6705
|
+
const model = super.identityDeclaration();
|
|
6706
|
+
if (!own) return model;
|
|
6707
|
+
if (!model) return own;
|
|
6708
|
+
return {
|
|
6709
|
+
namespaces: own.namespaces?.length ? own.namespaces : model.namespaces,
|
|
6710
|
+
documentTypes: { ...model.documentTypes, ...own.documentTypes },
|
|
6711
|
+
...own.companyPrefix ?? model.companyPrefix ? { companyPrefix: own.companyPrefix ?? model.companyPrefix } : {},
|
|
6712
|
+
...own.issuedClaim ?? model.issuedClaim ? { issuedClaim: own.issuedClaim ?? model.issuedClaim } : {}
|
|
6713
|
+
};
|
|
6675
6714
|
}
|
|
6676
6715
|
identityGroundingView() {
|
|
6677
6716
|
return identityGroundingOf(this.productionSpec);
|
|
@@ -6820,11 +6859,42 @@ var MesKernel = class extends FlowEngine {
|
|
|
6820
6859
|
return d.recipes[0];
|
|
6821
6860
|
}
|
|
6822
6861
|
/** 자재 키 → 구체 gtin 클래스(idpat). 구체 식별은 바인딩+prefix 로 인스턴스가 주입. */
|
|
6862
|
+
/**
|
|
6863
|
+
* 자재 키 → **품목 클래스 식별자.** 선언된 스킴이 모양을 정한다.
|
|
6864
|
+
*
|
|
6865
|
+
* ── 두 길 (2026-08-21) ─────────────────────────────────────────────────────
|
|
6866
|
+
* **프리픽스가 있으면** `binding[key]` 를 품번(itemRef)으로 보고 SGTIN 을 조립한다(예전 거동).
|
|
6867
|
+
* **없으면** `binding[key]` 가 **이미 완성된 클래스 식별자**다 — 커넥터가 선언된 도메인 아래에서
|
|
6868
|
+
* 만든 CBV 형식(`https://<도메인>/class/<id>`)이고, 조립하면 그 값을 망친다.
|
|
6869
|
+
*
|
|
6870
|
+
* 실 연동에서 이것이 드러났다: 커넥터가 `binding` 에 완성된 URI 를 실었는데 커널이 그것을 품번으로
|
|
6871
|
+
* 보고 프리픽스와 이어 붙이려 했다. 선언이 권위이므로 선언된 모양을 그대로 쓴다.
|
|
6872
|
+
*/
|
|
6823
6873
|
classOf(materialKey) {
|
|
6824
|
-
|
|
6874
|
+
const spec = this.productionSpec;
|
|
6875
|
+
const bound = spec.binding[materialKey];
|
|
6876
|
+
if (spec.companyPrefix) return sgtinClass(spec.companyPrefix, bound);
|
|
6877
|
+
if (!bound) {
|
|
6878
|
+
throw new Error(
|
|
6879
|
+
`material '${materialKey}' has no binding \u2014 the model must say which item this key names. The kernel does not invent an identifier for it.`
|
|
6880
|
+
);
|
|
6881
|
+
}
|
|
6882
|
+
return bound;
|
|
6825
6883
|
}
|
|
6884
|
+
/**
|
|
6885
|
+
* 자재 키 + 일련번호 → **개체 식별자.**
|
|
6886
|
+
*
|
|
6887
|
+
* 프리픽스가 있으면 SGTIN(직렬), 없으면 선언된 이름공간 아래 CBV §8.2.4 `.../obj/<Objid>`.
|
|
6888
|
+
* `Objid` 는 경로 성분 하나여야 하므로 구분자를 넣지 않는다 — 자재 키와 일련번호를 `-` 로 잇는다.
|
|
6889
|
+
*/
|
|
6826
6890
|
serialOf(materialKey, serial) {
|
|
6827
|
-
|
|
6891
|
+
const spec = this.productionSpec;
|
|
6892
|
+
if (spec.companyPrefix) return sgtinUri(spec.companyPrefix, spec.binding[materialKey], serial);
|
|
6893
|
+
const uri = this.declaredObjectId(`${materialKey}-${serial}`);
|
|
6894
|
+
if (uri) return uri;
|
|
6895
|
+
throw new Error(
|
|
6896
|
+
`cannot name an instance of '${materialKey}': the model declares neither \`companyPrefix\` (GS1 keys) nor a usable \`identity.namespaces\` entry (CBV 2.0 \xA78.2.4 \`.../obj/<id>\`). The kernel does not invent one.`
|
|
6897
|
+
);
|
|
6828
6898
|
}
|
|
6829
6899
|
/**
|
|
6830
6900
|
* 이 공정이 **만든다고 선언한 품목** — 표준 `OpMaterialSpecification` `MaterialUse='produced'`.
|
|
@@ -6965,7 +7035,7 @@ var MesKernel = class extends FlowEngine {
|
|
|
6965
7035
|
* 번호라고 말해 주지 않는다.
|
|
6966
7036
|
*/
|
|
6967
7037
|
workOrderId(serial) {
|
|
6968
|
-
const identity = this.
|
|
7038
|
+
const identity = this.identityDeclaration();
|
|
6969
7039
|
for (const ns of identity?.namespaces ?? []) {
|
|
6970
7040
|
const uri = bizTransactionUri(ns, serial);
|
|
6971
7041
|
if (uri) return uri;
|
|
@@ -7113,7 +7183,7 @@ var EmsKernel = class extends FlowEngine {
|
|
|
7113
7183
|
closed = [];
|
|
7114
7184
|
closedTotal = 0;
|
|
7115
7185
|
peak;
|
|
7116
|
-
/** 이 구간에서 이미 제안을 냈나 — 같은 사실을 되풀어
|
|
7186
|
+
/** 이 구간에서 이미 제안을 냈나 — 같은 사실을 되풀어 브로드캐스팅하지 않는다(라이브 브리지의 교훈). */
|
|
7117
7187
|
suggestedFor;
|
|
7118
7188
|
windowMs;
|
|
7119
7189
|
/**
|
package/package.json
CHANGED