@operato/ops-contract 0.1.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 +23 -0
- package/dist/capability.d.ts +142 -0
- package/dist/capability.js +127 -0
- package/dist/capacity.d.ts +99 -0
- package/dist/capacity.js +172 -0
- package/dist/contract.d.ts +3557 -0
- package/dist/contract.js +1248 -0
- package/dist/domain-catalog.d.ts +280 -0
- package/dist/domain-catalog.js +322 -0
- package/dist/domain-definition.d.ts +356 -0
- package/dist/domain-definition.js +137 -0
- package/dist/ems-profile.d.ts +147 -0
- package/dist/ems-profile.js +367 -0
- package/dist/energy-ingest.d.ts +214 -0
- package/dist/energy-ingest.js +801 -0
- package/dist/epcis.d.ts +458 -0
- package/dist/epcis.js +640 -0
- package/dist/face2-adapter.d.ts +191 -0
- package/dist/face2-adapter.js +284 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +39 -0
- package/dist/iso-duration.d.ts +5 -0
- package/dist/iso-duration.js +43 -0
- package/dist/master-data.d.ts +46 -0
- package/dist/master-data.js +100 -0
- package/dist/mes-profile.d.ts +14 -0
- package/dist/mes-profile.js +59 -0
- package/dist/operational-ingest.d.ts +44 -0
- package/dist/operational-ingest.js +379 -0
- package/dist/operations-capability.d.ts +117 -0
- package/dist/operations-capability.js +120 -0
- package/dist/scenario-validate.d.ts +15 -0
- package/dist/scenario-validate.js +72 -0
- package/dist/vocabulary.d.ts +28 -0
- package/dist/vocabulary.js +81 -0
- package/dist/wms-profile.d.ts +20 -0
- package/dist/wms-profile.js +58 -0
- package/dist/yms-profile.d.ts +15 -0
- package/dist/yms-profile.js +39 -0
- package/dist-cjs/index.cjs +3767 -0
- package/package.json +30 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { type CapacityAnalysis, type CapacityInput, type CapacityRequirement } from './capacity.ts';
|
|
2
|
+
import { type ISOTime, type WorkCalendarEntry } from './contract.ts';
|
|
3
|
+
/**
|
|
4
|
+
* 능력의 종류 — **표준 열거형**(B2MML `CapabilityTypeType`). 우리 낱말을 새로 만들지 않는다.
|
|
5
|
+
*
|
|
6
|
+
* 표준 정의(스키마 주석 그대로 옮김):
|
|
7
|
+
* - `Committed` — 앞으로 쓰기로 약정된 용량
|
|
8
|
+
* - `Unattainable` — 설비 상태·점유·인원·자재 때문에 앞으로 쓸 수 없는 용량
|
|
9
|
+
* - `Available` — 앞으로 더 쓸 수 있는 용량
|
|
10
|
+
* - `Used` / `Unused` — **과거** 값(품질 합격분 / 안 쓰였거나 불합격분)
|
|
11
|
+
* - `Total` — 과거로는 Used+Unused, 미래로는 Available+Unattainable+Committed
|
|
12
|
+
*/
|
|
13
|
+
export type CapabilityType = 'Used' | 'Unused' | 'Total' | 'Committed' | 'Available' | 'Unattainable' | 'Other';
|
|
14
|
+
/** 활동의 종류 — 표준 열거형(`OperationsTypeType`). 지금 우리는 생산만 다룬다. */
|
|
15
|
+
export type OperationsType = 'Production' | 'Maintenance' | 'Quality' | 'Inventory' | 'Mixed' | 'Other';
|
|
16
|
+
/** 한 공정의 능력 — 미래 분해. */
|
|
17
|
+
export interface OperationCapability {
|
|
18
|
+
operation: string;
|
|
19
|
+
/**
|
|
20
|
+
* 이 공정이 이 구간에 낼 수 있는 최대량 — `Committed + Available + Unattainable`.
|
|
21
|
+
*
|
|
22
|
+
* **`null` 은 "능력 없음" 이 아니라 "모른다"** 다: 공정 명세가 자원을 하나도 요구하지 않으면 상한이
|
|
23
|
+
* 서지 않는다. 0 으로 메우면 "이 공정은 아무것도 못 한다" 는 없는 사실이 생긴다.
|
|
24
|
+
*/
|
|
25
|
+
total: number | null;
|
|
26
|
+
/** 이미 약정된 몫 — 이 구간에 납기가 걸린 미완 오더의 잔량. */
|
|
27
|
+
committed: number;
|
|
28
|
+
/**
|
|
29
|
+
* 지금 쓸 수 없는 자원 때문에 잃는 몫. 자원의 현재 사실을 받지 못했으면 **`null`**(모른다).
|
|
30
|
+
* 0 으로 내면 "고장도 자격 만료도 없다" 는 없는 사실이 된다.
|
|
31
|
+
*/
|
|
32
|
+
unattainable: number | null;
|
|
33
|
+
/**
|
|
34
|
+
* 남은 능력 — `total − committed − unattainable`.
|
|
35
|
+
*
|
|
36
|
+
* **음수를 0 으로 깎지 않는다.** 음수는 사실이다(받을 수 없는 물량을 받았다). 가장 알아야 할 상태를
|
|
37
|
+
* `Math.max(0, …)` 하나로 덮는 일이 실제로 이 트윈에서 여러 번 있었다.
|
|
38
|
+
*/
|
|
39
|
+
available: number | null;
|
|
40
|
+
/** 무엇이 **상한**을 정하고 있나 — 축과 등급(용량 분석이 이미 낸다). 선언 기준이다. */
|
|
41
|
+
constraint?: CapacityRequirement;
|
|
42
|
+
/**
|
|
43
|
+
* **남은 능력을 제약하는 축** — 달성불가를 되찾으려면 여기부터다. `unattainable > 0` 일 때만 있다.
|
|
44
|
+
*
|
|
45
|
+
* 상한을 제약하는 축과 **다를 수 있고**, 그 차이가 곧 사람이 헛걸음하는 자리다: 용접기 2대와 용접
|
|
46
|
+
* 자격자 2명이 같은 상한을 낼 때 상한은 어느 쪽으로도 제약되지만, 자격 하나가 만료돼 잃은 몫의
|
|
47
|
+
* 원인은 인원이다. 화면이 상한 쪽만 보이면 사람이 **설비를 보러 간다**(실제로 그랬다).
|
|
48
|
+
*
|
|
49
|
+
* 값은 이미 손에 있다 — 달성불가를 내려고 가용 자원 기준으로 같은 계산을 한 번 더 실행하기 때문이다.
|
|
50
|
+
* 그 계산이 제약하는 축이 곧 이 답이고, 따로 판정을 만들지 않는다.
|
|
51
|
+
*/
|
|
52
|
+
unattainableBy?: CapacityRequirement;
|
|
53
|
+
}
|
|
54
|
+
export interface OperationsCapabilityReport {
|
|
55
|
+
/** 표준 `OperationsType` — 지금은 생산뿐이다(정비·품질 능력은 별도 보고서가 될 자리). */
|
|
56
|
+
operationsType: OperationsType;
|
|
57
|
+
/**
|
|
58
|
+
* 어느 구간의 능력인가 — **구간 없이 능력은 존재하지 않는다**(표준 `StartTime`·`EndTime`).
|
|
59
|
+
* `workingHours` 는 그 구간의 **가동 시간**이다(벽시계 시간이 아니다 — 쉬는 시간에는 안 나온다).
|
|
60
|
+
*/
|
|
61
|
+
window: {
|
|
62
|
+
fromTime: ISOTime;
|
|
63
|
+
toTime: ISOTime;
|
|
64
|
+
workingHours: number;
|
|
65
|
+
};
|
|
66
|
+
perOperation: OperationCapability[];
|
|
67
|
+
/** 라인을 묶는 공정 — 용량 분석의 판정을 그대로 쓴다. */
|
|
68
|
+
bottleneck?: CapacityAnalysis['bottleneck'];
|
|
69
|
+
/** 이 보고서를 낸 시각(표준 `PublishedDate`). 부르는 쪽이 준다 — 커널은 벽시계를 읽지 않는다. */
|
|
70
|
+
publishedDate?: ISOTime;
|
|
71
|
+
}
|
|
72
|
+
/** 이 구간에 납기가 걸린 미완 오더 — 약정의 재료. */
|
|
73
|
+
export interface CommittedDemand {
|
|
74
|
+
/** 남은 수량(요청 − 이행). 음수는 0 으로 본다(이행 초과는 약정이 아니다). */
|
|
75
|
+
remaining: number;
|
|
76
|
+
/** 납기 — 이 구간 안이어야 이 구간의 약정이다. 없으면 구간에 속하지 않는다(짐작하지 않는다). */
|
|
77
|
+
dueTime?: ISOTime;
|
|
78
|
+
/** 어느 공정의 약정인가. 없으면 **모든 공정에** 걸린다(라인 전체가 그 물량을 통과시켜야 한다). */
|
|
79
|
+
operation?: string;
|
|
80
|
+
}
|
|
81
|
+
export interface OperationsCapabilityInput {
|
|
82
|
+
/** 전량 선언 기준 — 이 모델이 가진 모든 자원. */
|
|
83
|
+
capacity: CapacityInput;
|
|
84
|
+
/**
|
|
85
|
+
* **지금 쓸 수 있는 자원만** 남긴 같은 입력. 없으면 `unattainable` 은 `null` 이다.
|
|
86
|
+
*
|
|
87
|
+
* 판정을 여기서 다시 하지 않는 이유: 판정은 한 벌이어야 하고 그 한 벌은 `capabilityOf` 다
|
|
88
|
+
* (고장·보류·교대·휴일·자격 만료·유효기간). 부르는 쪽이 그것으로 걸러 준 자원을 넘긴다.
|
|
89
|
+
*/
|
|
90
|
+
usableCapacity?: CapacityInput;
|
|
91
|
+
window: {
|
|
92
|
+
fromTime: ISOTime;
|
|
93
|
+
toTime: ISOTime;
|
|
94
|
+
};
|
|
95
|
+
calendar?: readonly WorkCalendarEntry[];
|
|
96
|
+
utcOffsetMinutes?: number;
|
|
97
|
+
demands?: readonly CommittedDemand[];
|
|
98
|
+
operationsType?: OperationsType;
|
|
99
|
+
publishedDate?: ISOTime;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* 구간의 **가동 시간** — 1분 간격으로 캘린더에 묻는다.
|
|
103
|
+
*
|
|
104
|
+
* 규칙을 다시 적지 않는다(`inWorkCalendarAt` 한 곳에만 있다). 캘린더가 없으면 종일 가동으로 본다 —
|
|
105
|
+
* 제약이 **선언되지 않은** 것이고, 선언이 없으면 커널은 멈출 이유를 모른다(용량 분석과 같은 규율).
|
|
106
|
+
*
|
|
107
|
+
* 비용은 구간에 비례한다(1년 = 약 53만 번의 값싼 호출). 근사하지 않는 이유: 한 주를 재서 곱하면
|
|
108
|
+
* 공휴일이 사라지고, 그 오차는 조용하다.
|
|
109
|
+
*/
|
|
110
|
+
export declare function workingHoursBetween(calendar: readonly WorkCalendarEntry[] | undefined, fromMs: number, toMs: number, utcOffsetMinutes?: number): number;
|
|
111
|
+
/**
|
|
112
|
+
* 생산 능력 보고서 — 있는 용량 분석 위에 **구간 · 종류 · 약정**을 올린다.
|
|
113
|
+
*
|
|
114
|
+
* 용량 분석을 두 번 부른다(전량 · 가용). 같은 함수를 두 번 부르는 것이 요점이다: 달성불가를 따로
|
|
115
|
+
* 계산하면 규칙이 두 벌이 되고, 두 벌은 어긋난다.
|
|
116
|
+
*/
|
|
117
|
+
export declare function operationsCapabilityOf(input: OperationsCapabilityInput): OperationsCapabilityReport;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 생산 능력 — **ISA-95 Part 4 `OperationsCapability`**: "이 현장이 어느 구간에 얼마나 할 수 있는가."
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 용량 분석만으로는 부족한가 ────────────────────────────────────────────
|
|
5
|
+
* `capacity.ts` 는 이미 네 축(설비·인원·자산·자리) 정상상태 상한과 병목을 낸다. 그런데 그것은 **비율**
|
|
6
|
+
* 이다(시간당 몇 대). 표준이 게시하라는 것은 **구간의 양**이고, 그 양을 **종류로 구분한다.**
|
|
7
|
+
*
|
|
8
|
+
* 종류가 없으면 답할 수 없는 질문이 있다 — 그리고 그것이 이 개념을 쓰는 가장 흔한 질문이다:
|
|
9
|
+
* **"지금 주문을 더 받을 수 있나."** 상한만 있으면 "시간당 12대" 까지고, 이미 약속한 물량을 빼야
|
|
10
|
+
* 비로소 남은 능력이 나온다.
|
|
11
|
+
*
|
|
12
|
+
* ── 표준의 어휘를 그대로 쓴다 ────────────────────────────────────────────────
|
|
13
|
+
* `CapabilityType` 은 자유 문자열이 아니라 **열거형**이다(B2MML-Common.xsd 대조 완료). 그리고 표준은
|
|
14
|
+
* `Total` 을 **두 갈래**로 쪼갠다:
|
|
15
|
+
*
|
|
16
|
+
* · 과거 — `Total = Used + Unused` (쓴 몫 · 못 쓴 몫)
|
|
17
|
+
* · 미래 — `Total = Committed + Available + Unattainable` (약정 · 가용 · 달성불가)
|
|
18
|
+
*
|
|
19
|
+
* 이 보고서는 **미래 쪽**이다. 과거 쪽은 이미 두 곳에 있다(설비 OEE 의 가동/유휴, 호스트의 구간
|
|
20
|
+
* 가동률) — 우리가 "가동률이 두 벌이다" 로 본 것이 여기서 설명된다: **두 벌인 것이 맞고, 표준이 그
|
|
21
|
+
* 둘을 다른 종류로 부른다.** 합칠 일이 아니라 각자 어느 종류인지 밝힐 일이다.
|
|
22
|
+
*
|
|
23
|
+
* `Unattainable` 의 표준 정의는 우리 판정과 정확히 겹친다 — *설비 상태·설비 점유·인원 가용성·자재
|
|
24
|
+
* 가용성 때문에 앞으로 쓸 수 없는 용량.* 그래서 이 모듈은 그것을 **다시 판정하지 않는다**:
|
|
25
|
+
* 부르는 쪽이 "전량 선언 기준" 과 "지금 쓸 수 있는 자원 기준" 두 벌을 주면, 그 **차이가 곧
|
|
26
|
+
* 달성불가**다(§unattainableOf). 판정은 한 벌이어야 하고 그 한 벌은 `capabilityOf` 다.
|
|
27
|
+
*/
|
|
28
|
+
import { analyzeCapacity } from "./capacity.js";
|
|
29
|
+
import { inWorkCalendarAt } from "./contract.js";
|
|
30
|
+
/**
|
|
31
|
+
* 구간의 **가동 시간** — 1분 간격으로 캘린더에 묻는다.
|
|
32
|
+
*
|
|
33
|
+
* 규칙을 다시 적지 않는다(`inWorkCalendarAt` 한 곳에만 있다). 캘린더가 없으면 종일 가동으로 본다 —
|
|
34
|
+
* 제약이 **선언되지 않은** 것이고, 선언이 없으면 커널은 멈출 이유를 모른다(용량 분석과 같은 규율).
|
|
35
|
+
*
|
|
36
|
+
* 비용은 구간에 비례한다(1년 = 약 53만 번의 값싼 호출). 근사하지 않는 이유: 한 주를 재서 곱하면
|
|
37
|
+
* 공휴일이 사라지고, 그 오차는 조용하다.
|
|
38
|
+
*/
|
|
39
|
+
export function workingHoursBetween(calendar, fromMs, toMs, utcOffsetMinutes) {
|
|
40
|
+
if (!(toMs > fromMs))
|
|
41
|
+
return 0;
|
|
42
|
+
if (!calendar?.length)
|
|
43
|
+
return (toMs - fromMs) / 3_600_000;
|
|
44
|
+
let minutes = 0;
|
|
45
|
+
for (let ms = fromMs; ms < toMs; ms += 60_000)
|
|
46
|
+
if (inWorkCalendarAt(calendar, ms, utcOffsetMinutes))
|
|
47
|
+
minutes++;
|
|
48
|
+
return minutes / 60;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 생산 능력 보고서 — 있는 용량 분석 위에 **구간 · 종류 · 약정**을 올린다.
|
|
52
|
+
*
|
|
53
|
+
* 용량 분석을 두 번 부른다(전량 · 가용). 같은 함수를 두 번 부르는 것이 요점이다: 달성불가를 따로
|
|
54
|
+
* 계산하면 규칙이 두 벌이 되고, 두 벌은 어긋난다.
|
|
55
|
+
*/
|
|
56
|
+
export function operationsCapabilityOf(input) {
|
|
57
|
+
const fromMs = Date.parse(input.window.fromTime);
|
|
58
|
+
const toMs = Date.parse(input.window.toTime);
|
|
59
|
+
if (!Number.isFinite(fromMs) || !Number.isFinite(toMs)) {
|
|
60
|
+
throw new Error(`능력의 구간을 읽을 수 없다: ${input.window.fromTime} ~ ${input.window.toTime}`);
|
|
61
|
+
}
|
|
62
|
+
if (toMs <= fromMs)
|
|
63
|
+
throw new Error(`능력의 구간이 뒤집혔다: ${input.window.fromTime} ~ ${input.window.toTime}`);
|
|
64
|
+
const workingHours = workingHoursBetween(input.calendar ?? input.capacity.calendar, fromMs, toMs, input.utcOffsetMinutes);
|
|
65
|
+
const full = analyzeCapacity(input.capacity);
|
|
66
|
+
const usable = input.usableCapacity ? analyzeCapacity(input.usableCapacity) : undefined;
|
|
67
|
+
/** 비율(시간당) → 구간의 양. 상한이 무한(요구가 없는 공정)이면 **모른다**. */
|
|
68
|
+
const amountOf = (perHour) => (Number.isFinite(perHour) ? perHour * workingHours : null);
|
|
69
|
+
const committedOf = (operation) => {
|
|
70
|
+
let sum = 0;
|
|
71
|
+
for (const d of input.demands ?? []) {
|
|
72
|
+
if (d.operation !== undefined && d.operation !== operation)
|
|
73
|
+
continue;
|
|
74
|
+
/* 납기가 없으면 이 구간의 약정이 아니다 — 짐작해서 넣으면 남은 능력이 실제보다 적게 보인다. */
|
|
75
|
+
if (!d.dueTime)
|
|
76
|
+
continue;
|
|
77
|
+
const due = Date.parse(d.dueTime);
|
|
78
|
+
if (!Number.isFinite(due) || due < fromMs || due >= toMs)
|
|
79
|
+
continue;
|
|
80
|
+
sum += Math.max(0, d.remaining);
|
|
81
|
+
}
|
|
82
|
+
return sum;
|
|
83
|
+
};
|
|
84
|
+
const perOperation = full.operations.map(op => {
|
|
85
|
+
const total = amountOf(op.perHour);
|
|
86
|
+
const committed = committedOf(op.operation);
|
|
87
|
+
/*
|
|
88
|
+
* 달성불가 = 전량 기준 − 가용 자원 기준. 자원의 현재 사실을 못 받았으면 **모른다**(0 이 아니다).
|
|
89
|
+
* 음수가 되는 일은 없어야 하지만(가용 ⊆ 전량), 되면 그것을 감추지 않고 0 으로 깎는다 — 부분집합이
|
|
90
|
+
* 아닌 입력을 받은 것이므로 값을 지어내는 대신 아래 `available` 이 어긋남을 드러낸다.
|
|
91
|
+
*/
|
|
92
|
+
let unattainable = null;
|
|
93
|
+
let unattainableBy;
|
|
94
|
+
if (usable) {
|
|
95
|
+
const left = usable.operations.find(o => o.operation === op.operation);
|
|
96
|
+
const usableAmount = amountOf(left?.perHour ?? NaN);
|
|
97
|
+
unattainable = total != null && usableAmount != null ? Math.max(0, total - usableAmount) : null;
|
|
98
|
+
/* 잃은 몫이 있을 때만 원인을 말한다 — 잃은 것이 없는데 "여기부터" 라고 하면 없는 일을 만든다. */
|
|
99
|
+
if (unattainable != null && unattainable > 0 && left?.constraint)
|
|
100
|
+
unattainableBy = left.constraint;
|
|
101
|
+
}
|
|
102
|
+
const available = total == null || unattainable == null ? null : total - committed - unattainable;
|
|
103
|
+
return {
|
|
104
|
+
operation: op.operation,
|
|
105
|
+
total,
|
|
106
|
+
committed,
|
|
107
|
+
unattainable,
|
|
108
|
+
available,
|
|
109
|
+
...(op.constraint ? { constraint: op.constraint } : {}),
|
|
110
|
+
...(unattainableBy ? { unattainableBy } : {})
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
operationsType: input.operationsType ?? 'Production',
|
|
115
|
+
window: { fromTime: input.window.fromTime, toTime: input.window.toTime, workingHours },
|
|
116
|
+
perOperation,
|
|
117
|
+
...(full.bottleneck ? { bottleneck: full.bottleneck } : {}),
|
|
118
|
+
...(input.publishedDate ? { publishedDate: input.publishedDate } : {})
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ScenarioDef } from './contract.ts';
|
|
2
|
+
export type ScenarioValidation = {
|
|
3
|
+
ok: true;
|
|
4
|
+
} | {
|
|
5
|
+
ok: false;
|
|
6
|
+
errorCode: string;
|
|
7
|
+
errorParams?: Record<string, string | number>;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* 시나리오 선언이 실릴 수 있는가.
|
|
11
|
+
*
|
|
12
|
+
* 첫 번째 잘못에서 멈추고 그것을 말한다 — 전부 모아 보고하면 화면이 무엇부터 고칠지 알 수 없다.
|
|
13
|
+
* 생성기가 하나도 없는 것은 **잘못이 아니다**(자극 없이 관측만 하는 시나리오는 성립한다).
|
|
14
|
+
*/
|
|
15
|
+
export declare function validateScenario(def: ScenarioDef | undefined | null): ScenarioValidation;
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
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[];
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
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[];
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
];
|
|
@@ -0,0 +1,15 @@
|
|
|
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[];
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
];
|