@operato/twin-kernel 0.7.44 → 0.7.45

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.
@@ -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
  * 정체성의 **근거** — 값이 어디서 왔나. 「우리가 구현했나」와 **다른 축**이다.
@@ -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
  * 않는다 — 소비처가 자료의 뿌리를 읽고 통째로 잘못된 답을 만든다.
@@ -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
  /**
@@ -59,7 +59,7 @@ export class EmsKernel extends FlowEngine {
59
59
  closed = [];
60
60
  closedTotal = 0;
61
61
  peak;
62
- /** 이 구간에서 이미 제안을 냈나 — 같은 사실을 되풀어 방송하지 않는다(라이브 브리지의 교훈). */
62
+ /** 이 구간에서 이미 제안을 냈나 — 같은 사실을 되풀어 브로드캐스팅하지 않는다(라이브 브리지의 교훈). */
63
63
  suggestedFor;
64
64
  windowMs;
65
65
  /**
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('.');
@@ -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;
@@ -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, gdtiUri, objectEvent, ssccUri, transactionEvent } from "./epcis.js";
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.declaredObjectId(`PAL-${seq}`) ?? ssccUri(LEGACY_COMPANY_PREFIX, seq); // 팔레트
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.declaredBizTransactionId(`PO-${poSeq}`) ?? gdtiUri(LEGACY_COMPANY_PREFIX, this.identityDeclaration()?.documentTypes?.purchaseorder ?? '401', poSeq);
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.declaredBizTransactionId(`SO-${soSeq}`) ?? gdtiUri(LEGACY_COMPANY_PREFIX, this.identityDeclaration()?.documentTypes?.salesorder ?? '402', soSeq);
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.declaredObjectId(`PAL-${palSeq}`) ?? ssccUri(LEGACY_COMPANY_PREFIX, palSeq);
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.declaredObjectId(`SHP-${shpSeq}`) ?? ssccUri(LEGACY_COMPANY_PREFIX, shpSeq);
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/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 로 방송(⊥ tick). 클라 보간의 nowSim. */
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 로 방송(⊥ tick). 클라 보간의 nowSim. */
40
+ /** 클록-싱크 하트비트 메시지 — host 가 자기 send-rate 로 브로드캐스팅(⊥ tick). 클라 보간의 nowSim. */
41
41
  nowClock() {
42
42
  return { kind: 'clock', revision: this.currentRevision, simClockMs: this.kernel.getSnapshot().simClockMs };
43
43
  }
@@ -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, gdtiUri, ssccUri } from "./epcis.js";
22
- import { YARD_BIZSTEP, BTT_DELIVERY, graiUri } from "./yms-profile.js";
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.declaredObjectId(`TRL-${trSeq}`) ?? graiUri(LEGACY_CP, this.identityDeclaration()?.documentTypes?.trailer ?? '10', trSeq);
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.declaredObjectId(`CGO-${c}`) ?? ssccUri(LEGACY_CP, c);
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.declaredBizTransactionId(`APPT-${apSeq}`) ?? gdtiUri(LEGACY_CP, this.identityDeclaration()?.documentTypes?.appointment ?? '404', apSeq);
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,
@@ -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 로 방송(⊥ tick). 클라 보간의 nowSim. */
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.declaredObjectId(`PAL-${seq}`) ?? ssccUri(LEGACY_COMPANY_PREFIX, seq);
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.declaredBizTransactionId(`PO-${poSeq}`) ?? gdtiUri(LEGACY_COMPANY_PREFIX, this.identityDeclaration()?.documentTypes?.purchaseorder ?? "401", poSeq);
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.declaredBizTransactionId(`SO-${soSeq}`) ?? gdtiUri(LEGACY_COMPANY_PREFIX, this.identityDeclaration()?.documentTypes?.salesorder ?? "402", soSeq);
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.declaredObjectId(`PAL-${palSeq}`) ?? ssccUri(LEGACY_COMPANY_PREFIX, palSeq);
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.declaredObjectId(`SHP-${shpSeq}`) ?? ssccUri(LEGACY_COMPANY_PREFIX, shpSeq);
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.declaredObjectId(`TRL-${trSeq}`) ?? graiUri(LEGACY_CP, this.identityDeclaration()?.documentTypes?.trailer ?? "10", trSeq);
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.declaredObjectId(`CGO-${c}`) ?? ssccUri(LEGACY_CP, c);
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.declaredBizTransactionId(`APPT-${apSeq}`) ?? gdtiUri(LEGACY_CP, this.identityDeclaration()?.documentTypes?.appointment ?? "404", apSeq);
6500
+ const appt = this.requireBizTransactionId(`APPT-${apSeq}`, "appointment");
6475
6501
  const order = {
6476
6502
  id,
6477
6503
  kind,
@@ -7113,7 +7139,7 @@ var EmsKernel = class extends FlowEngine {
7113
7139
  closed = [];
7114
7140
  closedTotal = 0;
7115
7141
  peak;
7116
- /** 이 구간에서 이미 제안을 냈나 — 같은 사실을 되풀어 방송하지 않는다(라이브 브리지의 교훈). */
7142
+ /** 이 구간에서 이미 제안을 냈나 — 같은 사실을 되풀어 브로드캐스팅하지 않는다(라이브 브리지의 교훈). */
7117
7143
  suggestedFor;
7118
7144
  windowMs;
7119
7145
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/twin-kernel",
3
- "version": "0.7.44",
3
+ "version": "0.7.45",
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": {