@operato/twin-kernel 0.7.43 → 0.7.44

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.
@@ -2015,6 +2015,19 @@ export declare function readBoardEquipment(def: TwinModelDef | Record<string, un
2015
2015
  /** 저장된 보드의 반복사용 자산 — 설비와 같은 정규화를 거친다. */
2016
2016
  export declare function readBoardAssets(def: TwinModelDef | Record<string, unknown>): NonNullable<TwinModelDef['assets']>;
2017
2017
  export interface TwinModelDef {
2018
+ /**
2019
+ * **이 트윈의 정체성 선언** — 팔레트·트레일러·문서의 식별자가 어디서 오나.
2020
+ *
2021
+ * ── 왜 모델에 있나 (2026-08-21) ──────────────────────────────────────────
2022
+ * 처음에는 `ProductionSpec.identity` 하나였다. 그때 선언 모드가 있는 커널이 MES 뿐이었기 때문이다.
2023
+ * 그런데 **정체성은 생산과 무관하다**: 창고는 팔레트(SSCC)를, 야드는 트레일러(GRAI)를 식별하는데
2024
+ * 그 둘은 아무것도 생산하지 않는다. 생산 선언에 두면 생산하지 않는 트윈이 정체성을 선언할 자리가 없고,
2025
+ * 실제로 그래서 두 커널이 프리픽스를 코드에 두고 있었다.
2026
+ *
2027
+ * 읽는 곳은 하나다(§`FlowEngine.identityDeclaration`). `ProductionSpec.identity` 는 **먼저 생긴
2028
+ * 자리**이고, 픽스처·호스트가 이 자리로 옮기면 사라진다.
2029
+ */
2030
+ identity?: IdentityDeclaration;
2018
2031
  /** parallelism = 동시 처리 수(LocationState.parallelism 참조). capacity 는 저장 용량. */
2019
2032
  locations: {
2020
2033
  id: string;
@@ -61,6 +61,17 @@ export interface TwinAxisInfo {
61
61
  * (저장·계약의 이름과 어긋나면 그 순간 방언이 생긴다).
62
62
  */
63
63
  axis: string;
64
+ /**
65
+ * **무엇이 이 축의 항목을 가리키나** — 없으면 소비처가 `id` 를 쓴다.
66
+ *
67
+ * 대부분의 축은 항목마다 `id` 가 있다. 그렇지 않은 축이 있고, 그것이 결함은 아니다 — 수요 구간의
68
+ * 정체성은 **구간이 언제 시작했나**다(요금의 알갱이가 그 시각으로 정해진다).
69
+ *
70
+ * 이 칸이 없던 동안 소비처는 `id`·`key`·`gtin` 을 **짐작**했고, 그래서 화면이 실재하는 항목을
71
+ * 「식별자 없음 — 참조할 수 없는 항목」으로 보였다. 값은 있는데 가리킬 수 없다고 말한 것이다.
72
+ * 짐작을 없애고 선언이 답한다 — 축이 사는 자리(`path`)를 선언하는 것과 같은 이유다.
73
+ */
74
+ idField?: string;
64
75
  /**
65
76
  * 이 축을 **어디서 읽나.**
66
77
  *
@@ -115,7 +115,17 @@ export const TWIN_AXES = [
115
115
  * 아직 세우지 않은 것: **요금 구간**(TariffPeriod)과 **원단위**(EnPI). 둘 다 아직 아무도 만들지
116
116
  * 않는다 — 선언만 하면 개념 지도가 언제나 0 을 보여 주고, 그것은 결손처럼 읽힌다(§10 7단계의 일).
117
117
  */
118
- { axis: 'demandWindows', path: 'energy.closed', label: 'twin.axis.demandWindows', kind: 'instance',
118
+ /*
119
+ * **무엇이 이 항목을 가리키나** — `idField` 가 말한다.
120
+ *
121
+ * 수요 구간에는 `id` 가 없다. 그것이 결함은 아니다: 구간의 정체성은 **그 구간이 언제 시작했나**이고
122
+ * (요금의 알갱이가 그 시각으로 정해진다) 커널도 그것으로 키를 만든다(`contract-projected-over:<startMs>`).
123
+ *
124
+ * 그런데 소비처가 `id`·`key`·`gtin` 을 **짐작**하고 있어서, 화면이 여섯 구간 모두를 「식별자 없음 —
125
+ * 참조할 수 없는 항목」으로 보였다. 값은 실재하는데 가리킬 수 없다고 말한 것이다. 짐작을 없애고
126
+ * 선언이 답하게 한다 — 축이 사는 자리(`path`)를 선언하는 것과 같은 이유다.
127
+ */
128
+ { axis: 'demandWindows', path: 'energy.closed', idField: 'startMs', label: 'twin.axis.demandWindows', kind: 'instance',
119
129
  source: 'state', historical: true, standardClass: {}, systems: ['ems'] }
120
130
  ];
121
131
  /** 관계 전체 — 지도의 선과 항목의 이웃이 여기서 나온다(화면은 이 목록을 갖지 않는다). */
package/dist/epcis.d.ts CHANGED
@@ -271,6 +271,20 @@ export declare function gdtiUri(companyPrefix: string, docType: string, serial:
271
271
  * 이름공간의 모양으로 URL 형태와 URN 형태를 가른다. 판정할 수 없는 모양이면 **답하지 않는다**(지어내지
272
272
  * 않는다) — 호출부가 다른 길을 고르게 한다.
273
273
  */
274
+ /**
275
+ * **선언된 이름공간 아래의 개체 식별자** — SSCC·GRAI 를 쓰지 않는 길.
276
+ *
277
+ * 팔레트(SSCC)·트레일러(GRAI)는 개체 식별자이고, 그 조립에는 GS1 회사 프리픽스가 필요하다. 프리픽스가
278
+ * 없는 현장은 그 길로 갈 수 없다 — 그런데 커널이 프리픽스를 지어내면 저널에 남의 번호가 영구히 남는다.
279
+ *
280
+ * 표준이 다른 길을 정해 두었다.
281
+ * · CBV 2.0 §8.2.4 `http(s)://[Subdomain.]Domain/⁎⁎/obj/Objid` — 그 도메인 소유자가 배정
282
+ * · CBV 2.0 §8.2.3 `urn:URNNamespace:⁎⁎:obj:Objid` — URN 이름공간 소유자가 배정
283
+ *
284
+ * `obj` 표지가 필수다(클래스의 `class`·거래문서의 `bt` 와 같은 구조다). 다만 표준은 EPC URI 나 Digital
285
+ * Link 를 **권한다**(SHOULD) — 이 길은 프리픽스가 없을 때의 정합 경로다.
286
+ */
287
+ export declare function objectUri(namespace: string, objId: string | number): string | undefined;
274
288
  export declare function bizTransactionUri(namespace: string, transId: string | number): string | undefined;
275
289
  /** 모든 빌더가 공통으로 받는 표준 헤더 옵션(선택) — 방출부가 필요할 때 채운다. */
276
290
  export interface EpcisHeaderOptions {
package/dist/epcis.js CHANGED
@@ -132,25 +132,50 @@ export function gdtiUri(companyPrefix, docType, serial) {
132
132
  * 이름공간의 모양으로 URL 형태와 URN 형태를 가른다. 판정할 수 없는 모양이면 **답하지 않는다**(지어내지
133
133
  * 않는다) — 호출부가 다른 길을 고르게 한다.
134
134
  */
135
+ /**
136
+ * **선언된 이름공간 아래의 개체 식별자** — SSCC·GRAI 를 쓰지 않는 길.
137
+ *
138
+ * 팔레트(SSCC)·트레일러(GRAI)는 개체 식별자이고, 그 조립에는 GS1 회사 프리픽스가 필요하다. 프리픽스가
139
+ * 없는 현장은 그 길로 갈 수 없다 — 그런데 커널이 프리픽스를 지어내면 저널에 남의 번호가 영구히 남는다.
140
+ *
141
+ * 표준이 다른 길을 정해 두었다.
142
+ * · CBV 2.0 §8.2.4 `http(s)://[Subdomain.]Domain/⁎⁎/obj/Objid` — 그 도메인 소유자가 배정
143
+ * · CBV 2.0 §8.2.3 `urn:URNNamespace:⁎⁎:obj:Objid` — URN 이름공간 소유자가 배정
144
+ *
145
+ * `obj` 표지가 필수다(클래스의 `class`·거래문서의 `bt` 와 같은 구조다). 다만 표준은 EPC URI 나 Digital
146
+ * Link 를 **권한다**(SHOULD) — 이 길은 프리픽스가 없을 때의 정합 경로다.
147
+ */
148
+ export function objectUri(namespace, objId) {
149
+ return underNamespace(namespace, 'obj', objId);
150
+ }
135
151
  export function bizTransactionUri(namespace, transId) {
152
+ return underNamespace(namespace, 'bt', transId);
153
+ }
154
+ /**
155
+ * 선언된 이름공간 아래에 표지를 붙여 식별자를 만든다 — 표지만 다르고 규칙은 같다.
156
+ *
157
+ * `obj`(개체 §8.2.3·§8.2.4) · `class`(클래스 §8.3.3·§8.3.4) · `bt`(거래문서 §8.5.4·§8.5.5) 가 같은
158
+ * 모양이다. 규칙을 세 곳에 적으면 한 곳만 고쳐지는 날이 온다.
159
+ */
160
+ function underNamespace(namespace, marker, id) {
136
161
  const ns = namespace?.trim();
137
162
  if (!ns)
138
163
  return undefined;
139
- const id = String(transId);
140
- /* `transID` 에 구분자가 들어가면 표준이 요구하는 「성분 하나」가 깨진다. */
141
- if (!id || id.includes('/') || id.includes(':'))
164
+ const v = String(id);
165
+ /* 표준이 요구하는 「성분 하나」가 깨진다 — 구분자가 든 값은 만들지 않는다. */
166
+ if (!v || v.includes('/') || v.includes(':'))
142
167
  return undefined;
143
168
  if (/^https?:\/\/[^/\s]+/.test(ns))
144
- return `${ns.replace(/\/+$/, '')}/bt/${id}`;
169
+ return `${ns.replace(/\/+$/, '')}/${marker}/${v}`;
145
170
  /*
146
- * **GS1 이 소유한 URN 공간에는 우리가 `:bt:` 만들 수 없다.** `urn:epc:`·`urn:epcglobal:` 의
147
- * 소유 권한자는 GS1 이고(EPCIS §6.4), 그 안의 형태는 표준이 정해 둔 것만 유효하다. 그 공간을
148
- * 이름공간으로 선언한 현장은 GDTI 문서 타입을 선언하는 쪽으로 가야 한다.
171
+ * **GS1 이 소유한 URN 공간에는 우리가 표지를 만들 수 없다.** `urn:epc:`·`urn:epcglobal:` 의 소유
172
+ * 권한자는 GS1 이고(EPCIS §6.4), 그 안의 형태는 표준이 정해 둔 것만 유효하다. 그 공간을 이름공간으로
173
+ * 선언한 현장은 GS1 키(SSCC·GRAI·GDTI)를 쓰는 쪽으로 가야 한다.
149
174
  */
150
175
  if (/^urn:epc(global)?:/.test(ns))
151
176
  return undefined;
152
177
  if (/^urn:[^:\s]+/.test(ns))
153
- return `${ns.replace(/:+$/, '')}:bt:${id}`;
178
+ return `${ns.replace(/:+$/, '')}:${marker}:${v}`;
154
179
  return undefined;
155
180
  }
156
181
  function header(type, eventTime, bizStep, opts) {
@@ -1,4 +1,4 @@
1
- import type { TestResult, ISOTime, MaterialQuantity, WorkCalendarEntry, EffectivePeriod, Effectivity, OffCalendarReason, ResourceProperty, ResourceClassDef, MaterialDefinition, Attention, TwinModelDef, CanonicalEnvelope, Command, CommandAck, EventHandler, EquipmentMotion, OeeMetrics, AssetState, GeneratorSpec, InterventionOutcome, OrderState, PersonState, ScenarioControl, ScenarioOverride, StateSnapshot, TwinKernel, Unsubscribe, LocationState, ItemState, EquipmentState, OrderStatusDelta, TaskState, StructureShift, IdentityGroundingView } from './contract.ts';
1
+ import type { TestResult, ISOTime, MaterialQuantity, WorkCalendarEntry, EffectivePeriod, Effectivity, OffCalendarReason, ResourceProperty, ResourceClassDef, MaterialDefinition, Attention, TwinModelDef, CanonicalEnvelope, Command, CommandAck, EventHandler, EquipmentMotion, OeeMetrics, AssetState, GeneratorSpec, InterventionOutcome, OrderState, PersonState, ScenarioControl, ScenarioOverride, StateSnapshot, TwinKernel, Unsubscribe, LocationState, ItemState, EquipmentState, OrderStatusDelta, TaskState, StructureShift, IdentityGroundingView, IdentityDeclaration } from './contract.ts';
2
2
  import type { EpcisEvent, BizTransactionElement } from './epcis.ts';
3
3
  import type { AllocationPolicy, SlotView } from './allocation-policy.ts';
4
4
  import type { DurationEstimator, DurationContext } from './duration-estimator.ts';
@@ -1253,6 +1253,26 @@ export declare abstract class FlowEngine implements TwinKernel {
1253
1253
  * 코어는 여기서 **정체성의 값을 정하지 않는다**: 「어디서 왔나」만 묻는다.
1254
1254
  */
1255
1255
  protected identityGroundingView(): IdentityGroundingView;
1256
+ /**
1257
+ * **이 트윈의 정체성 선언을 읽는 한 곳.**
1258
+ *
1259
+ * 모델이 갖는다(§`TwinModelDef.identity`) — 정체성은 생산과 무관하고, 생산하지 않는 트윈도 팔레트·
1260
+ * 트레일러를 식별한다. 선언을 든 커널이 자기 것을 먼저 쓰도록 override 할 수 있다(MES 가 그렇게 한다:
1261
+ * `ProductionSpec.identity` 가 먼저 생긴 자리다).
1262
+ *
1263
+ * 규칙을 두 곳에 적지 않기 위해 조립하는 쪽은 전부 이 함수를 지난다.
1264
+ */
1265
+ protected identityDeclaration(): IdentityDeclaration | undefined;
1266
+ /**
1267
+ * 선언된 이름공간 아래의 **개체 식별자** — 없으면 답하지 않는다.
1268
+ *
1269
+ * 팔레트·트레일러는 개체 식별자이고 GS1 키(SSCC·GRAI)에는 회사 프리픽스가 필요하다. 프리픽스가 없는
1270
+ * 현장은 CBV §8.2.3·§8.2.4 의 길로 간다. 선언이 없으면 **지어내지 않고 `undefined` 를 답한다** —
1271
+ * 호출부가 무엇을 할지 정한다(레거시 경로는 아직 상수를 쓴다).
1272
+ */
1273
+ protected declaredObjectId(id: string | number): string | undefined;
1274
+ /** 선언된 이름공간 아래의 **거래 문서 식별자**(발주·주문·어포인트먼트) — 없으면 답하지 않는다. */
1275
+ protected declaredBizTransactionId(id: string | number): string | undefined;
1256
1276
  private produceMaterials;
1257
1277
  /**
1258
1278
  * 이 작업이 **딛고 선 것**이 아직 있나 — 없으면 무엇이 없는지 답한다.
@@ -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 } from "./epcis.js";
14
+ import { transformationEvent, aggregationEvent, objectEvent, parseEpc, DISP, ILMD_ATTR, CBV_BIZSTEP, objectUri, bizTransactionUri } 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";
@@ -2578,6 +2578,42 @@ export class FlowEngine {
2578
2578
  identityGroundingView() {
2579
2579
  return identityGroundingOf(undefined);
2580
2580
  }
2581
+ /**
2582
+ * **이 트윈의 정체성 선언을 읽는 한 곳.**
2583
+ *
2584
+ * 모델이 갖는다(§`TwinModelDef.identity`) — 정체성은 생산과 무관하고, 생산하지 않는 트윈도 팔레트·
2585
+ * 트레일러를 식별한다. 선언을 든 커널이 자기 것을 먼저 쓰도록 override 할 수 있다(MES 가 그렇게 한다:
2586
+ * `ProductionSpec.identity` 가 먼저 생긴 자리다).
2587
+ *
2588
+ * 규칙을 두 곳에 적지 않기 위해 조립하는 쪽은 전부 이 함수를 지난다.
2589
+ */
2590
+ identityDeclaration() {
2591
+ return this.boardDef?.identity;
2592
+ }
2593
+ /**
2594
+ * 선언된 이름공간 아래의 **개체 식별자** — 없으면 답하지 않는다.
2595
+ *
2596
+ * 팔레트·트레일러는 개체 식별자이고 GS1 키(SSCC·GRAI)에는 회사 프리픽스가 필요하다. 프리픽스가 없는
2597
+ * 현장은 CBV §8.2.3·§8.2.4 의 길로 간다. 선언이 없으면 **지어내지 않고 `undefined` 를 답한다** —
2598
+ * 호출부가 무엇을 할지 정한다(레거시 경로는 아직 상수를 쓴다).
2599
+ */
2600
+ declaredObjectId(id) {
2601
+ for (const ns of this.identityDeclaration()?.namespaces ?? []) {
2602
+ const uri = objectUri(ns, id);
2603
+ if (uri)
2604
+ return uri;
2605
+ }
2606
+ return undefined;
2607
+ }
2608
+ /** 선언된 이름공간 아래의 **거래 문서 식별자**(발주·주문·어포인트먼트) — 없으면 답하지 않는다. */
2609
+ declaredBizTransactionId(id) {
2610
+ for (const ns of this.identityDeclaration()?.namespaces ?? []) {
2611
+ const uri = bizTransactionUri(ns, id);
2612
+ if (uri)
2613
+ return uri;
2614
+ }
2615
+ return undefined;
2616
+ }
2581
2617
  produceMaterials(t) {
2582
2618
  /* 도메인이 소유를 주장하면 코어는 비켜선다 — 둘이 만들면 재고가 두 배가 된다. */
2583
2619
  if (this.producesOwnOutputs(t.kind))
package/dist/kernel.js CHANGED
@@ -57,13 +57,17 @@ export class WmsKernel extends FlowEngine {
57
57
  /** 입고 도착 — §4 라이프사이클: ASN(PO) → 팔레트 조립 → 수령 → putaway task. */
58
58
  onArrival(spec) {
59
59
  const dock = this.builtInLocation('dock', 'inbound pallets land here');
60
- const epc = ssccUri(LEGACY_COMPANY_PREFIX, ++this.epcSeq); // 팔레트 SSCC
60
+ /* 선언이 있으면 선언에서(CBV §8.2.4) 없으면 레거시 상수. 가드가 그 상수를 계속 센다. */
61
+ const seq = ++this.epcSeq;
62
+ const epc = this.declaredObjectId(`PAL-${seq}`) ?? ssccUri(LEGACY_COMPANY_PREFIX, seq); // 팔레트
61
63
  const gtin = this.pickGtin(spec.content.skuMix); // SGTIN idpat = epcClass
62
64
  /* 품목 구성이 비어 있으면 **도착을 만들지 않는다** — 무엇이 왔는지 말할 수 없는 입고는 사실이 아니다. */
63
65
  if (!gtin)
64
66
  return;
65
67
  const qty = this.randInt(spec.content.qtyPerLine.min, spec.content.qtyPerLine.max); // 케이스 수(비직렬)
66
- const po = gdtiUri(LEGACY_COMPANY_PREFIX, '401', ++this.poSeq);
68
+ const poSeq = ++this.poSeq;
69
+ /* 발주 문서 식별자 — 선언된 이름공간 아래(CBV §8.5.5) 또는 선언된 GDTI 문서 타입. */
70
+ const po = this.declaredBizTransactionId(`PO-${poSeq}`) ?? gdtiUri(LEGACY_COMPANY_PREFIX, this.identityDeclaration()?.documentTypes?.purchaseorder ?? '401', poSeq);
67
71
  const eventTime = this.now();
68
72
  const qtyList = [{ epcClass: gtin, quantity: qty }];
69
73
  const poTxn = [{ type: BTT.po, bizTransaction: po }];
@@ -137,7 +141,8 @@ export class WmsKernel extends FlowEngine {
137
141
  /** 오더 생성 — 약속(납기·우선순위)은 base `promiseOf` 가 계산한다(도메인마다 다르게 재지 않는다). */
138
142
  createSalesOrder(lines, spec) {
139
143
  const id = `order-${++this.orderSeq}`;
140
- const so = gdtiUri(LEGACY_COMPANY_PREFIX, '402', ++this.soSeq);
144
+ const soSeq = ++this.soSeq;
145
+ const so = this.declaredBizTransactionId(`SO-${soSeq}`) ?? gdtiUri(LEGACY_COMPANY_PREFIX, this.identityDeclaration()?.documentTypes?.salesorder ?? '402', soSeq);
141
146
  const requested = lines.reduce((s, l) => s + l.qty, 0);
142
147
  const order = {
143
148
  id, kind: 'outbound', status: 'created', requested, fulfilled: 0, bizTransaction: so,
@@ -349,7 +354,8 @@ export class WmsKernel extends FlowEngine {
349
354
  continue;
350
355
  /* 클래스 줄을 팔레트로 바꾼다 — 자리 점유는 줄 하나에서 줄 하나로(늘지 않는다). */
351
356
  this.items.delete(key);
352
- const pallet = ssccUri(LEGACY_COMPANY_PREFIX, ++this.epcSeq);
357
+ const palSeq = ++this.epcSeq;
358
+ const pallet = this.declaredObjectId(`PAL-${palSeq}`) ?? ssccUri(LEGACY_COMPANY_PREFIX, palSeq);
353
359
  const qtyList = [{ epcClass: m.definitionId, quantity: qty }];
354
360
  this.items.set(pallet, { epc: pallet, gtin: m.definitionId, qty, location: at, disposition: DISP.in_progress });
355
361
  const eventTime = this.now();
@@ -366,7 +372,8 @@ export class WmsKernel extends FlowEngine {
366
372
  창고의 모양이다(도크 단계가 없는 현장이 있다). 그 사실은 화물의 출발 자리로 저널에 남는다. */
367
373
  const shipDock = this.locationByType('dock-ship') ?? staging;
368
374
  const eventTime = this.now();
369
- const shipment = ssccUri(LEGACY_COMPANY_PREFIX, ++this.epcSeq);
375
+ const shpSeq = ++this.epcSeq;
376
+ const shipment = this.declaredObjectId(`SHP-${shpSeq}`) ?? ssccUri(LEGACY_COMPANY_PREFIX, shpSeq);
370
377
  order.shipmentEpc = shipment;
371
378
  const soTxn = [{ type: BTT.so, bizTransaction: order.bizTransaction }];
372
379
  // 패킹: 화물(shipment) ← 팔레트 조립(merge). 팔레트는 출하 DELETE 까지 독립 유지 → consume 없음.
@@ -37,6 +37,13 @@ export declare class MesKernel extends FlowEngine {
37
37
  * 아니다). 선언이 없는 MES 트윈은 이제 만들 수 없으므로(`loadTwinModel` 이 거절한다) `fabricated` 로
38
38
  * 판정될 일이 없다.
39
39
  */
40
+ /**
41
+ * MES 는 **생산 선언의 정체성을 먼저** 쓴다 — `ProductionSpec.identity` 가 먼저 생긴 자리다.
42
+ *
43
+ * 모델 자리(§`TwinModelDef.identity`)로 옮기면 이 override 는 사라진다. 두 자리를 동시에 읽는 것이
44
+ * 아니라 **하나를 고르는** 것이므로 규칙이 갈리지 않는다.
45
+ */
46
+ protected identityDeclaration(): import("./contract.ts").IdentityDeclaration | undefined;
40
47
  protected identityGroundingView(): IdentityGroundingView;
41
48
  /**
42
49
  * 선언이 말하는 자리 타입이 **이 트윈에 있나** — 로드 시점에 한 번, 모아서 말한다 (2026-08-20).
@@ -99,6 +99,15 @@ export class MesKernel extends FlowEngine {
99
99
  * 아니다). 선언이 없는 MES 트윈은 이제 만들 수 없으므로(`loadTwinModel` 이 거절한다) `fabricated` 로
100
100
  * 판정될 일이 없다.
101
101
  */
102
+ /**
103
+ * MES 는 **생산 선언의 정체성을 먼저** 쓴다 — `ProductionSpec.identity` 가 먼저 생긴 자리다.
104
+ *
105
+ * 모델 자리(§`TwinModelDef.identity`)로 옮기면 이 override 는 사라진다. 두 자리를 동시에 읽는 것이
106
+ * 아니라 **하나를 고르는** 것이므로 규칙이 갈리지 않는다.
107
+ */
108
+ identityDeclaration() {
109
+ return this.productionSpec?.identity ?? super.identityDeclaration();
110
+ }
102
111
  identityGroundingView() {
103
112
  return identityGroundingOf(this.productionSpec);
104
113
  }
@@ -64,19 +64,25 @@ export class YmsKernel extends FlowEngine {
64
64
  if (!gate || doors.length === 0)
65
65
  return;
66
66
  const inbound = kind === 'appointment';
67
- const epc = graiUri(LEGACY_CP, '10', ++this.epcSeq);
67
+ const trSeq = ++this.epcSeq;
68
+ /* 선언이 있으면 선언에서(CBV §8.2.4) — 없으면 레거시 상수. */
69
+ const epc = this.declaredObjectId(`TRL-${trSeq}`) ?? graiUri(LEGACY_CP, this.identityDeclaration()?.documentTypes?.trailer ?? '10', trSeq);
68
70
  this.items.set(epc, { epc, location: gate.id, disposition: DISP.in_progress });
69
71
  gate.occupancy++;
70
72
  this.emit(objectEvent({ eventTime: this.now(), action: 'ADD', bizStep: YARD_BIZSTEP.arriving, disposition: DISP.in_progress, epcList: [epc], readPoint: gate.id, bizLocation: gate.id }));
71
73
  // 인바운드: 화물 적재된 채 도착 → 조립(트레일러←화물) 기록. 아웃바운드: 빈 트레일러(화물은 도크에서 적재).
72
74
  if (inbound) {
73
- const cargo = Array.from({ length: CARGO_PER_TRAILER }, () => ssccUri(LEGACY_CP, ++this.cargoSeq));
75
+ const cargo = Array.from({ length: CARGO_PER_TRAILER }, () => {
76
+ const c = ++this.cargoSeq;
77
+ return this.declaredObjectId(`CGO-${c}`) ?? ssccUri(LEGACY_CP, c);
78
+ });
74
79
  this.trailerCargo.set(epc, cargo);
75
80
  this.aggregate(epc, cargo, { bizStep: YARD_BIZSTEP.arriving, readPoint: gate.id }); // 적재된 채 도착(자식 opaque, 미materialize)
76
81
  }
77
82
  const door = doors[this.doorRR++ % doors.length];
78
83
  const id = `order-${++this.orderSeq}`;
79
- const appt = gdtiUri(LEGACY_CP, '404', ++this.soSeq);
84
+ const apSeq = ++this.soSeq;
85
+ const appt = this.declaredBizTransactionId(`APPT-${apSeq}`) ?? gdtiUri(LEGACY_CP, this.identityDeclaration()?.documentTypes?.appointment ?? '404', apSeq);
80
86
  const order = { id, kind, status: 'created', requested: 1, fulfilled: 0, bizTransaction: appt, allocated: [epc], picked: [], dockDoor: door.id,
81
87
  /* 내부 스케줄 게이트(시뮬 ms) — 표준 `EarliestStartTime` 과 같은 개념의 우리 단위. */
82
88
  windowStartMs: this.clockMs + WINDOW_DELAY_MS,
@@ -138,6 +138,7 @@ __export(index_exports, {
138
138
  monteCarloForecast: () => monteCarloForecast,
139
139
  monteCarloForecastAsync: () => monteCarloForecastAsync,
140
140
  objectEvent: () => objectEvent,
141
+ objectUri: () => objectUri,
141
142
  offCalendarAt: () => offCalendarAt,
142
143
  offCalendarReasonAt: () => offCalendarReasonAt,
143
144
  operationalKindOf: () => operationalKindOf,
@@ -947,14 +948,20 @@ function parseEpc(uri) {
947
948
  function gdtiUri(companyPrefix, docType, serial) {
948
949
  return `urn:epc:id:gdti:${companyPrefix}.${docType}.${serial}`;
949
950
  }
951
+ function objectUri(namespace, objId) {
952
+ return underNamespace(namespace, "obj", objId);
953
+ }
950
954
  function bizTransactionUri(namespace, transId) {
955
+ return underNamespace(namespace, "bt", transId);
956
+ }
957
+ function underNamespace(namespace, marker, id) {
951
958
  const ns = namespace?.trim();
952
959
  if (!ns) return void 0;
953
- const id = String(transId);
954
- if (!id || id.includes("/") || id.includes(":")) return void 0;
955
- if (/^https?:\/\/[^/\s]+/.test(ns)) return `${ns.replace(/\/+$/, "")}/bt/${id}`;
960
+ const v = String(id);
961
+ if (!v || v.includes("/") || v.includes(":")) return void 0;
962
+ if (/^https?:\/\/[^/\s]+/.test(ns)) return `${ns.replace(/\/+$/, "")}/${marker}/${v}`;
956
963
  if (/^urn:epc(global)?:/.test(ns)) return void 0;
957
- if (/^urn:[^:\s]+/.test(ns)) return `${ns.replace(/:+$/, "")}:bt:${id}`;
964
+ if (/^urn:[^:\s]+/.test(ns)) return `${ns.replace(/:+$/, "")}:${marker}:${v}`;
958
965
  return void 0;
959
966
  }
960
967
  function header(type, eventTime, bizStep, opts) {
@@ -2526,9 +2533,20 @@ var TWIN_AXES = [
2526
2533
  * 아직 세우지 않은 것: **요금 구간**(TariffPeriod)과 **원단위**(EnPI). 둘 다 아직 아무도 만들지
2527
2534
  * 않는다 — 선언만 하면 개념 지도가 언제나 0 을 보여 주고, 그것은 결손처럼 읽힌다(§10 7단계의 일).
2528
2535
  */
2536
+ /*
2537
+ * **무엇이 이 항목을 가리키나** — `idField` 가 말한다.
2538
+ *
2539
+ * 수요 구간에는 `id` 가 없다. 그것이 결함은 아니다: 구간의 정체성은 **그 구간이 언제 시작했나**이고
2540
+ * (요금의 알갱이가 그 시각으로 정해진다) 커널도 그것으로 키를 만든다(`contract-projected-over:<startMs>`).
2541
+ *
2542
+ * 그런데 소비처가 `id`·`key`·`gtin` 을 **짐작**하고 있어서, 화면이 여섯 구간 모두를 「식별자 없음 —
2543
+ * 참조할 수 없는 항목」으로 보였다. 값은 실재하는데 가리킬 수 없다고 말한 것이다. 짐작을 없애고
2544
+ * 선언이 답하게 한다 — 축이 사는 자리(`path`)를 선언하는 것과 같은 이유다.
2545
+ */
2529
2546
  {
2530
2547
  axis: "demandWindows",
2531
2548
  path: "energy.closed",
2549
+ idField: "startMs",
2532
2550
  label: "twin.axis.demandWindows",
2533
2551
  kind: "instance",
2534
2552
  source: "state",
@@ -5430,6 +5448,40 @@ var FlowEngine = class {
5430
5448
  identityGroundingView() {
5431
5449
  return identityGroundingOf(void 0);
5432
5450
  }
5451
+ /**
5452
+ * **이 트윈의 정체성 선언을 읽는 한 곳.**
5453
+ *
5454
+ * 모델이 갖는다(§`TwinModelDef.identity`) — 정체성은 생산과 무관하고, 생산하지 않는 트윈도 팔레트·
5455
+ * 트레일러를 식별한다. 선언을 든 커널이 자기 것을 먼저 쓰도록 override 할 수 있다(MES 가 그렇게 한다:
5456
+ * `ProductionSpec.identity` 가 먼저 생긴 자리다).
5457
+ *
5458
+ * 규칙을 두 곳에 적지 않기 위해 조립하는 쪽은 전부 이 함수를 지난다.
5459
+ */
5460
+ identityDeclaration() {
5461
+ return this.boardDef?.identity;
5462
+ }
5463
+ /**
5464
+ * 선언된 이름공간 아래의 **개체 식별자** — 없으면 답하지 않는다.
5465
+ *
5466
+ * 팔레트·트레일러는 개체 식별자이고 GS1 키(SSCC·GRAI)에는 회사 프리픽스가 필요하다. 프리픽스가 없는
5467
+ * 현장은 CBV §8.2.3·§8.2.4 의 길로 간다. 선언이 없으면 **지어내지 않고 `undefined` 를 답한다** —
5468
+ * 호출부가 무엇을 할지 정한다(레거시 경로는 아직 상수를 쓴다).
5469
+ */
5470
+ declaredObjectId(id) {
5471
+ for (const ns of this.identityDeclaration()?.namespaces ?? []) {
5472
+ const uri = objectUri(ns, id);
5473
+ if (uri) return uri;
5474
+ }
5475
+ return void 0;
5476
+ }
5477
+ /** 선언된 이름공간 아래의 **거래 문서 식별자**(발주·주문·어포인트먼트) — 없으면 답하지 않는다. */
5478
+ declaredBizTransactionId(id) {
5479
+ for (const ns of this.identityDeclaration()?.namespaces ?? []) {
5480
+ const uri = bizTransactionUri(ns, id);
5481
+ if (uri) return uri;
5482
+ }
5483
+ return void 0;
5484
+ }
5433
5485
  produceMaterials(t) {
5434
5486
  if (this.producesOwnOutputs(t.kind)) return;
5435
5487
  const made = (this.operationSpecs.get(t.kind)?.materialSpecification ?? []).filter((m) => m.use === "produced");
@@ -6073,11 +6125,13 @@ var WmsKernel = class extends FlowEngine {
6073
6125
  /** 입고 도착 — §4 라이프사이클: ASN(PO) → 팔레트 조립 → 수령 → putaway task. */
6074
6126
  onArrival(spec) {
6075
6127
  const dock = this.builtInLocation("dock", "inbound pallets land here");
6076
- const epc = ssccUri(LEGACY_COMPANY_PREFIX, ++this.epcSeq);
6128
+ const seq = ++this.epcSeq;
6129
+ const epc = this.declaredObjectId(`PAL-${seq}`) ?? ssccUri(LEGACY_COMPANY_PREFIX, seq);
6077
6130
  const gtin = this.pickGtin(spec.content.skuMix);
6078
6131
  if (!gtin) return;
6079
6132
  const qty = this.randInt(spec.content.qtyPerLine.min, spec.content.qtyPerLine.max);
6080
- const po = gdtiUri(LEGACY_COMPANY_PREFIX, "401", ++this.poSeq);
6133
+ const poSeq = ++this.poSeq;
6134
+ const po = this.declaredBizTransactionId(`PO-${poSeq}`) ?? gdtiUri(LEGACY_COMPANY_PREFIX, this.identityDeclaration()?.documentTypes?.purchaseorder ?? "401", poSeq);
6081
6135
  const eventTime = this.now();
6082
6136
  const qtyList = [{ epcClass: gtin, quantity: qty }];
6083
6137
  const poTxn = [{ type: BTT.po, bizTransaction: po }];
@@ -6148,7 +6202,8 @@ var WmsKernel = class extends FlowEngine {
6148
6202
  /** 오더 생성 — 약속(납기·우선순위)은 base `promiseOf` 가 계산한다(도메인마다 다르게 재지 않는다). */
6149
6203
  createSalesOrder(lines, spec) {
6150
6204
  const id = `order-${++this.orderSeq}`;
6151
- const so = gdtiUri(LEGACY_COMPANY_PREFIX, "402", ++this.soSeq);
6205
+ const soSeq = ++this.soSeq;
6206
+ const so = this.declaredBizTransactionId(`SO-${soSeq}`) ?? gdtiUri(LEGACY_COMPANY_PREFIX, this.identityDeclaration()?.documentTypes?.salesorder ?? "402", soSeq);
6152
6207
  const requested = lines.reduce((s, l) => s + l.qty, 0);
6153
6208
  const order = {
6154
6209
  id,
@@ -6321,7 +6376,8 @@ var WmsKernel = class extends FlowEngine {
6321
6376
  const qty = row.qty ?? 0;
6322
6377
  if (qty <= 0) continue;
6323
6378
  this.items.delete(key);
6324
- const pallet = ssccUri(LEGACY_COMPANY_PREFIX, ++this.epcSeq);
6379
+ const palSeq = ++this.epcSeq;
6380
+ const pallet = this.declaredObjectId(`PAL-${palSeq}`) ?? ssccUri(LEGACY_COMPANY_PREFIX, palSeq);
6325
6381
  const qtyList = [{ epcClass: m.definitionId, quantity: qty }];
6326
6382
  this.items.set(pallet, { epc: pallet, gtin: m.definitionId, qty, location: at, disposition: DISP.in_progress });
6327
6383
  const eventTime = this.now();
@@ -6335,7 +6391,8 @@ var WmsKernel = class extends FlowEngine {
6335
6391
  finalizeOrder(order, staging) {
6336
6392
  const shipDock = this.locationByType("dock-ship") ?? staging;
6337
6393
  const eventTime = this.now();
6338
- const shipment = ssccUri(LEGACY_COMPANY_PREFIX, ++this.epcSeq);
6394
+ const shpSeq = ++this.epcSeq;
6395
+ const shipment = this.declaredObjectId(`SHP-${shpSeq}`) ?? ssccUri(LEGACY_COMPANY_PREFIX, shpSeq);
6339
6396
  order.shipmentEpc = shipment;
6340
6397
  const soTxn = [{ type: BTT.so, bizTransaction: order.bizTransaction }];
6341
6398
  this.aggregate(shipment, order.picked.slice(), { bizStep: BIZSTEP.packing, readPoint: staging.id, bizLocation: staging.id });
@@ -6398,18 +6455,23 @@ var YmsKernel = class extends FlowEngine {
6398
6455
  const doors = [...this.locations.values()].filter((n) => n.type === "dock-door").sort((a, b) => a.id.localeCompare(b.id));
6399
6456
  if (!gate || doors.length === 0) return;
6400
6457
  const inbound = kind === "appointment";
6401
- const epc = graiUri(LEGACY_CP, "10", ++this.epcSeq);
6458
+ const trSeq = ++this.epcSeq;
6459
+ const epc = this.declaredObjectId(`TRL-${trSeq}`) ?? graiUri(LEGACY_CP, this.identityDeclaration()?.documentTypes?.trailer ?? "10", trSeq);
6402
6460
  this.items.set(epc, { epc, location: gate.id, disposition: DISP.in_progress });
6403
6461
  gate.occupancy++;
6404
6462
  this.emit(objectEvent({ eventTime: this.now(), action: "ADD", bizStep: YARD_BIZSTEP.arriving, disposition: DISP.in_progress, epcList: [epc], readPoint: gate.id, bizLocation: gate.id }));
6405
6463
  if (inbound) {
6406
- const cargo = Array.from({ length: CARGO_PER_TRAILER }, () => ssccUri(LEGACY_CP, ++this.cargoSeq));
6464
+ const cargo = Array.from({ length: CARGO_PER_TRAILER }, () => {
6465
+ const c = ++this.cargoSeq;
6466
+ return this.declaredObjectId(`CGO-${c}`) ?? ssccUri(LEGACY_CP, c);
6467
+ });
6407
6468
  this.trailerCargo.set(epc, cargo);
6408
6469
  this.aggregate(epc, cargo, { bizStep: YARD_BIZSTEP.arriving, readPoint: gate.id });
6409
6470
  }
6410
6471
  const door = doors[this.doorRR++ % doors.length];
6411
6472
  const id = `order-${++this.orderSeq}`;
6412
- const appt = gdtiUri(LEGACY_CP, "404", ++this.soSeq);
6473
+ const apSeq = ++this.soSeq;
6474
+ const appt = this.declaredBizTransactionId(`APPT-${apSeq}`) ?? gdtiUri(LEGACY_CP, this.identityDeclaration()?.documentTypes?.appointment ?? "404", apSeq);
6413
6475
  const order = {
6414
6476
  id,
6415
6477
  kind,
@@ -6602,6 +6664,15 @@ var MesKernel = class extends FlowEngine {
6602
6664
  * 아니다). 선언이 없는 MES 트윈은 이제 만들 수 없으므로(`loadTwinModel` 이 거절한다) `fabricated` 로
6603
6665
  * 판정될 일이 없다.
6604
6666
  */
6667
+ /**
6668
+ * MES 는 **생산 선언의 정체성을 먼저** 쓴다 — `ProductionSpec.identity` 가 먼저 생긴 자리다.
6669
+ *
6670
+ * 모델 자리(§`TwinModelDef.identity`)로 옮기면 이 override 는 사라진다. 두 자리를 동시에 읽는 것이
6671
+ * 아니라 **하나를 고르는** 것이므로 규칙이 갈리지 않는다.
6672
+ */
6673
+ identityDeclaration() {
6674
+ return this.productionSpec?.identity ?? super.identityDeclaration();
6675
+ }
6605
6676
  identityGroundingView() {
6606
6677
  return identityGroundingOf(this.productionSpec);
6607
6678
  }
@@ -8489,6 +8560,7 @@ function retiredVocabularyIn(line) {
8489
8560
  monteCarloForecast,
8490
8561
  monteCarloForecastAsync,
8491
8562
  objectEvent,
8563
+ objectUri,
8492
8564
  offCalendarAt,
8493
8565
  offCalendarReasonAt,
8494
8566
  operationalKindOf,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/twin-kernel",
3
- "version": "0.7.43",
3
+ "version": "0.7.44",
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": {