@operato/twin-kernel 0.7.43 → 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.
@@ -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;
@@ -2229,6 +2242,19 @@ export interface IdentityDeclaration {
2229
2242
  * 키는 문서의 종류(예: `workorder`), 값은 그 현장이 배정한 문서 타입이다.
2230
2243
  */
2231
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;
2232
2258
  }
2233
2259
  /**
2234
2260
  * 정체성의 **근거** — 값이 어디서 왔나. 「우리가 구현했나」와 **다른 축**이다.
@@ -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
  *
@@ -89,7 +100,7 @@ export interface TwinAxisInfo {
89
100
  *
90
101
  * **`source` 가 가리키는 자료 안의 경로다** — 예전에는 「`document` 일 때만 있다」고 못 박았는데,
91
102
  * 상태 축도 중첩될 수 있다는 것이 에너지에서 드러났다(수요 구간은 `state.energy.closed` 에 산다).
92
- * 그때 축 이름을 상태의 최상위 키로 맞추려면 같은 배열을 두 자리에 실어야 했다(방송이 그만큼 커진다).
103
+ * 그때 축 이름을 상태의 최상위 키로 맞추려면 같은 배열을 두 자리에 실어야 했다(브로드캐스팅이 그만큼 커진다).
93
104
  *
94
105
  * 규칙은 하나다: **경로가 있으면 그 경로로 읽고, 없으면 축 이름으로 읽는다.** 빈 문자열로 두지
95
106
  * 않는다 — 소비처가 자료의 뿌리를 읽고 통째로 잘못된 답을 만든다.
@@ -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
  /** 관계 전체 — 지도의 선과 항목의 이웃이 여기서 나온다(화면은 이 목록을 갖지 않는다). */
@@ -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.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
@@ -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('.');
@@ -132,25 +145,50 @@ export function gdtiUri(companyPrefix, docType, serial) {
132
145
  * 이름공간의 모양으로 URL 형태와 URN 형태를 가른다. 판정할 수 없는 모양이면 **답하지 않는다**(지어내지
133
146
  * 않는다) — 호출부가 다른 길을 고르게 한다.
134
147
  */
148
+ /**
149
+ * **선언된 이름공간 아래의 개체 식별자** — SSCC·GRAI 를 쓰지 않는 길.
150
+ *
151
+ * 팔레트(SSCC)·트레일러(GRAI)는 개체 식별자이고, 그 조립에는 GS1 회사 프리픽스가 필요하다. 프리픽스가
152
+ * 없는 현장은 그 길로 갈 수 없다 — 그런데 커널이 프리픽스를 지어내면 저널에 남의 번호가 영구히 남는다.
153
+ *
154
+ * 표준이 다른 길을 정해 두었다.
155
+ * · CBV 2.0 §8.2.4 `http(s)://[Subdomain.]Domain/⁎⁎/obj/Objid` — 그 도메인 소유자가 배정
156
+ * · CBV 2.0 §8.2.3 `urn:URNNamespace:⁎⁎:obj:Objid` — URN 이름공간 소유자가 배정
157
+ *
158
+ * `obj` 표지가 필수다(클래스의 `class`·거래문서의 `bt` 와 같은 구조다). 다만 표준은 EPC URI 나 Digital
159
+ * Link 를 **권한다**(SHOULD) — 이 길은 프리픽스가 없을 때의 정합 경로다.
160
+ */
161
+ export function objectUri(namespace, objId) {
162
+ return underNamespace(namespace, 'obj', objId);
163
+ }
135
164
  export function bizTransactionUri(namespace, transId) {
165
+ return underNamespace(namespace, 'bt', transId);
166
+ }
167
+ /**
168
+ * 선언된 이름공간 아래에 표지를 붙여 식별자를 만든다 — 표지만 다르고 규칙은 같다.
169
+ *
170
+ * `obj`(개체 §8.2.3·§8.2.4) · `class`(클래스 §8.3.3·§8.3.4) · `bt`(거래문서 §8.5.4·§8.5.5) 가 같은
171
+ * 모양이다. 규칙을 세 곳에 적으면 한 곳만 고쳐지는 날이 온다.
172
+ */
173
+ function underNamespace(namespace, marker, id) {
136
174
  const ns = namespace?.trim();
137
175
  if (!ns)
138
176
  return undefined;
139
- const id = String(transId);
140
- /* `transID` 에 구분자가 들어가면 표준이 요구하는 「성분 하나」가 깨진다. */
141
- if (!id || id.includes('/') || id.includes(':'))
177
+ const v = String(id);
178
+ /* 표준이 요구하는 「성분 하나」가 깨진다 — 구분자가 든 값은 만들지 않는다. */
179
+ if (!v || v.includes('/') || v.includes(':'))
142
180
  return undefined;
143
181
  if (/^https?:\/\/[^/\s]+/.test(ns))
144
- return `${ns.replace(/\/+$/, '')}/bt/${id}`;
182
+ return `${ns.replace(/\/+$/, '')}/${marker}/${v}`;
145
183
  /*
146
- * **GS1 이 소유한 URN 공간에는 우리가 `:bt:` 만들 수 없다.** `urn:epc:`·`urn:epcglobal:` 의
147
- * 소유 권한자는 GS1 이고(EPCIS §6.4), 그 안의 형태는 표준이 정해 둔 것만 유효하다. 그 공간을
148
- * 이름공간으로 선언한 현장은 GDTI 문서 타입을 선언하는 쪽으로 가야 한다.
184
+ * **GS1 이 소유한 URN 공간에는 우리가 표지를 만들 수 없다.** `urn:epc:`·`urn:epcglobal:` 의 소유
185
+ * 권한자는 GS1 이고(EPCIS §6.4), 그 안의 형태는 표준이 정해 둔 것만 유효하다. 그 공간을 이름공간으로
186
+ * 선언한 현장은 GS1 키(SSCC·GRAI·GDTI)를 쓰는 쪽으로 가야 한다.
149
187
  */
150
188
  if (/^urn:epc(global)?:/.test(ns))
151
189
  return undefined;
152
190
  if (/^urn:[^:\s]+/.test(ns))
153
- return `${ns.replace(/:+$/, '')}:bt:${id}`;
191
+ return `${ns.replace(/:+$/, '')}:${marker}:${v}`;
154
192
  return undefined;
155
193
  }
156
194
  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,38 @@ 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
+ * 개체 식별자 — **선언이 없으면 오류를 낸다.**
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;
1286
+ /** 선언된 이름공간 아래의 **거래 문서 식별자**(발주·주문·어포인트먼트) — 없으면 답하지 않는다. */
1287
+ protected declaredBizTransactionId(id: string | number): string | undefined;
1256
1288
  private produceMaterials;
1257
1289
  /**
1258
1290
  * 이 작업이 **딛고 선 것**이 아직 있나 — 없으면 무엇이 없는지 답한다.
@@ -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, 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())
@@ -2578,6 +2578,75 @@ 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
+ * 개체 식별자 — **선언이 없으면 오류를 낸다.**
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
+ }
2641
+ /** 선언된 이름공간 아래의 **거래 문서 식별자**(발주·주문·어포인트먼트) — 없으면 답하지 않는다. */
2642
+ declaredBizTransactionId(id) {
2643
+ for (const ns of this.identityDeclaration()?.namespaces ?? []) {
2644
+ const uri = bizTransactionUri(ns, id);
2645
+ if (uri)
2646
+ return uri;
2647
+ }
2648
+ return undefined;
2649
+ }
2581
2650
  produceMaterials(t) {
2582
2651
  /* 도메인이 소유를 주장하면 코어는 비켜선다 — 둘이 만들면 재고가 두 배가 된다. */
2583
2652
  if (this.producesOwnOutputs(t.kind))
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 {
@@ -57,13 +45,17 @@ export class WmsKernel extends FlowEngine {
57
45
  /** 입고 도착 — §4 라이프사이클: ASN(PO) → 팔레트 조립 → 수령 → putaway task. */
58
46
  onArrival(spec) {
59
47
  const dock = this.builtInLocation('dock', 'inbound pallets land here');
60
- const epc = ssccUri(LEGACY_COMPANY_PREFIX, ++this.epcSeq); // 팔레트 SSCC
48
+ /* 선언이 있으면 선언에서(CBV §8.2.4) 없으면 레거시 상수. 가드가 그 상수를 계속 센다. */
49
+ const seq = ++this.epcSeq;
50
+ const epc = this.requireObjectId(`PAL-${seq}`); // 팔레트
61
51
  const gtin = this.pickGtin(spec.content.skuMix); // SGTIN idpat = epcClass
62
52
  /* 품목 구성이 비어 있으면 **도착을 만들지 않는다** — 무엇이 왔는지 말할 수 없는 입고는 사실이 아니다. */
63
53
  if (!gtin)
64
54
  return;
65
55
  const qty = this.randInt(spec.content.qtyPerLine.min, spec.content.qtyPerLine.max); // 케이스 수(비직렬)
66
- const po = gdtiUri(LEGACY_COMPANY_PREFIX, '401', ++this.poSeq);
56
+ const poSeq = ++this.poSeq;
57
+ /* 발주 문서 식별자 — 선언된 이름공간 아래(CBV §8.5.5) 또는 선언된 GDTI 문서 타입. */
58
+ const po = this.requireBizTransactionId(`PO-${poSeq}`, 'purchaseorder');
67
59
  const eventTime = this.now();
68
60
  const qtyList = [{ epcClass: gtin, quantity: qty }];
69
61
  const poTxn = [{ type: BTT.po, bizTransaction: po }];
@@ -137,7 +129,8 @@ export class WmsKernel extends FlowEngine {
137
129
  /** 오더 생성 — 약속(납기·우선순위)은 base `promiseOf` 가 계산한다(도메인마다 다르게 재지 않는다). */
138
130
  createSalesOrder(lines, spec) {
139
131
  const id = `order-${++this.orderSeq}`;
140
- const so = gdtiUri(LEGACY_COMPANY_PREFIX, '402', ++this.soSeq);
132
+ const soSeq = ++this.soSeq;
133
+ const so = this.requireBizTransactionId(`SO-${soSeq}`, 'salesorder');
141
134
  const requested = lines.reduce((s, l) => s + l.qty, 0);
142
135
  const order = {
143
136
  id, kind: 'outbound', status: 'created', requested, fulfilled: 0, bizTransaction: so,
@@ -349,7 +342,8 @@ export class WmsKernel extends FlowEngine {
349
342
  continue;
350
343
  /* 클래스 줄을 팔레트로 바꾼다 — 자리 점유는 줄 하나에서 줄 하나로(늘지 않는다). */
351
344
  this.items.delete(key);
352
- const pallet = ssccUri(LEGACY_COMPANY_PREFIX, ++this.epcSeq);
345
+ const palSeq = ++this.epcSeq;
346
+ const pallet = this.requireObjectId(`PAL-${palSeq}`);
353
347
  const qtyList = [{ epcClass: m.definitionId, quantity: qty }];
354
348
  this.items.set(pallet, { epc: pallet, gtin: m.definitionId, qty, location: at, disposition: DISP.in_progress });
355
349
  const eventTime = this.now();
@@ -366,7 +360,8 @@ export class WmsKernel extends FlowEngine {
366
360
  창고의 모양이다(도크 단계가 없는 현장이 있다). 그 사실은 화물의 출발 자리로 저널에 남는다. */
367
361
  const shipDock = this.locationByType('dock-ship') ?? staging;
368
362
  const eventTime = this.now();
369
- const shipment = ssccUri(LEGACY_COMPANY_PREFIX, ++this.epcSeq);
363
+ const shpSeq = ++this.epcSeq;
364
+ const shipment = this.requireObjectId(`SHP-${shpSeq}`);
370
365
  order.shipmentEpc = shipment;
371
366
  const soTxn = [{ type: BTT.so, bizTransaction: order.bizTransaction }];
372
367
  // 패킹: 화물(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
  }
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;
@@ -64,19 +58,25 @@ export class YmsKernel extends FlowEngine {
64
58
  if (!gate || doors.length === 0)
65
59
  return;
66
60
  const inbound = kind === 'appointment';
67
- const epc = graiUri(LEGACY_CP, '10', ++this.epcSeq);
61
+ const trSeq = ++this.epcSeq;
62
+ /* 선언이 있으면 선언에서(CBV §8.2.4) — 없으면 레거시 상수. */
63
+ const epc = this.requireObjectId(`TRL-${trSeq}`);
68
64
  this.items.set(epc, { epc, location: gate.id, disposition: DISP.in_progress });
69
65
  gate.occupancy++;
70
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 }));
71
67
  // 인바운드: 화물 적재된 채 도착 → 조립(트레일러←화물) 기록. 아웃바운드: 빈 트레일러(화물은 도크에서 적재).
72
68
  if (inbound) {
73
- const cargo = Array.from({ length: CARGO_PER_TRAILER }, () => ssccUri(LEGACY_CP, ++this.cargoSeq));
69
+ const cargo = Array.from({ length: CARGO_PER_TRAILER }, () => {
70
+ const c = ++this.cargoSeq;
71
+ return this.requireObjectId(`CGO-${c}`);
72
+ });
74
73
  this.trailerCargo.set(epc, cargo);
75
74
  this.aggregate(epc, cargo, { bizStep: YARD_BIZSTEP.arriving, readPoint: gate.id }); // 적재된 채 도착(자식 opaque, 미materialize)
76
75
  }
77
76
  const door = doors[this.doorRR++ % doors.length];
78
77
  const id = `order-${++this.orderSeq}`;
79
- const appt = gdtiUri(LEGACY_CP, '404', ++this.soSeq);
78
+ const apSeq = ++this.soSeq;
79
+ const appt = this.requireBizTransactionId(`APPT-${apSeq}`, 'appointment');
80
80
  const order = { id, kind, status: 'created', requested: 1, fulfilled: 0, bizTransaction: appt, allocated: [epc], picked: [], dockDoor: door.id,
81
81
  /* 내부 스케줄 게이트(시뮬 ms) — 표준 `EarliestStartTime` 과 같은 개념의 우리 단위. */
82
82
  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,
@@ -913,6 +914,8 @@ function lgtinClass(companyPrefix, itemRefAndIndicator, lot) {
913
914
  }
914
915
  function parseEpc(uri) {
915
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 };
916
919
  const cls = raw.match(/^urn:epc:class:lgtin:(.+)$/);
917
920
  if (cls) {
918
921
  const seg = cls[1].split(".");
@@ -947,14 +950,20 @@ function parseEpc(uri) {
947
950
  function gdtiUri(companyPrefix, docType, serial) {
948
951
  return `urn:epc:id:gdti:${companyPrefix}.${docType}.${serial}`;
949
952
  }
953
+ function objectUri(namespace, objId) {
954
+ return underNamespace(namespace, "obj", objId);
955
+ }
950
956
  function bizTransactionUri(namespace, transId) {
957
+ return underNamespace(namespace, "bt", transId);
958
+ }
959
+ function underNamespace(namespace, marker, id) {
951
960
  const ns = namespace?.trim();
952
961
  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}`;
962
+ const v = String(id);
963
+ if (!v || v.includes("/") || v.includes(":")) return void 0;
964
+ if (/^https?:\/\/[^/\s]+/.test(ns)) return `${ns.replace(/\/+$/, "")}/${marker}/${v}`;
956
965
  if (/^urn:epc(global)?:/.test(ns)) return void 0;
957
- if (/^urn:[^:\s]+/.test(ns)) return `${ns.replace(/:+$/, "")}:bt:${id}`;
966
+ if (/^urn:[^:\s]+/.test(ns)) return `${ns.replace(/:+$/, "")}:${marker}:${v}`;
958
967
  return void 0;
959
968
  }
960
969
  function header(type, eventTime, bizStep, opts) {
@@ -2526,9 +2535,20 @@ var TWIN_AXES = [
2526
2535
  * 아직 세우지 않은 것: **요금 구간**(TariffPeriod)과 **원단위**(EnPI). 둘 다 아직 아무도 만들지
2527
2536
  * 않는다 — 선언만 하면 개념 지도가 언제나 0 을 보여 주고, 그것은 결손처럼 읽힌다(§10 7단계의 일).
2528
2537
  */
2538
+ /*
2539
+ * **무엇이 이 항목을 가리키나** — `idField` 가 말한다.
2540
+ *
2541
+ * 수요 구간에는 `id` 가 없다. 그것이 결함은 아니다: 구간의 정체성은 **그 구간이 언제 시작했나**이고
2542
+ * (요금의 알갱이가 그 시각으로 정해진다) 커널도 그것으로 키를 만든다(`contract-projected-over:<startMs>`).
2543
+ *
2544
+ * 그런데 소비처가 `id`·`key`·`gtin` 을 **짐작**하고 있어서, 화면이 여섯 구간 모두를 「식별자 없음 —
2545
+ * 참조할 수 없는 항목」으로 보였다. 값은 실재하는데 가리킬 수 없다고 말한 것이다. 짐작을 없애고
2546
+ * 선언이 답하게 한다 — 축이 사는 자리(`path`)를 선언하는 것과 같은 이유다.
2547
+ */
2529
2548
  {
2530
2549
  axis: "demandWindows",
2531
2550
  path: "energy.closed",
2551
+ idField: "startMs",
2532
2552
  label: "twin.axis.demandWindows",
2533
2553
  kind: "instance",
2534
2554
  source: "state",
@@ -3002,7 +3022,7 @@ var TwinRuntime = class {
3002
3022
  resync() {
3003
3023
  return { revision: this.currentRevision, state: this.kernel.getSnapshot() };
3004
3024
  }
3005
- /** 클록-싱크 하트비트 메시지 — host 가 자기 send-rate 로 방송(⊥ tick). 클라 보간의 nowSim. */
3025
+ /** 클록-싱크 하트비트 메시지 — host 가 자기 send-rate 로 브로드캐스팅(⊥ tick). 클라 보간의 nowSim. */
3006
3026
  nowClock() {
3007
3027
  return { kind: "clock", revision: this.currentRevision, simClockMs: this.kernel.getSnapshot().simClockMs };
3008
3028
  }
@@ -5430,6 +5450,66 @@ var FlowEngine = class {
5430
5450
  identityGroundingView() {
5431
5451
  return identityGroundingOf(void 0);
5432
5452
  }
5453
+ /**
5454
+ * **이 트윈의 정체성 선언을 읽는 한 곳.**
5455
+ *
5456
+ * 모델이 갖는다(§`TwinModelDef.identity`) — 정체성은 생산과 무관하고, 생산하지 않는 트윈도 팔레트·
5457
+ * 트레일러를 식별한다. 선언을 든 커널이 자기 것을 먼저 쓰도록 override 할 수 있다(MES 가 그렇게 한다:
5458
+ * `ProductionSpec.identity` 가 먼저 생긴 자리다).
5459
+ *
5460
+ * 규칙을 두 곳에 적지 않기 위해 조립하는 쪽은 전부 이 함수를 지난다.
5461
+ */
5462
+ identityDeclaration() {
5463
+ return this.boardDef?.identity;
5464
+ }
5465
+ /**
5466
+ * 선언된 이름공간 아래의 **개체 식별자** — 없으면 답하지 않는다.
5467
+ *
5468
+ * 팔레트·트레일러는 개체 식별자이고 GS1 키(SSCC·GRAI)에는 회사 프리픽스가 필요하다. 프리픽스가 없는
5469
+ * 현장은 CBV §8.2.3·§8.2.4 의 길로 간다. 선언이 없으면 **지어내지 않고 `undefined` 를 답한다** —
5470
+ * 호출부가 무엇을 할지 정한다(레거시 경로는 아직 상수를 쓴다).
5471
+ */
5472
+ declaredObjectId(id) {
5473
+ for (const ns of this.identityDeclaration()?.namespaces ?? []) {
5474
+ const uri = objectUri(ns, id);
5475
+ if (uri) return uri;
5476
+ }
5477
+ return void 0;
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
+ }
5505
+ /** 선언된 이름공간 아래의 **거래 문서 식별자**(발주·주문·어포인트먼트) — 없으면 답하지 않는다. */
5506
+ declaredBizTransactionId(id) {
5507
+ for (const ns of this.identityDeclaration()?.namespaces ?? []) {
5508
+ const uri = bizTransactionUri(ns, id);
5509
+ if (uri) return uri;
5510
+ }
5511
+ return void 0;
5512
+ }
5433
5513
  produceMaterials(t) {
5434
5514
  if (this.producesOwnOutputs(t.kind)) return;
5435
5515
  const made = (this.operationSpecs.get(t.kind)?.materialSpecification ?? []).filter((m) => m.use === "produced");
@@ -6039,7 +6119,6 @@ function matches(s, req) {
6039
6119
 
6040
6120
  // src/kernel.ts
6041
6121
  var TRAVEL_MS = 3e4;
6042
- var LEGACY_COMPANY_PREFIX = "0614141";
6043
6122
  var SHELF_MS = 30 * 24 * 36e5;
6044
6123
  var SHELF_JITTER_MS = 5 * 24 * 36e5;
6045
6124
  var WmsKernel = class extends FlowEngine {
@@ -6073,11 +6152,13 @@ var WmsKernel = class extends FlowEngine {
6073
6152
  /** 입고 도착 — §4 라이프사이클: ASN(PO) → 팔레트 조립 → 수령 → putaway task. */
6074
6153
  onArrival(spec) {
6075
6154
  const dock = this.builtInLocation("dock", "inbound pallets land here");
6076
- const epc = ssccUri(LEGACY_COMPANY_PREFIX, ++this.epcSeq);
6155
+ const seq = ++this.epcSeq;
6156
+ const epc = this.requireObjectId(`PAL-${seq}`);
6077
6157
  const gtin = this.pickGtin(spec.content.skuMix);
6078
6158
  if (!gtin) return;
6079
6159
  const qty = this.randInt(spec.content.qtyPerLine.min, spec.content.qtyPerLine.max);
6080
- const po = gdtiUri(LEGACY_COMPANY_PREFIX, "401", ++this.poSeq);
6160
+ const poSeq = ++this.poSeq;
6161
+ const po = this.requireBizTransactionId(`PO-${poSeq}`, "purchaseorder");
6081
6162
  const eventTime = this.now();
6082
6163
  const qtyList = [{ epcClass: gtin, quantity: qty }];
6083
6164
  const poTxn = [{ type: BTT.po, bizTransaction: po }];
@@ -6148,7 +6229,8 @@ var WmsKernel = class extends FlowEngine {
6148
6229
  /** 오더 생성 — 약속(납기·우선순위)은 base `promiseOf` 가 계산한다(도메인마다 다르게 재지 않는다). */
6149
6230
  createSalesOrder(lines, spec) {
6150
6231
  const id = `order-${++this.orderSeq}`;
6151
- const so = gdtiUri(LEGACY_COMPANY_PREFIX, "402", ++this.soSeq);
6232
+ const soSeq = ++this.soSeq;
6233
+ const so = this.requireBizTransactionId(`SO-${soSeq}`, "salesorder");
6152
6234
  const requested = lines.reduce((s, l) => s + l.qty, 0);
6153
6235
  const order = {
6154
6236
  id,
@@ -6321,7 +6403,8 @@ var WmsKernel = class extends FlowEngine {
6321
6403
  const qty = row.qty ?? 0;
6322
6404
  if (qty <= 0) continue;
6323
6405
  this.items.delete(key);
6324
- const pallet = ssccUri(LEGACY_COMPANY_PREFIX, ++this.epcSeq);
6406
+ const palSeq = ++this.epcSeq;
6407
+ const pallet = this.requireObjectId(`PAL-${palSeq}`);
6325
6408
  const qtyList = [{ epcClass: m.definitionId, quantity: qty }];
6326
6409
  this.items.set(pallet, { epc: pallet, gtin: m.definitionId, qty, location: at, disposition: DISP.in_progress });
6327
6410
  const eventTime = this.now();
@@ -6335,7 +6418,8 @@ var WmsKernel = class extends FlowEngine {
6335
6418
  finalizeOrder(order, staging) {
6336
6419
  const shipDock = this.locationByType("dock-ship") ?? staging;
6337
6420
  const eventTime = this.now();
6338
- const shipment = ssccUri(LEGACY_COMPANY_PREFIX, ++this.epcSeq);
6421
+ const shpSeq = ++this.epcSeq;
6422
+ const shipment = this.requireObjectId(`SHP-${shpSeq}`);
6339
6423
  order.shipmentEpc = shipment;
6340
6424
  const soTxn = [{ type: BTT.so, bizTransaction: order.bizTransaction }];
6341
6425
  this.aggregate(shipment, order.picked.slice(), { bizStep: BIZSTEP.packing, readPoint: staging.id, bizLocation: staging.id });
@@ -6368,7 +6452,6 @@ var DWELL_MS = 3e4;
6368
6452
  var WINDOW_DELAY_MS = 4e4;
6369
6453
  var WINDOW_LENGTH_MS = 20 * 6e4;
6370
6454
  var CARGO_PER_TRAILER = 2;
6371
- var LEGACY_CP = "0614141";
6372
6455
  var YmsKernel = class extends FlowEngine {
6373
6456
  doorRR = 0;
6374
6457
  cargoSeq = 0;
@@ -6398,18 +6481,23 @@ var YmsKernel = class extends FlowEngine {
6398
6481
  const doors = [...this.locations.values()].filter((n) => n.type === "dock-door").sort((a, b) => a.id.localeCompare(b.id));
6399
6482
  if (!gate || doors.length === 0) return;
6400
6483
  const inbound = kind === "appointment";
6401
- const epc = graiUri(LEGACY_CP, "10", ++this.epcSeq);
6484
+ const trSeq = ++this.epcSeq;
6485
+ const epc = this.requireObjectId(`TRL-${trSeq}`);
6402
6486
  this.items.set(epc, { epc, location: gate.id, disposition: DISP.in_progress });
6403
6487
  gate.occupancy++;
6404
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 }));
6405
6489
  if (inbound) {
6406
- const cargo = Array.from({ length: CARGO_PER_TRAILER }, () => ssccUri(LEGACY_CP, ++this.cargoSeq));
6490
+ const cargo = Array.from({ length: CARGO_PER_TRAILER }, () => {
6491
+ const c = ++this.cargoSeq;
6492
+ return this.requireObjectId(`CGO-${c}`);
6493
+ });
6407
6494
  this.trailerCargo.set(epc, cargo);
6408
6495
  this.aggregate(epc, cargo, { bizStep: YARD_BIZSTEP.arriving, readPoint: gate.id });
6409
6496
  }
6410
6497
  const door = doors[this.doorRR++ % doors.length];
6411
6498
  const id = `order-${++this.orderSeq}`;
6412
- const appt = gdtiUri(LEGACY_CP, "404", ++this.soSeq);
6499
+ const apSeq = ++this.soSeq;
6500
+ const appt = this.requireBizTransactionId(`APPT-${apSeq}`, "appointment");
6413
6501
  const order = {
6414
6502
  id,
6415
6503
  kind,
@@ -6602,6 +6690,15 @@ var MesKernel = class extends FlowEngine {
6602
6690
  * 아니다). 선언이 없는 MES 트윈은 이제 만들 수 없으므로(`loadTwinModel` 이 거절한다) `fabricated` 로
6603
6691
  * 판정될 일이 없다.
6604
6692
  */
6693
+ /**
6694
+ * MES 는 **생산 선언의 정체성을 먼저** 쓴다 — `ProductionSpec.identity` 가 먼저 생긴 자리다.
6695
+ *
6696
+ * 모델 자리(§`TwinModelDef.identity`)로 옮기면 이 override 는 사라진다. 두 자리를 동시에 읽는 것이
6697
+ * 아니라 **하나를 고르는** 것이므로 규칙이 갈리지 않는다.
6698
+ */
6699
+ identityDeclaration() {
6700
+ return this.productionSpec?.identity ?? super.identityDeclaration();
6701
+ }
6605
6702
  identityGroundingView() {
6606
6703
  return identityGroundingOf(this.productionSpec);
6607
6704
  }
@@ -7042,7 +7139,7 @@ var EmsKernel = class extends FlowEngine {
7042
7139
  closed = [];
7043
7140
  closedTotal = 0;
7044
7141
  peak;
7045
- /** 이 구간에서 이미 제안을 냈나 — 같은 사실을 되풀어 방송하지 않는다(라이브 브리지의 교훈). */
7142
+ /** 이 구간에서 이미 제안을 냈나 — 같은 사실을 되풀어 브로드캐스팅하지 않는다(라이브 브리지의 교훈). */
7046
7143
  suggestedFor;
7047
7144
  windowMs;
7048
7145
  /**
@@ -8489,6 +8586,7 @@ function retiredVocabularyIn(line) {
8489
8586
  monteCarloForecast,
8490
8587
  monteCarloForecastAsync,
8491
8588
  objectEvent,
8589
+ objectUri,
8492
8590
  offCalendarAt,
8493
8591
  offCalendarReasonAt,
8494
8592
  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.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": {