@operato/twin-kernel 0.2.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,14 +15,14 @@ export interface TaskDeltaRow {
15
15
  }
16
16
  /** 작업에 붙어 있는 축의 값 — 어느 전이에서 왔든 모은다. */
17
17
  export interface TaskFacets {
18
- /** 수행 자원(무버). 설계상 자원을 쓰지 않는 공정(체류)에서는 없는 것이 정상이다. */
18
+ /** 수행 자원(설비). 설계상 자원을 쓰지 않는 공정(체류)에서는 없는 것이 정상이다. */
19
19
  resource?: string;
20
20
  /** 작업 종류(커널 어휘 — 사람이 읽는 라벨은 표현 계층의 몫). */
21
21
  kind?: string;
22
22
  /** 소속 오더. **없으면 없는 것이다** — 다른 식별자로 대체하지 않는다. */
23
23
  order?: string;
24
24
  /** 가장 최근 도착 지점. 진행에 따라 바뀌므로 마지막 것이 사실이다. */
25
- node?: string;
25
+ location?: string;
26
26
  }
27
27
  /** 작업 하나의 이정표 — 소비처가 이것으로 구간·지표를 만든다. */
28
28
  export interface TaskRecord {
package/dist/task-fold.js CHANGED
@@ -38,7 +38,7 @@ export function foldTaskRecords(rows) {
38
38
  rec = { taskId: id, facets: {} };
39
39
  byTask.set(id, rec);
40
40
  }
41
- /* 축의 값 — 처음 본 것을 남긴다(작업의 종류·오더·자원은 바뀌지 않는다). 노드는 예외로 마지막 것. */
41
+ /* 축의 값 — 처음 본 것을 남긴다(작업의 종류·오더·자원은 바뀌지 않는다). 자리는 예외로 마지막 것. */
42
42
  const f = rec.facets;
43
43
  if (f.resource === undefined && d.resourceRef)
44
44
  f.resource = d.resourceRef;
@@ -46,9 +46,9 @@ export function foldTaskRecords(rows) {
46
46
  f.kind = d.kind;
47
47
  if (f.order === undefined && d.orderId)
48
48
  f.order = d.orderId;
49
- const node = d.toNode ?? d.fromNode;
50
- if (node)
51
- f.node = node;
49
+ const loc = d.toNode ?? d.fromNode;
50
+ if (loc)
51
+ f.location = loc;
52
52
  if (d.status === 'created')
53
53
  rec.createdMs = at;
54
54
  else if (d.status === 'in-progress')
@@ -0,0 +1,19 @@
1
+ /** 금지 어휘 — 표준 어휘로 대체된 옛 낱말. 파생 식별자까지 잡도록 부분 일치로 본다. */
2
+ export declare const RETIRED_VOCABULARY: readonly ["mover", "Mover", "MOVER", "node", "Node", "NODE"];
3
+ /**
4
+ * 예외 — 남아 있는 것이 **정당한** 토큰. 각 항목에 이유가 붙는다.
5
+ *
6
+ * 판정은 "이 토큰을 정확히 포함하는가" 다(부분 문자열). 그래서 `moverId` 를 허용하면 `moverIds` 도
7
+ * 통과한다 — 와이어 필드에서 파생한 변수까지 허용하려는 의도다.
8
+ */
9
+ export declare const VOCABULARY_EXCEPTIONS: {
10
+ token: string;
11
+ why: string;
12
+ }[];
13
+ /**
14
+ * 한 줄에서 금지 어휘 위반을 찾는다 — 예외에 걸리는 부분은 먼저 지워 놓고 본다.
15
+ *
16
+ * 반환은 위반 낱말 목록(중복 제거 전). 비어 있으면 그 줄은 통과.
17
+ */
18
+ export declare const GUARD_PRAGMA = "vocabulary-guard: allow";
19
+ export declare function retiredVocabularyIn(line: string): string[];
@@ -0,0 +1,66 @@
1
+ /*
2
+ * 어휘 가드 규칙 — **"다 찾았나" 를 사람의 grep 감각이 아니라 기계가 판정한다.**
3
+ *
4
+ * 계기(2026-08-01): `movers → equipment`·`nodes → locations` 개명을 손으로 쓸다가 같은 실수를 세 번
5
+ * 반복했다. 단어 경계로 찾으면 `moverCount`·`nodeType` 같은 파생 식별자가 빠지고, 빠진 것은 다음
6
+ * 파도에서야 드러난다. 그 사이 코드에는 두 어휘가 섞여 있고, 다음 사람은 어느 쪽을 믿을지 모른다.
7
+ *
8
+ * 그래서 규칙을 여기 한 곳에 두고, 각 패키지의 가드 테스트가 **자기 소스를 훑는다**(파일 IO 는 커널
9
+ * 밖 — 커널은 zero-dep 유지). 규칙이 한 벌이므로 패키지마다 판정이 갈릴 수 없다.
10
+ *
11
+ * 예외는 **이유와 함께** 여기 적는다. 주석에만 적으면 다음 사람이 "원래 그런가 보다" 하고 넘긴다.
12
+ */
13
+ /** 금지 어휘 — 표준 어휘로 대체된 옛 낱말. 파생 식별자까지 잡도록 부분 일치로 본다. */
14
+ export const RETIRED_VOCABULARY = ['mover', 'Mover', 'MOVER', 'node', 'Node', 'NODE'];
15
+ /**
16
+ * 예외 — 남아 있는 것이 **정당한** 토큰. 각 항목에 이유가 붙는다.
17
+ *
18
+ * 판정은 "이 토큰을 정확히 포함하는가" 다(부분 문자열). 그래서 `moverId` 를 허용하면 `moverIds` 도
19
+ * 통과한다 — 와이어 필드에서 파생한 변수까지 허용하려는 의도다.
20
+ */
21
+ export const VOCABULARY_EXCEPTIONS = [
22
+ /* ── 저널 와이어 필드 — append-only 역사이므로 이름을 바꾸지 않는다 ───────
23
+ * 같은 사실이 시점에 따라 다른 키로 들어가면 낡은 이름보다 나쁘다. 개명은 이벤트 스키마
24
+ * 버저닝과 함께 다룬다. (개명 시점 실측: moverId 8,965행 · fromNode/toNode 9,679행) */
25
+ { token: 'moverId', why: 'journal wire field — renaming would mix two keys for one fact across history' },
26
+ { token: 'fromNode', why: 'journal wire field (task.status payload)' },
27
+ { token: 'toNode', why: 'journal wire field (task.status payload)' },
28
+ /* ── 씬 컴포넌트 타입 — 보드에 저장된 값이고, 뜻이 어긋나지도 않는다 ──────
29
+ * 보드 7개가 이 타입으로 컴포넌트를 담고 있어 개명하면 그 컴포넌트가 조용히 안 그려진다.
30
+ * 그리고 씬에서 이 이름은 자원이 아니라 **움직임 표현**을 가리킨다(표준과 충돌 아님). */
31
+ { token: 'twin-mover', why: 'scene component type persisted in boards; names a motion representation, not a resource' },
32
+ { token: 'TwinMover', why: 'class backing the persisted scene component type above' },
33
+ /* ── 플랫폼·언어 어휘 — 우리 도메인 낱말이 아니다 ─────────────────────── */
34
+ { token: 'node_modules', why: 'npm path' },
35
+ { token: 'node:', why: 'Node.js builtin module specifier' },
36
+ { token: 'nodemon', why: 'tool name' },
37
+ { token: 'Node.js', why: 'platform name' },
38
+ { token: 'NodeJS', why: 'TypeScript global namespace' },
39
+ { token: 'nodeName', why: 'DOM API' },
40
+ { token: 'childNodes', why: 'DOM API' },
41
+ { token: 'appendChild', why: 'DOM API (no match, kept for clarity)' }
42
+ ];
43
+ /**
44
+ * 한 줄에서 금지 어휘 위반을 찾는다 — 예외에 걸리는 부분은 먼저 지워 놓고 본다.
45
+ *
46
+ * 반환은 위반 낱말 목록(중복 제거 전). 비어 있으면 그 줄은 통과.
47
+ */
48
+ export const GUARD_PRAGMA = 'vocabulary-guard: allow';
49
+ export function retiredVocabularyIn(line) {
50
+ /* 줄 단위 예외 — 옛 키를 **정당하게** 언급해야 하는 곳(읽기 호환 함수·개명 경위 주석)이 있다.
51
+ 그때는 그 줄에 `vocabulary-guard: allow <이유>` 를 남긴다. 예외가 코드에 이유와 함께 남는다. */
52
+ if (line.includes(GUARD_PRAGMA))
53
+ return [];
54
+ let rest = line;
55
+ for (const { token } of VOCABULARY_EXCEPTIONS)
56
+ rest = rest.split(token).join(' ');
57
+ const hits = [];
58
+ /* **대소문자를 구별한다.** `gi` 로 두면 `removeRep` 의 `moveR` 이 `mover` 로 잡힌다(실제 오탐).
59
+ 그래서 실제로 쓰이는 표기만 나열하고 정확히 그 표기를 찾는다. */
60
+ for (const word of RETIRED_VOCABULARY) {
61
+ const re = new RegExp(`[A-Za-z_]*${word}[A-Za-z_]*`, 'g');
62
+ for (const m of rest.matchAll(re))
63
+ hits.push(m[0]);
64
+ }
65
+ return hits;
66
+ }
@@ -13,8 +13,8 @@ export declare const BTT: {
13
13
  readonly so: "urn:epcglobal:cbv:btt:so";
14
14
  };
15
15
  /**
16
- * WMS 노드 타입 카탈로그 — 커널이 실제로 키로 쓰는 로케이션 타입(nodeByType/slotViews).
16
+ * WMS 자리 타입 카탈로그 — 커널이 실제로 키로 쓰는 로케이션 타입(locationByType/slotViews).
17
17
  * 도메인 어휘 SSOT: 호스트·UI 는 이걸 소싱하고 재선언하지 않는다(방언 금지).
18
18
  */
19
- export declare const WMS_NODE_TYPES: readonly ["dock", "storage", "staging", "dock-ship"];
19
+ export declare const WMS_LOCATION_TYPES: readonly ["dock", "storage", "staging", "dock-ship"];
20
20
  export declare const WMS_TYPES: TwinTypeInfo[];
@@ -14,16 +14,16 @@ export const BTT = {
14
14
  so: 'urn:epcglobal:cbv:btt:so'
15
15
  };
16
16
  /**
17
- * WMS 노드 타입 카탈로그 — 커널이 실제로 키로 쓰는 로케이션 타입(nodeByType/slotViews).
17
+ * WMS 자리 타입 카탈로그 — 커널이 실제로 키로 쓰는 로케이션 타입(locationByType/slotViews).
18
18
  * 도메인 어휘 SSOT: 호스트·UI 는 이걸 소싱하고 재선언하지 않는다(방언 금지).
19
19
  */
20
- export const WMS_NODE_TYPES = ['dock', 'storage', 'staging', 'dock-ship'];
20
+ export const WMS_LOCATION_TYPES = ['dock', 'storage', 'staging', 'dock-ship'];
21
21
  /*
22
- * 트윈 타입 서술(ADR-0018 확장) — 노드 키는 WMS_NODE_TYPES 단일 출처에서 파생 + 무버 타입 추가.
23
- * WMS=EPCIS 도메인: 로케이션=bizLocation(SGLN), 무버(지게차)=추적 오브젝트/자산(GIAI). 능력은 씬 소유라 미포함.
22
+ * 트윈 타입 서술(ADR-0018 확장) — 자리 키는 WMS_LOCATION_TYPES 단일 출처에서 파생 + 설비 타입 추가.
23
+ * WMS=EPCIS 도메인: 로케이션=bizLocation(SGLN), 설비(지게차)=추적 오브젝트/자산(GIAI). 능력은 씬 소유라 미포함.
24
24
  */
25
25
  // label 은 언어 중립 i18n 키(twin.type.<key>) — 사람 언어는 표현계층이 렌더(design/plans/i18n.md L2).
26
26
  export const WMS_TYPES = [
27
- ...WMS_NODE_TYPES.map((k) => ({ key: k, role: 'node', label: `twin.type.${k}`, standardClass: { epcis: 'bizLocation' }, identity: { scheme: 'gs1:SGLN' }, capabilities: ['storable'] })),
28
- { key: 'forklift', role: 'mover', label: 'twin.type.forklift', standardClass: { epcis: 'object', iso55000: 'Asset' }, identity: { scheme: 'gs1:GIAI' }, capabilities: ['mobile', 'operable'] }
27
+ ...WMS_LOCATION_TYPES.map((k) => ({ key: k, role: 'location', label: `twin.type.${k}`, standardClass: { epcis: 'bizLocation' }, identity: { scheme: 'gs1:SGLN' }, capabilities: ['storable'] })),
28
+ { key: 'forklift', role: 'equipment', label: 'twin.type.forklift', standardClass: { epcis: 'object', iso55000: 'Asset' }, identity: { scheme: 'gs1:GIAI' }, capabilities: ['mobile', 'operable'] }
29
29
  ];
@@ -4,7 +4,7 @@
4
4
  * 아웃바운드(상차): 빈 트레일러 도착 → 야드 대기 → (창+도어+staging 화물) 도크 → 상차 → 적재 출차.
5
5
  *
6
6
  * ★ 검증 질문: base 가 "시간창 예약 스케줄링"을 흡수하는가?
7
- * → clock(base) + 노드 점유(도크도어 예약) + allocate hook(스케줄 게이트)로 표현 — base 변경 0.
7
+ * → clock(base) + 자리 점유(도크도어 예약) + allocate hook(스케줄 게이트)로 표현 — base 변경 0.
8
8
  *
9
9
  * 상·하차 대칭(AggregationEvent):
10
10
  * - 하차(inbound): 게이트-인에 트레일러←화물 조립(ADD), 도크에서 분해(DELETE) → 화물이 staging 에 등장(WMS receiving hand-off).
@@ -22,6 +22,16 @@ import { YARD_BIZSTEP, BTT_DELIVERY, graiUri } from "./yms-profile.js";
22
22
  const TRAVEL_MS = 20_000; // 야드 이동
23
23
  const DWELL_MS = 30_000; // 도크 체류(상·하차) — depart 이동에 folded
24
24
  const WINDOW_DELAY_MS = 40_000; // 도착 후 어포인트먼트 창까지(early arrival → 대기)
25
+ /**
26
+ * 어포인트먼트 **창 길이** — 창이 열린 뒤 이만큼 안에 도크에 들어가야 한다.
27
+ *
28
+ * 표준 대조(2026-08-02): 우리 `windowStartMs` 는 `OpSegmentRequirement.EarliestStartTime` 과 **같은
29
+ * 개념**이고 단위만 다르다(시뮬 ms). **없던 것은 창의 끝** — `LatestEndTime` 에 해당한다. 그래서
30
+ * 어포인트먼트도 납기를 갖게 되고, 야드에서도 "늦었나" 를 물을 수 있다.
31
+ *
32
+ * 창 길이는 표준이 정하지 않는다(현장 계약 사항) — 데모 기본값이고, 마스터가 말해 주면 그것을 쓴다.
33
+ */
34
+ const WINDOW_LENGTH_MS = 20 * 60_000;
25
35
  const CARGO_PER_TRAILER = 2; // 트레일러 적재/하역 화물(SSCC) 수
26
36
  const CP = '0614141';
27
37
  export class YmsKernel extends FlowEngine {
@@ -36,13 +46,13 @@ export class YmsKernel extends FlowEngine {
36
46
  this.mode = mode;
37
47
  }
38
48
  /** 인바운드(하차) 자극 — 화물 적재된 트레일러 도착. */
39
- onArrival(_spec) { this.spawnAppointment('appointment'); }
49
+ onArrival(_spec) { this.spawnAppointment('appointment', _spec); }
40
50
  /** 아웃바운드(상차) 자극 — 빈 트레일러 도착(도크에서 staging 화물 적재 예정). */
41
- onOrder(_spec) { this.spawnAppointment('appointment-out'); }
51
+ onOrder(_spec) { this.spawnAppointment('appointment-out', _spec); }
42
52
  /** 게이트-인 — 트레일러 도착 → 어포인트먼트(도크도어 배정 + 창). drop 은 야드 주차 태스크, live 는 게이트 대기. */
43
- spawnAppointment(kind) {
44
- const gate = this.nodeByType('gate');
45
- const doors = [...this.nodes.values()].filter(n => n.type === 'dock-door').sort((a, b) => a.id.localeCompare(b.id));
53
+ spawnAppointment(kind, spec) {
54
+ const gate = this.locationByType('gate');
55
+ const doors = [...this.locations.values()].filter(n => n.type === 'dock-door').sort((a, b) => a.id.localeCompare(b.id));
46
56
  if (!gate || doors.length === 0)
47
57
  return;
48
58
  const inbound = kind === 'appointment';
@@ -59,7 +69,14 @@ export class YmsKernel extends FlowEngine {
59
69
  const door = doors[this.doorRR++ % doors.length];
60
70
  const id = `order-${++this.orderSeq}`;
61
71
  const appt = gdtiUri(CP, '404', ++this.soSeq);
62
- const order = { id, kind, status: 'created', requested: 1, fulfilled: 0, bizTransaction: appt, allocated: [epc], picked: [], dockDoor: door.id, windowStartMs: this.clockMs + WINDOW_DELAY_MS };
72
+ const order = { id, kind, status: 'created', requested: 1, fulfilled: 0, bizTransaction: appt, allocated: [epc], picked: [], dockDoor: door.id,
73
+ /* 내부 스케줄 게이트(시뮬 ms) — 표준 `EarliestStartTime` 과 같은 개념의 우리 단위. */
74
+ windowStartMs: this.clockMs + WINDOW_DELAY_MS,
75
+ /* 표준 필드로도 함께 낸다 — 소비처(화면·집계·미러)는 표준 축으로 읽는다.
76
+ `startTime` = 창이 열리는 때 · `endTime` = 창이 닫히는 때(= 납기). */
77
+ startTime: new Date(Date.parse(this.now()) + WINDOW_DELAY_MS).toISOString(),
78
+ endTime: new Date(Date.parse(this.now()) + WINDOW_DELAY_MS + WINDOW_LENGTH_MS).toISOString(),
79
+ ...(spec?.priority !== undefined ? { priority: spec.priority } : {}) };
63
80
  this.orders.set(id, order);
64
81
  this.apptMode.set(id, this.mode);
65
82
  this.emitOrder(order);
@@ -85,18 +102,18 @@ export class YmsKernel extends FlowEngine {
85
102
  return;
86
103
  if (this.clockMs < (o.windowStartMs ?? 0))
87
104
  return; // 창 도래 전
88
- const door = this.nodes.get(o.dockDoor);
105
+ const door = this.locations.get(o.dockDoor);
89
106
  const inflight = [...this.tasks.values()].some(t => t.status !== 'completed' && t.toNode === door.id);
90
107
  if (door.occupancy > 0 || inflight)
91
108
  return; // 도어 사용중/예약됨 → 대기(직렬화)
92
109
  const mode = this.apptMode.get(o.id) ?? 'drop';
93
- if (mode === 'drop' && this.nodes.get(trailer.location)?.type !== 'yard-slot')
110
+ if (mode === 'drop' && this.locations.get(trailer.location)?.type !== 'yard-slot')
94
111
  return; // 아직 미주차
95
112
  const outbound = o.kind === 'appointment-out';
96
113
  const bizStep = outbound ? YARD_BIZSTEP.loading : YARD_BIZSTEP.unloading;
97
114
  if (outbound) {
98
115
  // 아웃바운드: staging 에 적재 화물(sellable, 미예약) 충분해야 도크로. 예약(disposition 미변경 → 조용한 예약).
99
- const staging = this.nodeByType('staging');
116
+ const staging = this.locationByType('staging');
100
117
  const reserved = new Set([...this.trailerCargo.values()].flat());
101
118
  const avail = [...this.items.values()]
102
119
  .filter(i => i.location === staging?.id && i.disposition === DISP.sellable && !reserved.has(i.epc))
@@ -117,15 +134,19 @@ export class YmsKernel extends FlowEngine {
117
134
  }
118
135
  /** 태스크 완료 — 목적지 타입으로 분기: yard-slot=주차, dock-door=상/하차+depart, gate=출차. */
119
136
  onTaskComplete(t) {
120
- const trailer = this.items.get(t.itemEpc);
121
- const from = this.nodes.get(t.fromNode);
122
- const to = this.nodes.get(t.toNode);
137
+ const trailer = this.itemByRef(t.itemEpc);
138
+ /* 여기 도달했다면 코어가 이미 물품을 확인했다(주체가 사라진 작업은 완료 전에 접힌다).
139
+ 그래도 단정(`!`)은 쓰지 않는다 — 계약이 바뀌면 조용히 틀리는 대신 분명히 멈춘다. */
140
+ if (!trailer)
141
+ throw new Error(`task ${t.id}: item "${t.itemEpc}" vanished between the core check and the domain hook`);
142
+ const from = this.locations.get(t.fromNode);
143
+ const to = this.locations.get(t.toNode);
123
144
  from.occupancy--;
124
145
  to.occupancy++;
125
146
  trailer.location = to.id;
126
147
  if (t.kind === 'dwell') {
127
148
  // 도크 체류(무자원 dwell) 완료 → 출차 이동(transport). 체류 중 호슬러 미점유(drop-and-hook 충실).
128
- const gate = this.nodeByType('gate');
149
+ const gate = this.locationByType('gate');
129
150
  const dep = { id: `task-${++this.taskSeq}`, kind: 'depart', status: 'created', itemEpc: trailer.epc, fromNode: t.toNode, toNode: gate.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: 'depart', fromNode: t.toNode, toNode: gate.id }, TRAVEL_MS), orderId: t.orderId };
130
151
  this.tasks.set(dep.id, dep);
131
152
  this.emitTask(dep);
@@ -140,7 +161,7 @@ export class YmsKernel extends FlowEngine {
140
161
  if (to.type === 'dock-door') {
141
162
  const order = t.orderId ? this.orders.get(t.orderId) : undefined;
142
163
  const outbound = order?.kind === 'appointment-out';
143
- const staging = this.nodeByType('staging');
164
+ const staging = this.locationByType('staging');
144
165
  const cargo = this.trailerCargo.get(trailer.epc) ?? [];
145
166
  // 도크 도착 — 상·하차 관측. 도어는 depart 완료까지 점유(예약 유지).
146
167
  /* 이벤트만 내고 상태를 안 바꾸면 이벤트와 상태가 다른 말을 한다(할당 때 잡힌 'reserved' 가
@@ -10,6 +10,6 @@ export declare const YARD_BIZSTEP: {
10
10
  export declare const BTT_DELIVERY = "urn:epcglobal:cbv:btt:deliv";
11
11
  /** 트레일러/컨테이너 = GRAI(Global Returnable Asset Identifier, 반납형 자산). */
12
12
  export declare function graiUri(companyPrefix: string, assetType: string, serial: number): string;
13
- /** YMS 노드 타입 카탈로그 — 커널이 키로 쓰는 야드 로케이션 타입(nodeByType/slotViews/.type). 도메인 SSOT. */
14
- export declare const YMS_NODE_TYPES: readonly ["gate", "yard-slot", "dock-door", "staging"];
13
+ /** YMS 자리 타입 카탈로그 — 커널이 키로 쓰는 야드 로케이션 타입(locationByType/slotViews/.type). 도메인 SSOT. */
14
+ export declare const YMS_LOCATION_TYPES: readonly ["gate", "yard-slot", "dock-door", "staging"];
15
15
  export declare const YMS_TYPES: TwinTypeInfo[];
@@ -12,13 +12,13 @@ export const BTT_DELIVERY = 'urn:epcglobal:cbv:btt:deliv';
12
12
  export function graiUri(companyPrefix, assetType, serial) {
13
13
  return `urn:epc:id:grai:${companyPrefix}.${assetType}.${String(serial).padStart(6, '0')}`;
14
14
  }
15
- /** YMS 노드 타입 카탈로그 — 커널이 키로 쓰는 야드 로케이션 타입(nodeByType/slotViews/.type). 도메인 SSOT. */
16
- export const YMS_NODE_TYPES = ['gate', 'yard-slot', 'dock-door', 'staging'];
15
+ /** YMS 자리 타입 카탈로그 — 커널이 키로 쓰는 야드 로케이션 타입(locationByType/slotViews/.type). 도메인 SSOT. */
16
+ export const YMS_LOCATION_TYPES = ['gate', 'yard-slot', 'dock-door', 'staging'];
17
17
  /*
18
- * 트윈 타입 서술(ADR-0018 확장) — 노드 키는 YMS_NODE_TYPES 단일 출처에서 파생 + 무버 타입 추가.
19
- * YMS=EPCIS zone: 로케이션/존=bizLocation(SGLN), 무버(야드 트랙터)=오브젝트/자산(GIAI).
18
+ * 트윈 타입 서술(ADR-0018 확장) — 자리 키는 YMS_LOCATION_TYPES 단일 출처에서 파생 + 설비 타입 추가.
19
+ * YMS=EPCIS zone: 로케이션/존=bizLocation(SGLN), 설비(야드 트랙터)=오브젝트/자산(GIAI).
20
20
  */
21
21
  export const YMS_TYPES = [
22
- ...YMS_NODE_TYPES.map((k) => ({ key: k, role: 'node', label: `twin.type.${k}`, standardClass: { epcis: 'bizLocation' }, identity: { scheme: 'gs1:SGLN' }, capabilities: ['storable'] })),
23
- { key: 'hostler', role: 'mover', label: 'twin.type.hostler', standardClass: { epcis: 'object', iso55000: 'Asset' }, identity: { scheme: 'gs1:GIAI' }, capabilities: ['mobile', 'operable'] }
22
+ ...YMS_LOCATION_TYPES.map((k) => ({ key: k, role: 'location', label: `twin.type.${k}`, standardClass: { epcis: 'bizLocation' }, identity: { scheme: 'gs1:SGLN' }, capabilities: ['storable'] })),
23
+ { key: 'hostler', role: 'equipment', label: 'twin.type.hostler', standardClass: { epcis: 'object', iso55000: 'Asset' }, identity: { scheme: 'gs1:GIAI' }, capabilities: ['mobile', 'operable'] }
24
24
  ];