@operato/twin-kernel 0.2.2 → 0.3.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.
- package/README.md +46 -5
- package/dist/capability.js +2 -2
- package/dist/contract.d.ts +336 -34
- package/dist/contract.js +199 -7
- package/dist/divergence.d.ts +1 -1
- package/dist/divergence.js +3 -3
- package/dist/domain-catalog.d.ts +4 -4
- package/dist/domain-catalog.js +4 -4
- package/dist/domain-definition.d.ts +5 -5
- package/dist/domain-definition.js +6 -6
- package/dist/event-journal.d.ts +1 -1
- package/dist/event-journal.js +1 -1
- package/dist/flow-engine.d.ts +55 -31
- package/dist/flow-engine.js +153 -124
- package/dist/forecast.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/kernel.js +6 -6
- package/dist/mes-kernel.js +25 -25
- package/dist/mes-profile.d.ts +2 -2
- package/dist/mes-profile.js +11 -11
- package/dist/observed-reducer.d.ts +7 -7
- package/dist/observed-reducer.js +40 -30
- package/dist/task-fold.d.ts +2 -2
- package/dist/task-fold.js +4 -4
- package/dist/vocabulary.d.ts +19 -0
- package/dist/vocabulary.js +66 -0
- package/dist/wms-profile.d.ts +2 -2
- package/dist/wms-profile.js +6 -6
- package/dist/yms-kernel.js +10 -10
- package/dist/yms-profile.d.ts +2 -2
- package/dist/yms-profile.js +6 -6
- package/dist-cjs/index.cjs +413 -214
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/wms-profile.d.ts
CHANGED
|
@@ -13,8 +13,8 @@ export declare const BTT: {
|
|
|
13
13
|
readonly so: "urn:epcglobal:cbv:btt:so";
|
|
14
14
|
};
|
|
15
15
|
/**
|
|
16
|
-
* WMS
|
|
16
|
+
* WMS 자리 타입 카탈로그 — 커널이 실제로 키로 쓰는 로케이션 타입(locationByType/slotViews).
|
|
17
17
|
* 도메인 어휘 SSOT: 호스트·UI 는 이걸 소싱하고 재선언하지 않는다(방언 금지).
|
|
18
18
|
*/
|
|
19
|
-
export declare const
|
|
19
|
+
export declare const WMS_LOCATION_TYPES: readonly ["dock", "storage", "staging", "dock-ship"];
|
|
20
20
|
export declare const WMS_TYPES: TwinTypeInfo[];
|
package/dist/wms-profile.js
CHANGED
|
@@ -14,16 +14,16 @@ export const BTT = {
|
|
|
14
14
|
so: 'urn:epcglobal:cbv:btt:so'
|
|
15
15
|
};
|
|
16
16
|
/**
|
|
17
|
-
* WMS
|
|
17
|
+
* WMS 자리 타입 카탈로그 — 커널이 실제로 키로 쓰는 로케이션 타입(locationByType/slotViews).
|
|
18
18
|
* 도메인 어휘 SSOT: 호스트·UI 는 이걸 소싱하고 재선언하지 않는다(방언 금지).
|
|
19
19
|
*/
|
|
20
|
-
export const
|
|
20
|
+
export const WMS_LOCATION_TYPES = ['dock', 'storage', 'staging', 'dock-ship'];
|
|
21
21
|
/*
|
|
22
|
-
* 트윈 타입 서술(ADR-0018 확장) —
|
|
23
|
-
* WMS=EPCIS 도메인: 로케이션=bizLocation(SGLN),
|
|
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
|
-
...
|
|
28
|
-
{ key: 'forklift', role: '
|
|
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
|
];
|
package/dist/yms-kernel.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 아웃바운드(상차): 빈 트레일러 도착 → 야드 대기 → (창+도어+staging 화물) 도크 → 상차 → 적재 출차.
|
|
5
5
|
*
|
|
6
6
|
* ★ 검증 질문: base 가 "시간창 예약 스케줄링"을 흡수하는가?
|
|
7
|
-
* → clock(base) +
|
|
7
|
+
* → clock(base) + 자리 점유(도크도어 예약) + allocate hook(스케줄 게이트)로 표현 — base 변경 0.
|
|
8
8
|
*
|
|
9
9
|
* 상·하차 대칭(AggregationEvent):
|
|
10
10
|
* - 하차(inbound): 게이트-인에 트레일러←화물 조립(ADD), 도크에서 분해(DELETE) → 화물이 staging 에 등장(WMS receiving hand-off).
|
|
@@ -41,8 +41,8 @@ export class YmsKernel extends FlowEngine {
|
|
|
41
41
|
onOrder(_spec) { this.spawnAppointment('appointment-out'); }
|
|
42
42
|
/** 게이트-인 — 트레일러 도착 → 어포인트먼트(도크도어 배정 + 창). drop 은 야드 주차 태스크, live 는 게이트 대기. */
|
|
43
43
|
spawnAppointment(kind) {
|
|
44
|
-
const gate = this.
|
|
45
|
-
const doors = [...this.
|
|
44
|
+
const gate = this.locationByType('gate');
|
|
45
|
+
const doors = [...this.locations.values()].filter(n => n.type === 'dock-door').sort((a, b) => a.id.localeCompare(b.id));
|
|
46
46
|
if (!gate || doors.length === 0)
|
|
47
47
|
return;
|
|
48
48
|
const inbound = kind === 'appointment';
|
|
@@ -85,18 +85,18 @@ export class YmsKernel extends FlowEngine {
|
|
|
85
85
|
return;
|
|
86
86
|
if (this.clockMs < (o.windowStartMs ?? 0))
|
|
87
87
|
return; // 창 도래 전
|
|
88
|
-
const door = this.
|
|
88
|
+
const door = this.locations.get(o.dockDoor);
|
|
89
89
|
const inflight = [...this.tasks.values()].some(t => t.status !== 'completed' && t.toNode === door.id);
|
|
90
90
|
if (door.occupancy > 0 || inflight)
|
|
91
91
|
return; // 도어 사용중/예약됨 → 대기(직렬화)
|
|
92
92
|
const mode = this.apptMode.get(o.id) ?? 'drop';
|
|
93
|
-
if (mode === 'drop' && this.
|
|
93
|
+
if (mode === 'drop' && this.locations.get(trailer.location)?.type !== 'yard-slot')
|
|
94
94
|
return; // 아직 미주차
|
|
95
95
|
const outbound = o.kind === 'appointment-out';
|
|
96
96
|
const bizStep = outbound ? YARD_BIZSTEP.loading : YARD_BIZSTEP.unloading;
|
|
97
97
|
if (outbound) {
|
|
98
98
|
// 아웃바운드: staging 에 적재 화물(sellable, 미예약) 충분해야 도크로. 예약(disposition 미변경 → 조용한 예약).
|
|
99
|
-
const staging = this.
|
|
99
|
+
const staging = this.locationByType('staging');
|
|
100
100
|
const reserved = new Set([...this.trailerCargo.values()].flat());
|
|
101
101
|
const avail = [...this.items.values()]
|
|
102
102
|
.filter(i => i.location === staging?.id && i.disposition === DISP.sellable && !reserved.has(i.epc))
|
|
@@ -118,14 +118,14 @@ export class YmsKernel extends FlowEngine {
|
|
|
118
118
|
/** 태스크 완료 — 목적지 타입으로 분기: yard-slot=주차, dock-door=상/하차+depart, gate=출차. */
|
|
119
119
|
onTaskComplete(t) {
|
|
120
120
|
const trailer = this.items.get(t.itemEpc);
|
|
121
|
-
const from = this.
|
|
122
|
-
const to = this.
|
|
121
|
+
const from = this.locations.get(t.fromNode);
|
|
122
|
+
const to = this.locations.get(t.toNode);
|
|
123
123
|
from.occupancy--;
|
|
124
124
|
to.occupancy++;
|
|
125
125
|
trailer.location = to.id;
|
|
126
126
|
if (t.kind === 'dwell') {
|
|
127
127
|
// 도크 체류(무자원 dwell) 완료 → 출차 이동(transport). 체류 중 호슬러 미점유(drop-and-hook 충실).
|
|
128
|
-
const gate = this.
|
|
128
|
+
const gate = this.locationByType('gate');
|
|
129
129
|
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
130
|
this.tasks.set(dep.id, dep);
|
|
131
131
|
this.emitTask(dep);
|
|
@@ -140,7 +140,7 @@ export class YmsKernel extends FlowEngine {
|
|
|
140
140
|
if (to.type === 'dock-door') {
|
|
141
141
|
const order = t.orderId ? this.orders.get(t.orderId) : undefined;
|
|
142
142
|
const outbound = order?.kind === 'appointment-out';
|
|
143
|
-
const staging = this.
|
|
143
|
+
const staging = this.locationByType('staging');
|
|
144
144
|
const cargo = this.trailerCargo.get(trailer.epc) ?? [];
|
|
145
145
|
// 도크 도착 — 상·하차 관측. 도어는 depart 완료까지 점유(예약 유지).
|
|
146
146
|
/* 이벤트만 내고 상태를 안 바꾸면 이벤트와 상태가 다른 말을 한다(할당 때 잡힌 'reserved' 가
|
package/dist/yms-profile.d.ts
CHANGED
|
@@ -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
|
|
14
|
-
export declare const
|
|
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[];
|
package/dist/yms-profile.js
CHANGED
|
@@ -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
|
|
16
|
-
export const
|
|
15
|
+
/** YMS 자리 타입 카탈로그 — 커널이 키로 쓰는 야드 로케이션 타입(locationByType/slotViews/.type). 도메인 SSOT. */
|
|
16
|
+
export const YMS_LOCATION_TYPES = ['gate', 'yard-slot', 'dock-door', 'staging'];
|
|
17
17
|
/*
|
|
18
|
-
* 트윈 타입 서술(ADR-0018 확장) —
|
|
19
|
-
* YMS=EPCIS zone: 로케이션/존=bizLocation(SGLN),
|
|
18
|
+
* 트윈 타입 서술(ADR-0018 확장) — 자리 키는 YMS_LOCATION_TYPES 단일 출처에서 파생 + 설비 타입 추가.
|
|
19
|
+
* YMS=EPCIS zone: 로케이션/존=bizLocation(SGLN), 설비(야드 트랙터)=오브젝트/자산(GIAI).
|
|
20
20
|
*/
|
|
21
21
|
export const YMS_TYPES = [
|
|
22
|
-
...
|
|
23
|
-
{ key: 'hostler', role: '
|
|
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
|
];
|