@operato/twin-kernel 0.8.1 → 0.8.2

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.
@@ -1,72 +0,0 @@
1
- const DISTRIBUTIONS = new Set(['poisson', 'uniform', 'constant', 'profile']);
2
- const isNum = (v) => typeof v === 'number' && Number.isFinite(v);
3
- const bad = (errorCode, errorParams) => ({ ok: false, errorCode, errorParams });
4
- /** 생성기 한 개 — 무엇이 빠지면 tick 이 터지는지를 기준으로 본다(장식이 아니라 필수만). */
5
- function validateGenerator(g, at) {
6
- if (!g || typeof g !== 'object')
7
- return bad('scenario-generator-invalid', { at });
8
- if (!g.kind || typeof g.kind !== 'string')
9
- return bad('scenario-generator-kind-required', { at });
10
- /* 도착률 — 없거나 숫자가 아니면 간격을 계산할 수 없다(여기서 터졌다). */
11
- const rate = g.rate;
12
- if (!rate || typeof rate !== 'object')
13
- return bad('scenario-rate-required', { at, kind: g.kind });
14
- if (!isNum(rate.meanPerHour) || rate.meanPerHour < 0)
15
- return bad('scenario-rate-mean-invalid', { at, kind: g.kind });
16
- if (!DISTRIBUTIONS.has(rate.distribution))
17
- return bad('scenario-rate-distribution-invalid', { at, kind: g.kind, distribution: String(rate.distribution ?? '') });
18
- /* `profile` 은 시간대 배율 표가 있어야 뜻이 있다 — 없으면 조용히 상수처럼 굴러 선언과 다르게 동작한다. */
19
- if (rate.distribution === 'profile' && !Array.isArray(rate.profile))
20
- return bad('scenario-rate-profile-required', { at, kind: g.kind });
21
- /* 내용 — 무엇을 얼마나 만드는가. skuMix 가 비면 만들 물건이 없다. */
22
- const content = g.content;
23
- if (!content || typeof content !== 'object')
24
- return bad('scenario-content-required', { at, kind: g.kind });
25
- if (!Array.isArray(content.skuMix) || content.skuMix.length === 0)
26
- return bad('scenario-sku-mix-required', { at, kind: g.kind });
27
- for (const s of content.skuMix) {
28
- if (!s?.gtin || typeof s.gtin !== 'string')
29
- return bad('scenario-sku-gtin-required', { at, kind: g.kind });
30
- if (!isNum(s.weight) || s.weight <= 0)
31
- return bad('scenario-sku-weight-invalid', { at, kind: g.kind, gtin: String(s.gtin) });
32
- }
33
- const q = content.qtyPerLine;
34
- if (!q || !isNum(q.min) || !isNum(q.max))
35
- return bad('scenario-qty-required', { at, kind: g.kind });
36
- if (q.min < 0 || q.max < q.min)
37
- return bad('scenario-qty-range-invalid', { at, kind: g.kind, min: q.min, max: q.max });
38
- const l = content.linesPerOrder;
39
- if (l && (!isNum(l.min) || !isNum(l.max) || l.min < 1 || l.max < l.min))
40
- return bad('scenario-lines-range-invalid', { at, kind: g.kind });
41
- if (g.stimulus !== undefined && g.stimulus !== 'arrival' && g.stimulus !== 'order') {
42
- return bad('scenario-stimulus-invalid', { at, kind: g.kind, stimulus: String(g.stimulus) });
43
- }
44
- return { ok: true };
45
- }
46
- /**
47
- * 시나리오 선언이 실릴 수 있는가.
48
- *
49
- * 첫 번째 잘못에서 멈추고 그것을 말한다 — 전부 모아 보고하면 화면이 무엇부터 고칠지 알 수 없다.
50
- * 생성기가 하나도 없는 것은 **잘못이 아니다**(자극 없이 관측만 하는 시나리오는 성립한다).
51
- */
52
- export function validateScenario(def) {
53
- if (!def || typeof def !== 'object')
54
- return bad('scenario-invalid');
55
- if (def.seed !== undefined && !isNum(def.seed))
56
- return bad('scenario-seed-invalid');
57
- if (def.speed !== undefined && (!isNum(def.speed) || def.speed <= 0))
58
- return bad('scenario-speed-invalid');
59
- if (def.horizon !== undefined && (!isNum(def.horizon) || def.horizon < 0))
60
- return bad('scenario-horizon-invalid');
61
- const gens = def.generators;
62
- if (gens === undefined)
63
- return { ok: true }; // 자극 없는 시나리오 — 성립한다
64
- if (!Array.isArray(gens))
65
- return bad('scenario-generators-invalid');
66
- for (let i = 0; i < gens.length; i++) {
67
- const r = validateGenerator(gens[i], i);
68
- if (!r.ok)
69
- return r;
70
- }
71
- return { ok: true };
72
- }
@@ -1,28 +0,0 @@
1
- /**
2
- * 금지 어휘 — 표준 어휘로 대체된 옛 낱말. 파생 식별자까지 잡도록 부분 일치로 본다.
3
- *
4
- * `BoardDef`·`loadBoard` 는 ADR-0033 으로 `TwinModelDef`·`loadTwinModel` 이 됐다. 커널 소스에는
5
- * **0곳**이라 개명이 끝났는데, 문서에는 31곳이 남아 있었다(2026-08-19 실측) — 코드에 없는 이름으로
6
- * 설계를 설명하고 있었다. 코드에 0곳이므로 여기 올려 되돌아오는 것을 막는다.
7
- *
8
- * (문서 쪽은 `test/doc-vocabulary-guard.test.ts` 가 **자기 목록**으로 본다 — 이 목록의 `node`·`mover` 는
9
- * ADR 기록이 없어 문서의 개념 명사에까지 들이대지 않는다.)
10
- */
11
- export declare const RETIRED_VOCABULARY: readonly ["mover", "Mover", "MOVER", "node", "Node", "NODE", "BoardDef", "loadBoard"];
12
- /**
13
- * 예외 — 남아 있는 것이 **정당한** 토큰. 각 항목에 이유가 붙는다.
14
- *
15
- * 판정은 "이 토큰을 정확히 포함하는가" 다(부분 문자열). 그래서 `moverId` 를 허용하면 `moverIds` 도
16
- * 통과한다 — 와이어 필드에서 파생한 변수까지 허용하려는 의도다.
17
- */
18
- export declare const VOCABULARY_EXCEPTIONS: {
19
- token: string;
20
- why: string;
21
- }[];
22
- /**
23
- * 한 줄에서 금지 어휘 위반을 찾는다 — 예외에 걸리는 부분은 먼저 지워 놓고 본다.
24
- *
25
- * 반환은 위반 낱말 목록(중복 제거 전). 비어 있으면 그 줄은 통과.
26
- */
27
- export declare const GUARD_PRAGMA = "vocabulary-guard: allow";
28
- export declare function retiredVocabularyIn(line: string): string[];
@@ -1,81 +0,0 @@
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
- * 금지 어휘 — 표준 어휘로 대체된 옛 낱말. 파생 식별자까지 잡도록 부분 일치로 본다.
15
- *
16
- * `BoardDef`·`loadBoard` 는 ADR-0033 으로 `TwinModelDef`·`loadTwinModel` 이 됐다. 커널 소스에는
17
- * **0곳**이라 개명이 끝났는데, 문서에는 31곳이 남아 있었다(2026-08-19 실측) — 코드에 없는 이름으로
18
- * 설계를 설명하고 있었다. 코드에 0곳이므로 여기 올려 되돌아오는 것을 막는다.
19
- *
20
- * (문서 쪽은 `test/doc-vocabulary-guard.test.ts` 가 **자기 목록**으로 본다 — 이 목록의 `node`·`mover` 는
21
- * ADR 기록이 없어 문서의 개념 명사에까지 들이대지 않는다.)
22
- */
23
- export const RETIRED_VOCABULARY = ['mover', 'Mover', 'MOVER', 'node', 'Node', 'NODE', 'BoardDef', 'loadBoard'];
24
- /**
25
- * 예외 — 남아 있는 것이 **정당한** 토큰. 각 항목에 이유가 붙는다.
26
- *
27
- * 판정은 "이 토큰을 정확히 포함하는가" 다(부분 문자열). 그래서 `moverId` 를 허용하면 `moverIds` 도
28
- * 통과한다 — 와이어 필드에서 파생한 변수까지 허용하려는 의도다.
29
- */
30
- export const VOCABULARY_EXCEPTIONS = [
31
- /* ── 저널 와이어 필드 — append-only 역사이므로 이름을 바꾸지 않는다 ───────
32
- * 같은 사실이 시점에 따라 다른 키로 들어가면 낡은 이름보다 나쁘다. 개명은 이벤트 스키마
33
- * 버저닝과 함께 다룬다. (개명 시점 실측: moverId 8,965행 · fromNode/toNode 9,679행) */
34
- { token: 'moverId', why: 'journal wire field — renaming would mix two keys for one fact across history' },
35
- { token: 'fromNode', why: 'journal wire field (task.status payload)' },
36
- { token: 'toNode', why: 'journal wire field (task.status payload)' },
37
- /* ── 트윈 모델의 옛 세대 키 — 저장된 데이터라 읽어는 줘야 한다 ────────────────
38
- * 설비 배열의 이름은 세 세대를 거쳤다(movers → equipmentList → equipment). 저장된 보드에는
39
- * 셋이 섞여 있고(실측 23개 중 13개가 `movers`), 하나라도 안 읽으면 그 트윈 모델은 **설비가 0인 공장**
40
- * 으로 조용히 읽힌다 — 화면의 설비 수가 0이 되고 용량 판정에서 자원이 사라진다. 쓰는 곳은
41
- * `readBoardEquipment` 한 곳뿐이고, 거기서 새 이름으로 정규화해 내보낸다. */
42
- { token: 'movers', why: 'legacy board key (movers → equipmentList → equipment); read-only normalization in readBoardEquipment, 13 stored boards still use it' },
43
- /* ── 씬 컴포넌트 타입 — 트윈 모델에 저장된 값이고, 뜻이 어긋나지도 않는다 ──────
44
- * 보드 7개가 이 타입으로 컴포넌트를 담고 있어 개명하면 그 컴포넌트가 조용히 안 그려진다.
45
- * 그리고 씬에서 이 이름은 자원이 아니라 **움직임 표현**을 가리킨다(표준과 충돌 아님). */
46
- { token: 'twin-mover', why: 'scene component type persisted in boards; names a motion representation, not a resource' },
47
- { token: 'TwinMover', why: 'class backing the persisted scene component type above' },
48
- /* ── 플랫폼·언어 어휘 — 우리 도메인 낱말이 아니다 ─────────────────────── */
49
- { token: 'node_modules', why: 'npm path' },
50
- { token: 'node:', why: 'Node.js builtin module specifier' },
51
- { token: 'nodemon', why: 'tool name' },
52
- { token: 'Node.js', why: 'platform name' },
53
- { token: 'NodeJS', why: 'TypeScript global namespace' },
54
- { token: 'nodeName', why: 'DOM API' },
55
- { token: 'childNodes', why: 'DOM API' },
56
- { token: 'appendChild', why: 'DOM API (no match, kept for clarity)' }
57
- ];
58
- /**
59
- * 한 줄에서 금지 어휘 위반을 찾는다 — 예외에 걸리는 부분은 먼저 지워 놓고 본다.
60
- *
61
- * 반환은 위반 낱말 목록(중복 제거 전). 비어 있으면 그 줄은 통과.
62
- */
63
- export const GUARD_PRAGMA = 'vocabulary-guard: allow';
64
- export function retiredVocabularyIn(line) {
65
- /* 줄 단위 예외 — 옛 키를 **정당하게** 언급해야 하는 곳(읽기 호환 함수·개명 경위 주석)이 있다.
66
- 그때는 그 줄에 `vocabulary-guard: allow <이유>` 를 남긴다. 예외가 코드에 이유와 함께 남는다. */
67
- if (line.includes(GUARD_PRAGMA))
68
- return [];
69
- let rest = line;
70
- for (const { token } of VOCABULARY_EXCEPTIONS)
71
- rest = rest.split(token).join(' ');
72
- const hits = [];
73
- /* **대소문자를 구별한다.** `gi` 로 두면 `removeRep` 의 `moveR` 이 `mover` 로 잡힌다(실제 오탐).
74
- 그래서 실제로 쓰이는 표기만 나열하고 정확히 그 표기를 찾는다. */
75
- for (const word of RETIRED_VOCABULARY) {
76
- const re = new RegExp(`[A-Za-z_]*${word}[A-Za-z_]*`, 'g');
77
- for (const m of rest.matchAll(re))
78
- hits.push(m[0]);
79
- }
80
- return hits;
81
- }
@@ -1,20 +0,0 @@
1
- import type { TwinTypeInfo } from './domain-catalog.ts';
2
- export declare const BIZSTEP: {
3
- readonly receiving: "urn:epcglobal:cbv:bizstep:receiving";
4
- readonly storing: "urn:epcglobal:cbv:bizstep:storing";
5
- readonly picking: "urn:epcglobal:cbv:bizstep:picking";
6
- readonly packing: "urn:epcglobal:cbv:bizstep:packing";
7
- readonly staging_outbound: "urn:epcglobal:cbv:bizstep:staging_outbound";
8
- readonly shipping: "urn:epcglobal:cbv:bizstep:shipping";
9
- readonly replenishing: "urn:epcglobal:cbv:bizstep:replenishing";
10
- };
11
- export declare const BTT: {
12
- readonly po: "urn:epcglobal:cbv:btt:po";
13
- readonly so: "urn:epcglobal:cbv:btt:so";
14
- };
15
- /**
16
- * WMS 자리 타입 카탈로그 — 커널이 실제로 키로 쓰는 로케이션 타입(locationByType/slotViews).
17
- * 도메인 어휘 SSOT: 호스트·UI 는 이걸 소싱하고 재선언하지 않는다(방언 금지).
18
- */
19
- export declare const WMS_LOCATION_TYPES: readonly ["dock", "storage", "staging", "dock-ship", "vas-station"];
20
- export declare const WMS_TYPES: TwinTypeInfo[];
@@ -1,58 +0,0 @@
1
- // bizStep — CBV URN (wms.md §11 확정 어휘)
2
- export const BIZSTEP = {
3
- receiving: 'urn:epcglobal:cbv:bizstep:receiving',
4
- storing: 'urn:epcglobal:cbv:bizstep:storing',
5
- picking: 'urn:epcglobal:cbv:bizstep:picking',
6
- packing: 'urn:epcglobal:cbv:bizstep:packing',
7
- staging_outbound: 'urn:epcglobal:cbv:bizstep:staging_outbound',
8
- shipping: 'urn:epcglobal:cbv:bizstep:shipping',
9
- replenishing: 'urn:epcglobal:cbv:bizstep:replenishing'
10
- };
11
- // bizTransaction 유형 — CBV btt (§2 "거래" 매핑: PO=입고 ASN, SO=출고 오더)
12
- export const BTT = {
13
- po: 'urn:epcglobal:cbv:btt:po',
14
- so: 'urn:epcglobal:cbv:btt:so'
15
- };
16
- /**
17
- * WMS 자리 타입 카탈로그 — 커널이 실제로 키로 쓰는 로케이션 타입(locationByType/slotViews).
18
- * 도메인 어휘 SSOT: 호스트·UI 는 이걸 소싱하고 재선언하지 않는다(방언 금지).
19
- */
20
- export const WMS_LOCATION_TYPES = ['dock', 'storage', 'staging', 'dock-ship', 'vas-station'];
21
- /*
22
- * 트윈 타입 서술(ADR-0018 확장) — 자리 키는 WMS_LOCATION_TYPES 단일 출처에서 파생 + 설비 타입 추가.
23
- * WMS=EPCIS 도메인: 로케이션=bizLocation(SGLN), 설비(지게차)=추적 오브젝트/자산(GIAI). 능력은 씬 소유라 미포함.
24
- */
25
- // label 은 언어 중립 i18n 키(twin.type.<key>) — 사람 언어는 표현계층이 렌더(design/plans/i18n.md L2).
26
- /**
27
- * 유통가공 작업대(`vas-station`) — 창고에서 **자재를 소비해 자재를 산출하는** 자리.
28
- *
29
- * 표준으로는 ISA-95 `WorkCenter` 이고, 하는 일은 EPCIS 로 보면 변환(부품 → 세트 SKU)이다.
30
- * "VAS" 는 물류업계 용어일 뿐 표준 엔티티가 아니라, 커널은 그 이름의 개념을 새로 만들지 않는다 —
31
- * 능력으로 말한다: 물건을 담고(`storable`) 가공한다(`processable`).
32
- *
33
- * 예전에는 이 타입이 카탈로그에 **없었다.** 그래서 유통가공 창고 템플릿이 이 자리를 만들 때마다
34
- * 인제스트가 "모르는 자리 타입" 경고를 냈고(그 경고는 화면에도 보이지 않았다), 커널은 그 자리를
35
- * 흐름에서 빼 두었다. 키팅이 일어나지 않는 유통가공 창고가 그렇게 만들어졌다.
36
- */
37
- /**
38
- * 자리의 **ISA-95 계층 단** — 총칭 관계를 그대로 쓴다(`WorkUnit` ⊂ `WorkCenter` 단이 아니라,
39
- * `StorageUnit` 은 `WorkUnit` 의 한 종류이고 `StorageZone` 은 `WorkCenter` 의 한 종류다).
40
- *
41
- * · `storage` = 보관 단위 하나(랙·빈) → `StorageUnit`
42
- * · `staging`·`dock`·`dock-ship` = 여러 단위를 담는 구역 → `StorageZone`
43
- * · `vas-station` = 자재를 소비해 산출하는 작업 자리 → `WorkCell`
44
- */
45
- const WMS_LOCATION_LEVEL = {
46
- dock: 'StorageZone',
47
- storage: 'StorageUnit',
48
- staging: 'StorageZone',
49
- 'dock-ship': 'StorageZone',
50
- 'vas-station': 'WorkCell'
51
- };
52
- export const WMS_TYPES = [
53
- ...WMS_LOCATION_TYPES.filter(k => k !== 'vas-station').map((k) => ({ key: k, role: 'location', label: `twin.type.${k}`, standardClass: { epcis: 'bizLocation' }, identity: { scheme: 'gs1:SGLN' }, level: WMS_LOCATION_LEVEL[k], capabilities: ['storable'] })),
54
- { key: 'vas-station', role: 'location', label: 'twin.type.vas-station', standardClass: { epcis: 'bizLocation', isa95: 'WorkCenter' }, identity: { scheme: 'gs1:SGLN' }, level: WMS_LOCATION_LEVEL['vas-station'], capabilities: ['storable', 'processable'] },
55
- { key: 'forklift', role: 'equipment', label: 'twin.type.forklift', standardClass: { epcis: 'object', iso55000: 'Asset' }, identity: { scheme: 'gs1:GIAI' }, capabilities: ['mobile', 'operable'] },
56
- /** 유통가공 작업자·작업대 설비 — 가공을 수행하는 능동 자원(ISA-95 `Equipment`). */
57
- { key: 'packer', role: 'equipment', label: 'twin.type.packer', standardClass: { epcis: 'object', isa95: 'Equipment' }, identity: { scheme: 'gs1:GIAI' }, capabilities: ['processable', 'operable'] }
58
- ];
@@ -1,15 +0,0 @@
1
- import type { TwinTypeInfo } from './domain-catalog.ts';
2
- export declare const YARD_BIZSTEP: {
3
- readonly arriving: "urn:epcglobal:cbv:bizstep:arriving";
4
- readonly staging: "urn:epcglobal:cbv:bizstep:staging";
5
- readonly loading: "urn:epcglobal:cbv:bizstep:loading";
6
- readonly unloading: "urn:epcglobal:cbv:bizstep:unloading";
7
- readonly departing: "urn:epcglobal:cbv:bizstep:departing";
8
- };
9
- /** 어포인트먼트/배송 거래 유형(CBV btt). */
10
- export declare const BTT_DELIVERY = "urn:epcglobal:cbv:btt:deliv";
11
- /** 트레일러/컨테이너 = GRAI(Global Returnable Asset Identifier, 반납형 자산). */
12
- export declare function graiUri(companyPrefix: string, assetType: string, serial: number): string;
13
- /** YMS 자리 타입 카탈로그 — 커널이 키로 쓰는 야드 로케이션 타입(locationByType/slotViews/.type). 도메인 SSOT. */
14
- export declare const YMS_LOCATION_TYPES: readonly ["gate", "yard-slot", "dock-door", "staging"];
15
- export declare const YMS_TYPES: TwinTypeInfo[];
@@ -1,39 +0,0 @@
1
- // 야드 bizStep — CBV (yms.md §11).
2
- export const YARD_BIZSTEP = {
3
- arriving: 'urn:epcglobal:cbv:bizstep:arriving', // 게이트-인
4
- staging: 'urn:epcglobal:cbv:bizstep:staging', // 야드 슬롯 주차·대기
5
- loading: 'urn:epcglobal:cbv:bizstep:loading', // 도크 작업(상차: 트레일러←화물)
6
- unloading: 'urn:epcglobal:cbv:bizstep:unloading', // 도크 작업(하차: 트레일러→화물)
7
- departing: 'urn:epcglobal:cbv:bizstep:departing' // 게이트-아웃
8
- };
9
- /** 어포인트먼트/배송 거래 유형(CBV btt). */
10
- export const BTT_DELIVERY = 'urn:epcglobal:cbv:btt:deliv';
11
- /** 트레일러/컨테이너 = GRAI(Global Returnable Asset Identifier, 반납형 자산). */
12
- export function graiUri(companyPrefix, assetType, serial) {
13
- return `urn:epc:id:grai:${companyPrefix}.${assetType}.${String(serial).padStart(6, '0')}`;
14
- }
15
- /** YMS 자리 타입 카탈로그 — 커널이 키로 쓰는 야드 로케이션 타입(locationByType/slotViews/.type). 도메인 SSOT. */
16
- export const YMS_LOCATION_TYPES = ['gate', 'yard-slot', 'dock-door', 'staging'];
17
- /*
18
- * 트윈 타입 서술(ADR-0018 확장) — 자리 키는 YMS_LOCATION_TYPES 단일 출처에서 파생 + 설비 타입 추가.
19
- * YMS=EPCIS zone: 로케이션/존=bizLocation(SGLN), 설비(야드 트랙터)=오브젝트/자산(GIAI).
20
- */
21
- /**
22
- * 자리의 **ISA-95 계층 단.**
23
- *
24
- * · `yard-slot`·`dock-door` = 트레일러 한 대가 서는 자리 → `StorageUnit`
25
- * · `staging` = 여러 자리를 담는 구역 → `StorageZone`
26
- * · `gate` = **표준에 대응하는 단이 없다.** 게이트는 보관도 작업도 아닌 **통과점**이다. 가까운
27
- * 이름을 억지로 적으면(예: StorageUnit) 롤업이 게이트를 보관 자리로 세고 점유율이 거짓이 된다.
28
- * 표준이 `Other` 를 탈출구로 두었으므로 그것을 쓰고, 현장의 낱말은 `type` 에 남는다.
29
- */
30
- const YMS_LOCATION_LEVEL = {
31
- gate: 'Other',
32
- 'yard-slot': 'StorageUnit',
33
- 'dock-door': 'StorageUnit',
34
- staging: 'StorageZone'
35
- };
36
- export const YMS_TYPES = [
37
- ...YMS_LOCATION_TYPES.map((k) => ({ key: k, role: 'location', label: `twin.type.${k}`, standardClass: { epcis: 'bizLocation' }, identity: { scheme: 'gs1:SGLN' }, level: YMS_LOCATION_LEVEL[k], capabilities: ['storable'] })),
38
- { key: 'hostler', role: 'equipment', label: 'twin.type.hostler', standardClass: { epcis: 'object', iso55000: 'Asset' }, identity: { scheme: 'gs1:GIAI' }, capabilities: ['mobile', 'operable'] }
39
- ];