@operato/ops-contract 0.7.1 → 0.8.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/dist/actuation.d.ts +157 -0
- package/dist/actuation.js +150 -0
- package/dist/contract.d.ts +1 -1
- package/dist/erp.d.ts +300 -0
- package/dist/erp.js +145 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mes-profile.d.ts +11 -0
- package/dist/mes-profile.js +25 -0
- package/dist-cjs/index.cjs +222 -0
- package/package.json +2 -2
- package/dist/version.d.ts +0 -1
- package/dist/version.js +0 -14
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { ISOTime } from './contract.ts';
|
|
2
|
+
/**
|
|
3
|
+
* 커맨드의 상태.
|
|
4
|
+
*
|
|
5
|
+
* proposed 트윈이 냈다. 아직 아무 일도 일어나지 않았다
|
|
6
|
+
* approved 사람이 승인했다 — 누가 · 언제가 함께 남는다
|
|
7
|
+
* rejected 사람이 거절했다. 여기서 끝난다
|
|
8
|
+
* dispatched 어댑터에 넘겼다. 저쪽이 받았는지는 아직 모른다
|
|
9
|
+
* acked 저쪽이 답했다
|
|
10
|
+
* failed 넘기다 실패했다. 다시 넘길 수 있다
|
|
11
|
+
*/
|
|
12
|
+
export declare const COMMAND_STATE: readonly ["proposed", "approved", "rejected", "dispatched", "acked", "failed"];
|
|
13
|
+
export type CommandState = (typeof COMMAND_STATE)[number];
|
|
14
|
+
/** 이 커맨드를 낸 것이 누구인가 — 사람이 승인할 때 그 판단의 재료가 된다. */
|
|
15
|
+
export declare const COMMAND_ORIGIN: readonly ["operator", "rule", "ai", "schedule"];
|
|
16
|
+
export type CommandOrigin = (typeof COMMAND_ORIGIN)[number];
|
|
17
|
+
/** 사람이 승인하거나 거절한 기록. **재기동을 넘어 살아야 한다.** */
|
|
18
|
+
export interface CommandApproval {
|
|
19
|
+
by: string;
|
|
20
|
+
at: ISOTime;
|
|
21
|
+
note?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 현장에 내리는 조치 하나.
|
|
25
|
+
*
|
|
26
|
+
* `reversible` 의 기본은 **종류가 정한다**(§`CommandTypeSpec.reversible`). 커맨드가 그것을 들고 다니는
|
|
27
|
+
* 이유는 저장되고 되세워지기 때문이다 — 명세를 매번 다시 찾지 않는다.
|
|
28
|
+
*
|
|
29
|
+
* **커맨드는 더 엄하게만 갈 수 있다.** 종류가 「되돌릴 수 있다」고 해도 그 한 건이 되돌릴 수 없다면
|
|
30
|
+
* 그렇게 적을 수 있다(절반 만든 작업의 취소). 반대는 안 된다 — 종류가 되돌릴 수 없다고 한 것을 커맨드가
|
|
31
|
+
* 뒤집으면 게이트가 그 한 건에서만 열리고, 그것이 이 자리에서 가장 나쁜 결과다.
|
|
32
|
+
*/
|
|
33
|
+
export interface TwinCommand {
|
|
34
|
+
id: string;
|
|
35
|
+
/** 어느 트윈의 조치인가. */
|
|
36
|
+
instanceId: string;
|
|
37
|
+
/** 무엇을 하라는 것인가 — 트윈 어휘. 레거시 호출로 옮기는 것은 어댑터의 일이다. */
|
|
38
|
+
type: string;
|
|
39
|
+
payload?: Record<string, unknown>;
|
|
40
|
+
origin: CommandOrigin;
|
|
41
|
+
proposedAt: ISOTime;
|
|
42
|
+
/**
|
|
43
|
+
* 되돌릴 수 있는가. **모르면 적지 않는다** — 없는 것은 「되돌릴 수 없다」로 읽는다.
|
|
44
|
+
* 모름을 「되돌릴 수 있다」로 읽으면 그 커맨드가 승인 없이 나간다.
|
|
45
|
+
*/
|
|
46
|
+
reversible?: boolean;
|
|
47
|
+
state: CommandState;
|
|
48
|
+
approval?: CommandApproval;
|
|
49
|
+
/** 어댑터가 돌려준 것 — 저쪽에서 무엇이 되었는지 되짚을 수 있어야 한다. */
|
|
50
|
+
dispatchRef?: string;
|
|
51
|
+
error?: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 사람이 먼저 봐야 하는가.
|
|
55
|
+
*
|
|
56
|
+
* **모르면 봐야 한다.** `reversible` 이 참이라고 명시된 것만 게이트를 지나지 않는다 — 판단하지 못한
|
|
57
|
+
* 커맨드가 자동으로 나가는 쪽이 반대보다 훨씬 나쁘다.
|
|
58
|
+
*/
|
|
59
|
+
export declare function requiresApproval(command: Pick<TwinCommand, 'reversible'>): boolean;
|
|
60
|
+
/** 상태를 옮기는 사건. */
|
|
61
|
+
export declare const COMMAND_EVENT: readonly ["approve", "reject", "dispatch", "ack", "fail"];
|
|
62
|
+
export type CommandEvent = (typeof COMMAND_EVENT)[number];
|
|
63
|
+
/** 그 전이가 허용되나 — 허용되면 다음 상태, 아니면 `undefined`. */
|
|
64
|
+
export declare function nextCommandState(from: CommandState, event: CommandEvent): CommandState | undefined;
|
|
65
|
+
/** 더 갈 곳이 없는 상태인가 — 목록에서 지워도 되는지 화면이 이것으로 판단한다. */
|
|
66
|
+
export declare function isCommandTerminal(state: CommandState): boolean;
|
|
67
|
+
declare const approvedBrand: unique symbol;
|
|
68
|
+
/**
|
|
69
|
+
* **승인을 지난 커맨드** — 조치를 부르는 함수는 이것만 받는다.
|
|
70
|
+
*
|
|
71
|
+
* 이 표식은 `approveCommand` 로만 붙는다. 그래서 승인 없는 커맨드를 조치에 넘기는 코드는 **컴파일이
|
|
72
|
+
* 되지 않는다.** 부르는 자리마다 참·거짓을 확인하는 방식과 다른 점이 이것이다 — 확인을 빠뜨릴 자리가
|
|
73
|
+
* 없다.
|
|
74
|
+
*
|
|
75
|
+
* 우회하려면 `as` 로 억지로 만들어야 하고, 그것은 검토에서 보인다.
|
|
76
|
+
*/
|
|
77
|
+
export type ApprovedCommand = TwinCommand & {
|
|
78
|
+
readonly [approvedBrand]: true;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* 승인한다 — **승인할 수 없는 것은 던진다.**
|
|
82
|
+
*
|
|
83
|
+
* 조용히 실패하지 않는 이유는, 실패를 답으로 돌려주면 부르는 쪽이 그것을 무시할 수 있기 때문이다.
|
|
84
|
+
* 승인이 안 된 것을 승인된 줄 알고 넘기는 것이 이 자리에서 가장 나쁜 결과다.
|
|
85
|
+
*/
|
|
86
|
+
export declare function approveCommand(command: TwinCommand, approval: CommandApproval): ApprovedCommand;
|
|
87
|
+
/**
|
|
88
|
+
* 되돌릴 수 있는 커맨드를 사람 없이 통과시킨다 — **게이트를 여는 유일한 다른 문**이다.
|
|
89
|
+
*
|
|
90
|
+
* 시뮬레이션과 what-if 가 이 길로 간다. 되돌릴 수 없는 것에 이 함수를 쓰면 던진다 — 그것이 이 문의
|
|
91
|
+
* 뜻이고, 뜻을 어기면 게이트가 없는 것과 같아진다.
|
|
92
|
+
*/
|
|
93
|
+
export declare function passThroughReversible(command: TwinCommand): ApprovedCommand;
|
|
94
|
+
/**
|
|
95
|
+
* 승인 표식이 붙었는지 실행 중에도 본다 — **타입만으로는 부족한 자리가 있다.**
|
|
96
|
+
*
|
|
97
|
+
* 저장소에서 되세운 커맨드는 타입이 지워진 채로 온다(JSON 에는 표식이 없다). 그때 이 함수로 확인하고
|
|
98
|
+
* 다시 표식을 붙인다. 확인 없이 `as` 로 붙이면 재기동이 게이트를 여는 길이 된다.
|
|
99
|
+
*/
|
|
100
|
+
export declare function asApproved(command: TwinCommand): ApprovedCommand;
|
|
101
|
+
/**
|
|
102
|
+
* 조치 한 종류의 명세.
|
|
103
|
+
*
|
|
104
|
+
* ── 왜 필요한가 (2026-08-31, 인티그레이션 레인 지적) ──────────────────────
|
|
105
|
+
* `TwinCommand.type` 이 열린 문자열이었다. 그러면 **어댑터를 만드는 사람이 이름을 지어내고**, 그
|
|
106
|
+
* 이름이 계약에 없으므로 다음 어댑터가 다른 이름을 쓴다. 같은 조치가 두 이름으로 갈린다.
|
|
107
|
+
*
|
|
108
|
+
* ── 왜 전역 목록이 아닌가 ────────────────────────────────────────────────
|
|
109
|
+
* 조치는 도메인마다 다르다 — 제조의 「생산 지시」와 창고의 「오더 보류」는 같은 목록에 있을 것이 아니다.
|
|
110
|
+
* 전역 목록으로 두면 도메인이 늘 때마다 계약을 고치게 된다.
|
|
111
|
+
*
|
|
112
|
+
* 그래서 **도메인 프로파일이 자기 종류를 선언한다**(`MES_COMMANDS` 처럼). bizStep 과 자리 타입을
|
|
113
|
+
* 프로파일이 선언하는 것과 같은 결이다.
|
|
114
|
+
*/
|
|
115
|
+
export interface CommandTypeSpec {
|
|
116
|
+
/** 트윈 어휘의 종류 이름. 점으로 나눈다(`production.order`). */
|
|
117
|
+
type: string;
|
|
118
|
+
label: string;
|
|
119
|
+
/**
|
|
120
|
+
* 이 종류가 되돌릴 수 있는가 — **기본을 정하는 자리**.
|
|
121
|
+
*
|
|
122
|
+
* 커맨드는 이것보다 **더 엄하게만** 갈 수 있다(§`TwinCommand.reversible`). 느슨하게 가는 길을 열면
|
|
123
|
+
* 그것을 내는 코드의 실수 하나가 게이트를 그 한 건에서만 연다.
|
|
124
|
+
*/
|
|
125
|
+
reversible: boolean;
|
|
126
|
+
/**
|
|
127
|
+
* 이 종류가 요구하는 `payload` 칸.
|
|
128
|
+
*
|
|
129
|
+
* 없으면 보내는 쪽이 무엇을 담을지 짐작하고, 받는 어댑터는 없는 칸을 조용히 지나친다.
|
|
130
|
+
*/
|
|
131
|
+
required: readonly string[];
|
|
132
|
+
}
|
|
133
|
+
/** 그 종류의 명세를 찾는다 — 모르는 종류면 `undefined`. */
|
|
134
|
+
export declare function commandTypeOf(specs: readonly CommandTypeSpec[], type: string): CommandTypeSpec | undefined;
|
|
135
|
+
/**
|
|
136
|
+
* 그 종류의 커맨드를 만든다 — **되돌릴 수 있는지를 명세에서 가져온다.**
|
|
137
|
+
*
|
|
138
|
+
* 부르는 쪽이 `reversible` 을 적지 않는다. 적게 두면 그 값이 두 곳에 살고, 내는 코드의 실수 하나가
|
|
139
|
+
* 게이트를 그 한 건에서만 연다.
|
|
140
|
+
*/
|
|
141
|
+
export declare function commandFromSpec(spec: CommandTypeSpec, seed: {
|
|
142
|
+
id: string;
|
|
143
|
+
instanceId: string;
|
|
144
|
+
origin: CommandOrigin;
|
|
145
|
+
proposedAt: ISOTime;
|
|
146
|
+
payload?: Record<string, unknown>;
|
|
147
|
+
}): TwinCommand;
|
|
148
|
+
/** 요구하는 칸 중 없는 것 — 목록으로 낸다(하나씩 고치게 하지 않는다). */
|
|
149
|
+
export declare function missingCommandPayload(spec: CommandTypeSpec, payload: Record<string, unknown> | undefined): string[];
|
|
150
|
+
/**
|
|
151
|
+
* 커맨드가 자기 종류의 명세와 어긋나지 않는가 — **어긋남을 조용히 지나치지 않는다.**
|
|
152
|
+
*
|
|
153
|
+
* 저장본을 손으로 고치거나 옛 코드가 만든 커맨드가 명세와 다른 `reversible` 을 들고 있을 수 있다.
|
|
154
|
+
* 그 한 건에서만 게이트가 열리는 것을 막는다.
|
|
155
|
+
*/
|
|
156
|
+
export declare function commandSpecGaps(specs: readonly CommandTypeSpec[], command: TwinCommand): string[];
|
|
157
|
+
export {};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 커맨드의 상태.
|
|
3
|
+
*
|
|
4
|
+
* proposed 트윈이 냈다. 아직 아무 일도 일어나지 않았다
|
|
5
|
+
* approved 사람이 승인했다 — 누가 · 언제가 함께 남는다
|
|
6
|
+
* rejected 사람이 거절했다. 여기서 끝난다
|
|
7
|
+
* dispatched 어댑터에 넘겼다. 저쪽이 받았는지는 아직 모른다
|
|
8
|
+
* acked 저쪽이 답했다
|
|
9
|
+
* failed 넘기다 실패했다. 다시 넘길 수 있다
|
|
10
|
+
*/
|
|
11
|
+
export const COMMAND_STATE = ['proposed', 'approved', 'rejected', 'dispatched', 'acked', 'failed'];
|
|
12
|
+
/** 이 커맨드를 낸 것이 누구인가 — 사람이 승인할 때 그 판단의 재료가 된다. */
|
|
13
|
+
export const COMMAND_ORIGIN = ['operator', 'rule', 'ai', 'schedule'];
|
|
14
|
+
/**
|
|
15
|
+
* 사람이 먼저 봐야 하는가.
|
|
16
|
+
*
|
|
17
|
+
* **모르면 봐야 한다.** `reversible` 이 참이라고 명시된 것만 게이트를 지나지 않는다 — 판단하지 못한
|
|
18
|
+
* 커맨드가 자동으로 나가는 쪽이 반대보다 훨씬 나쁘다.
|
|
19
|
+
*/
|
|
20
|
+
export function requiresApproval(command) {
|
|
21
|
+
return command?.reversible !== true;
|
|
22
|
+
}
|
|
23
|
+
/* ── 상태 전이 ─────────────────────────────────────────────────────────────── */
|
|
24
|
+
/** 상태를 옮기는 사건. */
|
|
25
|
+
export const COMMAND_EVENT = ['approve', 'reject', 'dispatch', 'ack', 'fail'];
|
|
26
|
+
/**
|
|
27
|
+
* 어느 상태에서 어느 사건이 허용되나 — **표 하나가 전부다.**
|
|
28
|
+
*
|
|
29
|
+
* `if` 사슬로 두면 새 상태를 넣을 때 고칠 자리가 흩어지고, 한 곳을 빠뜨린 것이 「그 전이만 조용히
|
|
30
|
+
* 허용됨」으로 나타난다.
|
|
31
|
+
*/
|
|
32
|
+
const ALLOWED = {
|
|
33
|
+
proposed: { approve: 'approved', reject: 'rejected' },
|
|
34
|
+
approved: { dispatch: 'dispatched', reject: 'rejected' },
|
|
35
|
+
/* 넘기다 실패한 것은 다시 넘길 수 있다 — 승인은 그대로 살아 있다. */
|
|
36
|
+
failed: { dispatch: 'dispatched', reject: 'rejected' },
|
|
37
|
+
dispatched: { ack: 'acked', fail: 'failed' },
|
|
38
|
+
acked: {},
|
|
39
|
+
rejected: {}
|
|
40
|
+
};
|
|
41
|
+
/** 그 전이가 허용되나 — 허용되면 다음 상태, 아니면 `undefined`. */
|
|
42
|
+
export function nextCommandState(from, event) {
|
|
43
|
+
return ALLOWED[from]?.[event];
|
|
44
|
+
}
|
|
45
|
+
/** 더 갈 곳이 없는 상태인가 — 목록에서 지워도 되는지 화면이 이것으로 판단한다. */
|
|
46
|
+
export function isCommandTerminal(state) {
|
|
47
|
+
return Object.keys(ALLOWED[state] ?? {}).length === 0;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 승인한다 — **승인할 수 없는 것은 던진다.**
|
|
51
|
+
*
|
|
52
|
+
* 조용히 실패하지 않는 이유는, 실패를 답으로 돌려주면 부르는 쪽이 그것을 무시할 수 있기 때문이다.
|
|
53
|
+
* 승인이 안 된 것을 승인된 줄 알고 넘기는 것이 이 자리에서 가장 나쁜 결과다.
|
|
54
|
+
*/
|
|
55
|
+
export function approveCommand(command, approval) {
|
|
56
|
+
if (!approval?.by?.trim())
|
|
57
|
+
throw new Error('승인에는 누가 승인했는지가 있어야 한다');
|
|
58
|
+
if (!approval?.at?.trim())
|
|
59
|
+
throw new Error('승인에는 언제 승인했는지가 있어야 한다');
|
|
60
|
+
const to = nextCommandState(command.state, 'approve');
|
|
61
|
+
if (!to)
|
|
62
|
+
throw new Error(`${command.state} 상태의 커맨드는 승인할 수 없다 (${command.id})`);
|
|
63
|
+
return { ...command, state: to, approval };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 되돌릴 수 있는 커맨드를 사람 없이 통과시킨다 — **게이트를 여는 유일한 다른 문**이다.
|
|
67
|
+
*
|
|
68
|
+
* 시뮬레이션과 what-if 가 이 길로 간다. 되돌릴 수 없는 것에 이 함수를 쓰면 던진다 — 그것이 이 문의
|
|
69
|
+
* 뜻이고, 뜻을 어기면 게이트가 없는 것과 같아진다.
|
|
70
|
+
*/
|
|
71
|
+
export function passThroughReversible(command) {
|
|
72
|
+
if (requiresApproval(command)) {
|
|
73
|
+
throw new Error(`되돌릴 수 없는 커맨드는 사람 승인을 지나야 한다 (${command.id} · ${command.type})`);
|
|
74
|
+
}
|
|
75
|
+
const to = nextCommandState(command.state, 'approve');
|
|
76
|
+
if (!to)
|
|
77
|
+
throw new Error(`${command.state} 상태의 커맨드는 넘길 수 없다 (${command.id})`);
|
|
78
|
+
return { ...command, state: to };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 승인 표식이 붙었는지 실행 중에도 본다 — **타입만으로는 부족한 자리가 있다.**
|
|
82
|
+
*
|
|
83
|
+
* 저장소에서 되세운 커맨드는 타입이 지워진 채로 온다(JSON 에는 표식이 없다). 그때 이 함수로 확인하고
|
|
84
|
+
* 다시 표식을 붙인다. 확인 없이 `as` 로 붙이면 재기동이 게이트를 여는 길이 된다.
|
|
85
|
+
*/
|
|
86
|
+
export function asApproved(command) {
|
|
87
|
+
if (command.state !== 'approved' && command.state !== 'failed') {
|
|
88
|
+
throw new Error(`승인되지 않은 커맨드를 조치로 넘길 수 없다 (${command.id} · ${command.state})`);
|
|
89
|
+
}
|
|
90
|
+
if (requiresApproval(command) && !command.approval?.by) {
|
|
91
|
+
throw new Error(`승인 기록이 없는 커맨드를 조치로 넘길 수 없다 (${command.id})`);
|
|
92
|
+
}
|
|
93
|
+
return command;
|
|
94
|
+
}
|
|
95
|
+
/** 그 종류의 명세를 찾는다 — 모르는 종류면 `undefined`. */
|
|
96
|
+
export function commandTypeOf(specs, type) {
|
|
97
|
+
return specs.find(s => s.type === type);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* 그 종류의 커맨드를 만든다 — **되돌릴 수 있는지를 명세에서 가져온다.**
|
|
101
|
+
*
|
|
102
|
+
* 부르는 쪽이 `reversible` 을 적지 않는다. 적게 두면 그 값이 두 곳에 살고, 내는 코드의 실수 하나가
|
|
103
|
+
* 게이트를 그 한 건에서만 연다.
|
|
104
|
+
*/
|
|
105
|
+
export function commandFromSpec(spec, seed) {
|
|
106
|
+
const missing = missingCommandPayload(spec, seed.payload);
|
|
107
|
+
if (missing.length) {
|
|
108
|
+
throw new Error(`${spec.type}: 요구하는 칸이 없다 — ${missing.join(', ')}`);
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
id: seed.id,
|
|
112
|
+
instanceId: seed.instanceId,
|
|
113
|
+
type: spec.type,
|
|
114
|
+
...(seed.payload ? { payload: seed.payload } : {}),
|
|
115
|
+
origin: seed.origin,
|
|
116
|
+
proposedAt: seed.proposedAt,
|
|
117
|
+
reversible: spec.reversible,
|
|
118
|
+
state: 'proposed'
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/** 요구하는 칸 중 없는 것 — 목록으로 낸다(하나씩 고치게 하지 않는다). */
|
|
122
|
+
export function missingCommandPayload(spec, payload) {
|
|
123
|
+
const has = (k) => {
|
|
124
|
+
const v = payload?.[k];
|
|
125
|
+
if (v === undefined || v === null)
|
|
126
|
+
return false;
|
|
127
|
+
return typeof v === 'string' ? v.trim().length > 0 : true;
|
|
128
|
+
};
|
|
129
|
+
return spec.required.filter(k => !has(k));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* 커맨드가 자기 종류의 명세와 어긋나지 않는가 — **어긋남을 조용히 지나치지 않는다.**
|
|
133
|
+
*
|
|
134
|
+
* 저장본을 손으로 고치거나 옛 코드가 만든 커맨드가 명세와 다른 `reversible` 을 들고 있을 수 있다.
|
|
135
|
+
* 그 한 건에서만 게이트가 열리는 것을 막는다.
|
|
136
|
+
*/
|
|
137
|
+
export function commandSpecGaps(specs, command) {
|
|
138
|
+
const spec = commandTypeOf(specs, command.type);
|
|
139
|
+
if (!spec)
|
|
140
|
+
return [`모르는 조치 종류다 — ${command.type}`];
|
|
141
|
+
const gaps = missingCommandPayload(spec, command.payload).map(k => `요구하는 칸이 없다 — ${k}`);
|
|
142
|
+
/*
|
|
143
|
+
* **느슨해진 것만 잡는다.** 종류가 되돌릴 수 없다고 했는데 커맨드가 되돌릴 수 있다고 하면, 그 한 건이
|
|
144
|
+
* 사람 승인을 건너뛴다. 반대(더 엄해진 것)는 정상이다 — 절반 만든 작업의 취소가 그렇다.
|
|
145
|
+
*/
|
|
146
|
+
if (command.reversible === true && spec.reversible === false) {
|
|
147
|
+
gaps.push(`${command.type} 은 되돌릴 수 없는 종류인데 이 커맨드가 되돌릴 수 있다고 한다 — 승인을 건너뛴다`);
|
|
148
|
+
}
|
|
149
|
+
return gaps;
|
|
150
|
+
}
|
package/dist/contract.d.ts
CHANGED
|
@@ -560,7 +560,7 @@ export interface TestSpecification extends EffectivePeriod {
|
|
|
560
560
|
id: string;
|
|
561
561
|
/** 표준 `Description`. i18n 키일 수 있다(사람 언어는 표현 계층이 렌더한다). */
|
|
562
562
|
description?: string;
|
|
563
|
-
/** 표준 `Version` — 같은 시험의
|
|
563
|
+
/** 표준 `Version` — 같은 시험의 버전이 바뀌면 자격의 뜻도 바뀐다(어느 버전으로 검증했나). */
|
|
564
564
|
version?: string;
|
|
565
565
|
/**
|
|
566
566
|
* 판정 기준들 — 표준 `TestSpecificationCriteria`(`maxOccurs="unbounded"`).
|
package/dist/erp.d.ts
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import type { ISOTime } from './contract.ts';
|
|
2
|
+
/**
|
|
3
|
+
* **무엇을 얼마나 만들라** — 이 사실을 나르는 자리가 둘이고, 어휘는 하나다.
|
|
4
|
+
*
|
|
5
|
+
* ── 왜 갈라 두나 (2026-08-31) ────────────────────────────────────────────
|
|
6
|
+
* 같은 사실이 두 방향으로 흐른다.
|
|
7
|
+
*
|
|
8
|
+
* ERP → MES `ProductionScheduleEntry` 저쪽 오더번호와 상태를 함께 든다
|
|
9
|
+
* 트윈 → MES `production.order` 조치 번호가 없다. MES 가 받아서 자기 번호를 붙인다
|
|
10
|
+
*
|
|
11
|
+
* 처음에는 둘을 따로 적었다. ERP 쪽은 `materialId`, 조치 쪽은 `materialDefinitionId` 였다 —
|
|
12
|
+
* **같은 사실에 이름이 두 벌**이었고, 그것은 이 저장소가 반복해서 거절하는 모양이다. 옮기는 코드가
|
|
13
|
+
* 생기고, 그 코드가 언젠가 어긋난다.
|
|
14
|
+
*
|
|
15
|
+
* 이름은 `materialDefinitionId` 로 모았다 — 마스터의 축과 엔티티가 그 이름이다(`MaterialDefinition`).
|
|
16
|
+
* `startTime`·`endTime` 은 ISA-95 `JobOrder` 의 낱말이고 계약이 이미 그것을 쓴다.
|
|
17
|
+
*/
|
|
18
|
+
export interface ProductionDemand {
|
|
19
|
+
/** 무엇을 만드나 — 마스터의 `MaterialDefinition` 식별자. */
|
|
20
|
+
materialDefinitionId: string;
|
|
21
|
+
/** 얼마나 — **받은 단위 그대로 든다.** 우리가 변환하지 않는다(변환계수는 원천의 사실이다). */
|
|
22
|
+
quantity: number;
|
|
23
|
+
uom: string;
|
|
24
|
+
/** 언제까지 — 예정 착수와 예정 완료(표준 `JobOrder.StartTime`·`EndTime`). */
|
|
25
|
+
startTime?: ISOTime;
|
|
26
|
+
endTime?: ISOTime;
|
|
27
|
+
/**
|
|
28
|
+
* 어느 레시피로 만드나 — 품목만으로는 정해지지 않는다(같은 품목에 대체 레시피가 있다).
|
|
29
|
+
* 아는 경우만 싣는다.
|
|
30
|
+
*/
|
|
31
|
+
recipeKey?: string;
|
|
32
|
+
/** 작은 값이 급하다(우리 `JobOrder.Priority` 와 같은 규약). */
|
|
33
|
+
priority?: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* ERP 가 내린 생산 지시 하나.
|
|
37
|
+
*
|
|
38
|
+
* **식별자는 ERP 것이다.** `orderId` 는 저쪽 오더번호이고 우리가 만들지 않는다. 우리 식별자를 여기
|
|
39
|
+
* 담으면 ERP 를 바꾸는 날 지난 기록이 가리킬 것을 잃는다.
|
|
40
|
+
*/
|
|
41
|
+
export interface ProductionScheduleEntry extends ProductionDemand {
|
|
42
|
+
/** ERP 의 오더번호. 이 지시를 가리키는 이름이다. */
|
|
43
|
+
orderId: string;
|
|
44
|
+
/**
|
|
45
|
+
* **이 지시의 판** — 수량 · 납기 · 취소가 바뀌면 올라간다.
|
|
46
|
+
*
|
|
47
|
+
* 덮어쓰지 않고 판마다 남기는 이유는, 이미 올린 실적이 **어느 지시에 대한 것인지** 잃으면 정산이
|
|
48
|
+
* 성립하지 않기 때문이다. ERP 가 판을 말해 주지 않으면 받은 시각으로 대신하지 않는다 — 그것은
|
|
49
|
+
* 우리 사정이지 지시의 사실이 아니다.
|
|
50
|
+
*/
|
|
51
|
+
revision?: string;
|
|
52
|
+
/** 어디서 — ERP 가 말하는 현장과 작업장. SAP 는 플랜트, Oracle 은 Organization 이라 부른다. */
|
|
53
|
+
site?: string;
|
|
54
|
+
workCenter?: string;
|
|
55
|
+
/** 지시 상태 — 아래 `SCHEDULE_STATUS`. 잠긴 것에는 실적을 올릴 수 없다. */
|
|
56
|
+
status: ScheduleStatus;
|
|
57
|
+
recordTime?: ISOTime;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 지시의 상태 — **어느 ERP 에나 있는 여섯**.
|
|
61
|
+
*
|
|
62
|
+
* ERP 마다 이름이 다르고 수가 다르다. SAP 는 `REL`·`TECO`·`CLSD` 로, Oracle 은
|
|
63
|
+
* `Released`·`Complete`·`Closed` 로, 더존은 다른 코드로 말한다. 그 코드를 그대로 들면 어댑터마다
|
|
64
|
+
* 다른 어휘가 화면까지 올라온다.
|
|
65
|
+
*
|
|
66
|
+
* **여섯으로 좁힌 기준은 「부르는 쪽의 판단이 갈리는가」다.** 이름이 달라도 판단이 같으면 한 칸이다.
|
|
67
|
+
*
|
|
68
|
+
* draft 아직 내려오지 않았다. 실적을 올릴 수 없다
|
|
69
|
+
* released 내려왔다. 올릴 수 있다
|
|
70
|
+
* started 일부 올라갔다. 계속 올릴 수 있다
|
|
71
|
+
* completed 생산이 끝났다고 ERP 가 정했다. 더 못 올린다
|
|
72
|
+
* closed 정산까지 닫혔다. 되돌리는 것도 사람이 ERP 에서 한다
|
|
73
|
+
* cancelled 취소됐다
|
|
74
|
+
*
|
|
75
|
+
* `completed` 와 `closed` 를 합치지 않는 이유는 되돌리기가 갈리기 때문이다 — 앞은 ERP 가 열어 주면
|
|
76
|
+
* 되돌릴 수 있고 뒤는 그것도 안 된다.
|
|
77
|
+
*/
|
|
78
|
+
export declare const SCHEDULE_STATUS: readonly ["draft", "released", "started", "completed", "closed", "cancelled"];
|
|
79
|
+
export type ScheduleStatus = (typeof SCHEDULE_STATUS)[number];
|
|
80
|
+
/** 실적을 받을 수 있는 지시인가 — 잠긴 것에 올리면 거절되고, 그 거절은 기다려도 풀리지 않는다. */
|
|
81
|
+
export declare function acceptsPerformance(status: ScheduleStatus): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* MES 가 올리는 생산 실적 하나.
|
|
84
|
+
*
|
|
85
|
+
* 수량 확정과 자재 소비는 ERP 에서 **별개 문서**다(§`erp-sap.md` 4-6). 한쪽만 올라간 상태가 실제로
|
|
86
|
+
* 생기므로 이 레코드는 둘을 함께 들되, 보내는 쪽이 **각각의 결과를 따로 적을 수 있어야** 한다.
|
|
87
|
+
*/
|
|
88
|
+
export interface ProductionPerformanceEntry {
|
|
89
|
+
/** 어느 지시에 대한 것인가 — ERP 의 오더번호. */
|
|
90
|
+
orderId: string;
|
|
91
|
+
/** 그 지시의 어느 판에 대한 것인가. 지시가 판을 말했으면 그대로 옮긴다. */
|
|
92
|
+
revision?: string;
|
|
93
|
+
/**
|
|
94
|
+
* 어느 공정인가 — ERP 의 공정 식별자. **오더 단위로만 받는 ERP 가 있다.**
|
|
95
|
+
*
|
|
96
|
+
* 어댑터가 `performanceGranularity: 'order'` 를 말하면 이 값을 보내도 저쪽이 버린다. 그때 이 칸을
|
|
97
|
+
* 채워 보내는 쪽은 공정별 실적이 올라간 줄 알게 되므로, 어댑터가 **버렸다고 말해야 한다.**
|
|
98
|
+
*/
|
|
99
|
+
operationId?: string;
|
|
100
|
+
/** 만든 것 — 양품과 불량을 나눠 든다. 합쳐 보내면 수율이 사라진다. */
|
|
101
|
+
goodQuantity: number;
|
|
102
|
+
scrapQuantity: number;
|
|
103
|
+
uom: string;
|
|
104
|
+
/** 언제 — 이 실적이 덮는 구간. 마감된 구간이라 미래일 수 없다. */
|
|
105
|
+
from: ISOTime;
|
|
106
|
+
to: ISOTime;
|
|
107
|
+
/** 누가 했나 — ERP 의 인사번호. 「누가 적었나」는 여기 오지 않는다(MES 의 감사 추적이다). */
|
|
108
|
+
personId?: string;
|
|
109
|
+
/** 쓴 자재 — 소비 문서가 된다. 없으면 자재 소비를 올리지 않는다(0 으로 채우지 않는다). */
|
|
110
|
+
materialActual?: MaterialActual[];
|
|
111
|
+
/**
|
|
112
|
+
* **이것이 이 지시의 마지막 실적인가** — ERP 가 이 값으로 오더를 닫는다.
|
|
113
|
+
*
|
|
114
|
+
* 커널이 판정하지 않는다. 「계획 수량에 도달했으니 마지막」은 틀린 규칙이다 — 초과 생산과 미달 종료가
|
|
115
|
+
* 둘 다 정상이다. 사람이 정한 것만 참이다.
|
|
116
|
+
*/
|
|
117
|
+
final?: boolean;
|
|
118
|
+
recordTime?: ISOTime;
|
|
119
|
+
}
|
|
120
|
+
/** 실제로 들어가고 나온 자재 한 줄. */
|
|
121
|
+
export interface MaterialActual {
|
|
122
|
+
/** 마스터의 `MaterialDefinition` 식별자 — 지시와 같은 이름을 쓴다. */
|
|
123
|
+
materialDefinitionId: string;
|
|
124
|
+
quantity: number;
|
|
125
|
+
uom: string;
|
|
126
|
+
/** `consumed` 소비 · `produced` 산출. 부호로 구별하지 않는다 — 음수는 되돌림과 섞인다. */
|
|
127
|
+
direction: 'consumed' | 'produced';
|
|
128
|
+
lot?: string;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* 실적의 **멱등 키** — 같은 실적이 두 번 올라가지 않게.
|
|
132
|
+
*
|
|
133
|
+
* ── 왜 계약에 있나 ───────────────────────────────────────────────────────
|
|
134
|
+
* ERP 확정은 삭제가 없다. 잘못 올린 것은 역분개로만 되돌리고 그 역분개도 장부에 남는다. 그래서 이 키를
|
|
135
|
+
* 만드는 쪽과 확인하는 쪽이 **한 글자까지 같은 규칙**을 써야 한다. 규칙이 두 벌이면 한쪽만 바뀌는 날
|
|
136
|
+
* 같은 실적이 두 번 앉는다.
|
|
137
|
+
*
|
|
138
|
+
* ── 무엇으로 만드나 ──────────────────────────────────────────────────────
|
|
139
|
+
* 오더 · 공정 · 구간. 같은 오더의 같은 공정에서 **같은 구간**의 실적은 하나뿐이다. 수량을 넣지 않는
|
|
140
|
+
* 이유는, 수량을 고쳐 다시 올리는 것이 「다른 실적」이 아니라 「같은 실적의 정정」이기 때문이다 —
|
|
141
|
+
* 정정은 역분개로 처리할 일이지 새 키를 받을 일이 아니다.
|
|
142
|
+
*/
|
|
143
|
+
export declare function performanceKey(e: Pick<ProductionPerformanceEntry, 'orderId' | 'operationId' | 'from' | 'to'>): string;
|
|
144
|
+
/**
|
|
145
|
+
* 자재 소비의 멱등 키 — **확정과 따로 둔다.**
|
|
146
|
+
*
|
|
147
|
+
* 수량 확정과 자재 소비는 ERP 에서 별개 문서이고 한쪽만 성공하는 순간이 실제로 있다. 키를 하나로
|
|
148
|
+
* 쓰면 「확정은 올라갔고 소비는 안 올라간 상태」를 표시할 방법이 없어진다.
|
|
149
|
+
*/
|
|
150
|
+
export declare function materialMovementKey(e: Pick<ProductionPerformanceEntry, 'orderId' | 'operationId' | 'from' | 'to'>): string;
|
|
151
|
+
/**
|
|
152
|
+
* 보내기 전에 스스로 본다 — **틀린 모양이 ERP 에서 걸리면 그때는 이미 늦다.**
|
|
153
|
+
*
|
|
154
|
+
* 거절 이유를 목록으로 낸다. 첫 번째에서 멈추면 사람이 한 번에 하나씩 고치게 된다.
|
|
155
|
+
*/
|
|
156
|
+
export declare function validatePerformance(e: ProductionPerformanceEntry, nowMs: number): string[];
|
|
157
|
+
/**
|
|
158
|
+
* 잘못 올린 실적을 되돌리는 요청 — **삭제가 아니라 역분개다.**
|
|
159
|
+
*
|
|
160
|
+
* ERP 는 확정을 지우지 않는다. 되돌림도 장부에 남고, 그래서 되돌림에도 자기 멱등 키가 필요하다 —
|
|
161
|
+
* 되돌림이 두 번 올라가면 원래 수량이 다시 살아난다.
|
|
162
|
+
*/
|
|
163
|
+
export interface PerformanceReversal {
|
|
164
|
+
/** 되돌릴 실적의 멱등 키(`performanceKey`). */
|
|
165
|
+
performanceKey: string;
|
|
166
|
+
/** ERP 가 돌려준 확정 문서 — 이것이 있어야 무엇을 되돌릴지 저쪽이 안다. */
|
|
167
|
+
confirmationRef: string;
|
|
168
|
+
reason: string;
|
|
169
|
+
decidedBy: string;
|
|
170
|
+
decidedAt: ISOTime;
|
|
171
|
+
}
|
|
172
|
+
/** 되돌림의 멱등 키 — 원래 실적의 키에 표식 하나. */
|
|
173
|
+
export declare function reversalKey(performanceKey: string): string;
|
|
174
|
+
/**
|
|
175
|
+
* **ERP 어댑터가 구현하는 자리** — 부르는 쪽은 이것만 안다.
|
|
176
|
+
*
|
|
177
|
+
* ── 왜 먼저 정하나 (2026-08-31, 사용자 지적) ─────────────────────────────
|
|
178
|
+
* SAP 구현을 먼저 만들면 **SAP 의 모양이 곧 표준이 된다.** 두 번째 ERP(ECC · Oracle · Dynamics)는
|
|
179
|
+
* 자기에게 안 맞는 모양을 물려받고, 그때 고치려면 첫 소비처가 이미 그 위에 서 있다.
|
|
180
|
+
*
|
|
181
|
+
* 그래서 이 자리에는 **어느 ERP에나 있는 것만** 둔다. OData 인지 IDoc 인지, 어느 엔티티에서 꺼내는지는
|
|
182
|
+
* 어댑터의 사정이다.
|
|
183
|
+
*
|
|
184
|
+
* ── 트윈 커넥터와 같은 규율 두 벌 ────────────────────────────────────────
|
|
185
|
+
* `capabilities` 로 무엇을 할 수 있는지 선언하고, **선언만 있고 구현이 없으면 없는 것으로 읽는다** —
|
|
186
|
+
* 선언만 있는 능력은 사용자에게 거짓이 된다(§`ReferenceAdapter.capabilities`).
|
|
187
|
+
* `grounding` 은 「그 선언이 무엇으로 확인됐나」다 — 목에 대고 만든 것과 실 시스템에 붙어 본 것은
|
|
188
|
+
* 다르고, 화면이 그 구분을 보여야 한다.
|
|
189
|
+
*/
|
|
190
|
+
export interface ErpAdapter {
|
|
191
|
+
/** 어느 ERP 인가 — `sap-s4hana` · `sap-ecc` 처럼. */
|
|
192
|
+
readonly type: string;
|
|
193
|
+
readonly label: string;
|
|
194
|
+
readonly capabilities: readonly ErpCapability[];
|
|
195
|
+
/** `mock` 목에 대고 확인 · `vendor-doc` 공개 문서 기준 · `verified` 실 시스템에 붙여 확인. */
|
|
196
|
+
readonly grounding: 'mock' | 'vendor-doc' | 'verified';
|
|
197
|
+
/**
|
|
198
|
+
* **ERP 마다 갈리는 성질** — 능력이 있고 없고와 다른 축이다.
|
|
199
|
+
*
|
|
200
|
+
* 「할 수 있나」가 아니라 「어떻게 하나」이고, 부르는 쪽의 코드가 이 값에 따라 달라진다. 선언하지
|
|
201
|
+
* 않으면 부르는 쪽이 짐작하게 되고, 짐작이 틀린 것은 화면에 드러나지 않는다.
|
|
202
|
+
*/
|
|
203
|
+
readonly traits: ErpTraits;
|
|
204
|
+
/** 붙는지 본다. 값을 바꾸지 않는다. */
|
|
205
|
+
probe(cfg: ErpConfig): Promise<ErpOutcome<{
|
|
206
|
+
detail?: string;
|
|
207
|
+
}>>;
|
|
208
|
+
/** 지시를 읽는다. `since` 이후에 바뀐 것만 — 없으면 어댑터가 정한 창을 쓴다. */
|
|
209
|
+
readSchedules?(cfg: ErpConfig, since?: ISOTime): Promise<ErpOutcome<ProductionScheduleEntry[]>>;
|
|
210
|
+
/**
|
|
211
|
+
* 실적을 올린다. **`key` 로 두 번 올라가지 않게 한다**(§`performanceKey`).
|
|
212
|
+
*
|
|
213
|
+
* 어댑터가 그 키로 이미 올렸는지 먼저 확인해야 한다 — ERP 확정은 삭제가 없다.
|
|
214
|
+
*/
|
|
215
|
+
postPerformance?(cfg: ErpConfig, entry: ProductionPerformanceEntry, key: string): Promise<ErpOutcome<ErpDocumentRef>>;
|
|
216
|
+
/**
|
|
217
|
+
* 자재 소비·산출을 올린다 — **실적과 별개 문서다.** 한쪽만 성공하는 순간이 실제로 있다.
|
|
218
|
+
*/
|
|
219
|
+
postMaterialMovement?(cfg: ErpConfig, entry: ProductionPerformanceEntry, key: string): Promise<ErpOutcome<ErpDocumentRef>>;
|
|
220
|
+
/** 잘못 올린 실적을 되돌린다 — 삭제가 아니라 역분개다. */
|
|
221
|
+
reversePerformance?(cfg: ErpConfig, reversal: PerformanceReversal, key: string): Promise<ErpOutcome<ErpDocumentRef>>;
|
|
222
|
+
/** 마스터를 당긴다 — 구조는 밀지 않고 당겨 간다(사실과 다른 규율). */
|
|
223
|
+
readMaster?(cfg: ErpConfig, axis: ErpMasterAxis): Promise<ErpOutcome<unknown[]>>;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* ERP 마다 갈리는 성질.
|
|
227
|
+
*
|
|
228
|
+
* 실제로 갈리는 것만 둔다 — 아래 셋은 SAP · Oracle · Dynamics · 국내 ERP 를 견주면 서로 다르고,
|
|
229
|
+
* 부르는 쪽이 그것을 모르면 조용히 틀린 결과를 낸다.
|
|
230
|
+
*/
|
|
231
|
+
export interface ErpTraits {
|
|
232
|
+
/**
|
|
233
|
+
* 실적을 어느 단위로 받나.
|
|
234
|
+
*
|
|
235
|
+
* `order` 만 받는 ERP 에 공정별 실적을 올리면 저쪽이 공정을 버리고 합산한다. 부르는 쪽은 공정별로
|
|
236
|
+
* 올라간 줄 알지만 장부에는 오더 하나로 앉는다.
|
|
237
|
+
*/
|
|
238
|
+
performanceGranularity: 'order' | 'operation';
|
|
239
|
+
/**
|
|
240
|
+
* 자재 소비가 실적과 **같은 문서**인가.
|
|
241
|
+
*
|
|
242
|
+
* SAP 는 별개 문서라 한쪽만 성공하는 순간이 있고, 함께 앉는 ERP 는 그 상태가 아예 없다. 별개인 곳에서
|
|
243
|
+
* 하나로 다루면 반쯤 올라간 것을 아무도 못 본다. 함께인 곳에서 둘로 다루면 소비가 두 번 앉는다.
|
|
244
|
+
*/
|
|
245
|
+
materialWithPerformance: boolean;
|
|
246
|
+
/**
|
|
247
|
+
* 되돌리기를 API 로 할 수 있나.
|
|
248
|
+
*
|
|
249
|
+
* `none` 이면 잘못 올린 실적을 **사람이 ERP 화면에서** 지워야 한다. 그 사실을 화면이 말해야 현장이
|
|
250
|
+
* 기다리지 않는다.
|
|
251
|
+
*/
|
|
252
|
+
reversal: 'api' | 'manual' | 'none';
|
|
253
|
+
}
|
|
254
|
+
/** 이 ERP 가 할 수 있다고 선언하는 것. 구현이 없으면 없는 것으로 읽는다. */
|
|
255
|
+
export declare const ERP_CAPABILITY: readonly ["schedule", "performance", "material-movement", "reversal", "master"];
|
|
256
|
+
export type ErpCapability = (typeof ERP_CAPABILITY)[number];
|
|
257
|
+
/** 당겨 올 수 있는 마스터의 축. */
|
|
258
|
+
export declare const ERP_MASTER_AXIS: readonly ["product", "bom", "routing", "work-center"];
|
|
259
|
+
export type ErpMasterAxis = (typeof ERP_MASTER_AXIS)[number];
|
|
260
|
+
/** 연결 설정 — 어댑터마다 다른 칸이 붙는다. 비밀값은 여기 두지 않는다. */
|
|
261
|
+
export interface ErpConfig {
|
|
262
|
+
baseUrl: string;
|
|
263
|
+
/** ERP 안에서 우리가 어느 현장인가. SAP 는 플랜트, Oracle 은 Organization 이다. */
|
|
264
|
+
site?: string;
|
|
265
|
+
[key: string]: unknown;
|
|
266
|
+
}
|
|
267
|
+
/** ERP 가 돌려준 문서 — 되돌리려면 이것이 있어야 저쪽이 무엇을 되돌릴지 안다. */
|
|
268
|
+
export interface ErpDocumentRef {
|
|
269
|
+
/** 그 ERP 의 문서 식별자 원문. 우리가 모양을 정하지 않는다. */
|
|
270
|
+
ref: string;
|
|
271
|
+
postedAt?: ISOTime;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* ERP 호출의 결과 — **다음에 무엇을 할지가 여기서 나온다.**
|
|
275
|
+
*
|
|
276
|
+
* 웹훅 응답에서 세운 것과 같은 규율이다: 「다시 보내면 되는가 · 기다리면 되는가 · 사람을 불러야 하는가」가
|
|
277
|
+
* 하나로 정해져야 한다. 그 판정을 부르는 쪽이 본문을 읽어 짐작하게 두면 어댑터마다 달라진다.
|
|
278
|
+
*/
|
|
279
|
+
export type ErpOutcome<T> = {
|
|
280
|
+
ok: true;
|
|
281
|
+
value: T;
|
|
282
|
+
} | {
|
|
283
|
+
ok: false;
|
|
284
|
+
action: ErpFailureAction;
|
|
285
|
+
error: string;
|
|
286
|
+
};
|
|
287
|
+
/**
|
|
288
|
+
* retry 다시 보내면 될 수 있다 — 저쪽 오류 · 시간 초과
|
|
289
|
+
* wait 지금은 안 되고 기다리면 된다 — 저쪽이 따라오지 못한다
|
|
290
|
+
* stop 다시 보내도 같다 — 잠긴 오더 · 없는 자재 · 권한 없음. 사람이 붙어야 한다
|
|
291
|
+
* duplicate 이미 올라가 있다 — 실패가 아니다. 우리 기록만 맞추면 된다
|
|
292
|
+
*/
|
|
293
|
+
export type ErpFailureAction = 'retry' | 'wait' | 'stop' | 'duplicate';
|
|
294
|
+
/**
|
|
295
|
+
* 선언과 구현이 어긋나지 않는가 — **선언만 있는 능력은 사용자에게 거짓이 된다.**
|
|
296
|
+
*
|
|
297
|
+
* 어긋난 것을 목록으로 낸다. 등록할 때 이것을 지나게 하면 「할 수 있다고 적혀 있는데 안 되는」 어댑터가
|
|
298
|
+
* 화면에 서지 않는다.
|
|
299
|
+
*/
|
|
300
|
+
export declare function erpCapabilityGaps(adapter: ErpAdapter): string[];
|
package/dist/erp.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 지시의 상태 — **어느 ERP 에나 있는 여섯**.
|
|
3
|
+
*
|
|
4
|
+
* ERP 마다 이름이 다르고 수가 다르다. SAP 는 `REL`·`TECO`·`CLSD` 로, Oracle 은
|
|
5
|
+
* `Released`·`Complete`·`Closed` 로, 더존은 다른 코드로 말한다. 그 코드를 그대로 들면 어댑터마다
|
|
6
|
+
* 다른 어휘가 화면까지 올라온다.
|
|
7
|
+
*
|
|
8
|
+
* **여섯으로 좁힌 기준은 「부르는 쪽의 판단이 갈리는가」다.** 이름이 달라도 판단이 같으면 한 칸이다.
|
|
9
|
+
*
|
|
10
|
+
* draft 아직 내려오지 않았다. 실적을 올릴 수 없다
|
|
11
|
+
* released 내려왔다. 올릴 수 있다
|
|
12
|
+
* started 일부 올라갔다. 계속 올릴 수 있다
|
|
13
|
+
* completed 생산이 끝났다고 ERP 가 정했다. 더 못 올린다
|
|
14
|
+
* closed 정산까지 닫혔다. 되돌리는 것도 사람이 ERP 에서 한다
|
|
15
|
+
* cancelled 취소됐다
|
|
16
|
+
*
|
|
17
|
+
* `completed` 와 `closed` 를 합치지 않는 이유는 되돌리기가 갈리기 때문이다 — 앞은 ERP 가 열어 주면
|
|
18
|
+
* 되돌릴 수 있고 뒤는 그것도 안 된다.
|
|
19
|
+
*/
|
|
20
|
+
export const SCHEDULE_STATUS = ['draft', 'released', 'started', 'completed', 'closed', 'cancelled'];
|
|
21
|
+
/** 실적을 받을 수 있는 지시인가 — 잠긴 것에 올리면 거절되고, 그 거절은 기다려도 풀리지 않는다. */
|
|
22
|
+
export function acceptsPerformance(status) {
|
|
23
|
+
return status === 'released' || status === 'started';
|
|
24
|
+
}
|
|
25
|
+
/* ── 멱등 ──────────────────────────────────────────────────────────────────── */
|
|
26
|
+
/**
|
|
27
|
+
* 실적의 **멱등 키** — 같은 실적이 두 번 올라가지 않게.
|
|
28
|
+
*
|
|
29
|
+
* ── 왜 계약에 있나 ───────────────────────────────────────────────────────
|
|
30
|
+
* ERP 확정은 삭제가 없다. 잘못 올린 것은 역분개로만 되돌리고 그 역분개도 장부에 남는다. 그래서 이 키를
|
|
31
|
+
* 만드는 쪽과 확인하는 쪽이 **한 글자까지 같은 규칙**을 써야 한다. 규칙이 두 벌이면 한쪽만 바뀌는 날
|
|
32
|
+
* 같은 실적이 두 번 앉는다.
|
|
33
|
+
*
|
|
34
|
+
* ── 무엇으로 만드나 ──────────────────────────────────────────────────────
|
|
35
|
+
* 오더 · 공정 · 구간. 같은 오더의 같은 공정에서 **같은 구간**의 실적은 하나뿐이다. 수량을 넣지 않는
|
|
36
|
+
* 이유는, 수량을 고쳐 다시 올리는 것이 「다른 실적」이 아니라 「같은 실적의 정정」이기 때문이다 —
|
|
37
|
+
* 정정은 역분개로 처리할 일이지 새 키를 받을 일이 아니다.
|
|
38
|
+
*/
|
|
39
|
+
export function performanceKey(e) {
|
|
40
|
+
const parts = [e.orderId, e.operationId ?? '-', e.from, e.to];
|
|
41
|
+
if (parts.some(p => !String(p ?? '').trim())) {
|
|
42
|
+
throw new Error(`performanceKey: 오더 · 구간이 없으면 멱등 키를 만들 수 없다 — ${JSON.stringify(e)}`);
|
|
43
|
+
}
|
|
44
|
+
return parts.join('|');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 자재 소비의 멱등 키 — **확정과 따로 둔다.**
|
|
48
|
+
*
|
|
49
|
+
* 수량 확정과 자재 소비는 ERP 에서 별개 문서이고 한쪽만 성공하는 순간이 실제로 있다. 키를 하나로
|
|
50
|
+
* 쓰면 「확정은 올라갔고 소비는 안 올라간 상태」를 표시할 방법이 없어진다.
|
|
51
|
+
*/
|
|
52
|
+
export function materialMovementKey(e) {
|
|
53
|
+
return `${performanceKey(e)}|goods`;
|
|
54
|
+
}
|
|
55
|
+
/* ── 검증 ──────────────────────────────────────────────────────────────────── */
|
|
56
|
+
/**
|
|
57
|
+
* 보내기 전에 스스로 본다 — **틀린 모양이 ERP 에서 걸리면 그때는 이미 늦다.**
|
|
58
|
+
*
|
|
59
|
+
* 거절 이유를 목록으로 낸다. 첫 번째에서 멈추면 사람이 한 번에 하나씩 고치게 된다.
|
|
60
|
+
*/
|
|
61
|
+
export function validatePerformance(e, nowMs) {
|
|
62
|
+
const errors = [];
|
|
63
|
+
if (!String(e?.orderId ?? '').trim())
|
|
64
|
+
errors.push('orderId 없음 — 어느 지시에 대한 실적인지 지어낼 수 없다');
|
|
65
|
+
if (!String(e?.uom ?? '').trim())
|
|
66
|
+
errors.push('uom 없음 — 단위 없는 수량은 정산에 쓸 수 없다');
|
|
67
|
+
for (const [name, v] of [
|
|
68
|
+
['goodQuantity', e?.goodQuantity],
|
|
69
|
+
['scrapQuantity', e?.scrapQuantity]
|
|
70
|
+
]) {
|
|
71
|
+
if (typeof v !== 'number' || !Number.isFinite(v))
|
|
72
|
+
errors.push(`${name} 이 수가 아니다`);
|
|
73
|
+
else if (v < 0)
|
|
74
|
+
errors.push(`${name} 이 음수다 — 되돌림은 역분개로 하고 음수 실적으로 하지 않는다`);
|
|
75
|
+
}
|
|
76
|
+
if (typeof e?.goodQuantity === 'number' && typeof e?.scrapQuantity === 'number' && e.goodQuantity + e.scrapQuantity <= 0) {
|
|
77
|
+
errors.push('만든 것이 하나도 없다 — 올릴 실적이 아니다');
|
|
78
|
+
}
|
|
79
|
+
const from = Date.parse(String(e?.from ?? ''));
|
|
80
|
+
const to = Date.parse(String(e?.to ?? ''));
|
|
81
|
+
if (!Number.isFinite(from) || !Number.isFinite(to))
|
|
82
|
+
errors.push('구간이 없다 — from · to 가 있어야 한다');
|
|
83
|
+
else {
|
|
84
|
+
if (to < from)
|
|
85
|
+
errors.push('구간이 거꾸로다');
|
|
86
|
+
/* 마감된 구간의 사실이다. 아직 오지 않은 시각을 정산에 올리면 그 달의 장부가 틀린다. */
|
|
87
|
+
if (to > nowMs)
|
|
88
|
+
errors.push('아직 오지 않은 시각까지의 실적이다');
|
|
89
|
+
}
|
|
90
|
+
for (const m of e?.materialActual ?? []) {
|
|
91
|
+
if (!String(m?.materialDefinitionId ?? '').trim())
|
|
92
|
+
errors.push('자재 줄에 materialDefinitionId 가 없다');
|
|
93
|
+
if (!String(m?.uom ?? '').trim())
|
|
94
|
+
errors.push(`${m?.materialDefinitionId}: uom 없음 — 단위는 ERP 의 사실이므로 지어내지 않는다`);
|
|
95
|
+
if (typeof m?.quantity !== 'number' || !(m.quantity > 0))
|
|
96
|
+
errors.push(`${m?.materialDefinitionId}: 수량이 0 이하다`);
|
|
97
|
+
if (m?.direction !== 'consumed' && m?.direction !== 'produced') {
|
|
98
|
+
errors.push(`${m?.materialDefinitionId}: direction 이 consumed · produced 가 아니다`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return errors;
|
|
102
|
+
}
|
|
103
|
+
/** 되돌림의 멱등 키 — 원래 실적의 키에 표식 하나. */
|
|
104
|
+
export function reversalKey(performanceKey) {
|
|
105
|
+
if (!String(performanceKey ?? '').trim())
|
|
106
|
+
throw new Error('reversalKey: 되돌릴 실적의 키가 없다');
|
|
107
|
+
return `${performanceKey}|reversal`;
|
|
108
|
+
}
|
|
109
|
+
/** 이 ERP 가 할 수 있다고 선언하는 것. 구현이 없으면 없는 것으로 읽는다. */
|
|
110
|
+
export const ERP_CAPABILITY = ['schedule', 'performance', 'material-movement', 'reversal', 'master'];
|
|
111
|
+
/** 당겨 올 수 있는 마스터의 축. */
|
|
112
|
+
export const ERP_MASTER_AXIS = ['product', 'bom', 'routing', 'work-center'];
|
|
113
|
+
/**
|
|
114
|
+
* 선언과 구현이 어긋나지 않는가 — **선언만 있는 능력은 사용자에게 거짓이 된다.**
|
|
115
|
+
*
|
|
116
|
+
* 어긋난 것을 목록으로 낸다. 등록할 때 이것을 지나게 하면 「할 수 있다고 적혀 있는데 안 되는」 어댑터가
|
|
117
|
+
* 화면에 서지 않는다.
|
|
118
|
+
*/
|
|
119
|
+
export function erpCapabilityGaps(adapter) {
|
|
120
|
+
const needs = {
|
|
121
|
+
schedule: 'readSchedules',
|
|
122
|
+
performance: 'postPerformance',
|
|
123
|
+
'material-movement': 'postMaterialMovement',
|
|
124
|
+
reversal: 'reversePerformance',
|
|
125
|
+
master: 'readMaster'
|
|
126
|
+
};
|
|
127
|
+
const gaps = [];
|
|
128
|
+
for (const cap of adapter.capabilities ?? []) {
|
|
129
|
+
const fn = needs[cap];
|
|
130
|
+
if (typeof adapter[fn] !== 'function')
|
|
131
|
+
gaps.push(`${cap} 을 선언했는데 ${String(fn)} 구현이 없다`);
|
|
132
|
+
}
|
|
133
|
+
/* 되돌림을 API 로 한다고 말했으면 그 구현이 있어야 한다. `manual`·`none` 은 없는 것이 정상이다. */
|
|
134
|
+
if (adapter.traits?.reversal === 'api' && typeof adapter.reversePerformance !== 'function') {
|
|
135
|
+
gaps.push('traits.reversal 이 api 인데 reversePerformance 구현이 없다');
|
|
136
|
+
}
|
|
137
|
+
if (adapter.traits?.reversal !== 'api' && adapter.capabilities?.includes('reversal')) {
|
|
138
|
+
gaps.push('reversal 능력을 선언했는데 traits.reversal 이 api 가 아니다 — 둘 중 하나가 틀렸다');
|
|
139
|
+
}
|
|
140
|
+
/* 자재가 실적과 같은 문서인 ERP 에 따로 올리는 문을 두면 소비가 두 번 앉는다. */
|
|
141
|
+
if (adapter.traits?.materialWithPerformance && typeof adapter.postMaterialMovement === 'function') {
|
|
142
|
+
gaps.push('traits.materialWithPerformance 가 참인데 postMaterialMovement 가 따로 있다 — 소비가 두 번 앉는다');
|
|
143
|
+
}
|
|
144
|
+
return gaps;
|
|
145
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/mes-profile.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TwinTypeInfo } from './domain-catalog.ts';
|
|
2
|
+
import type { CommandTypeSpec } from './actuation.ts';
|
|
2
3
|
export declare const MES_BIZSTEP: {
|
|
3
4
|
readonly receiving: "urn:epcglobal:cbv:bizstep:receiving";
|
|
4
5
|
readonly producing: "urn:epcglobal:cbv:bizstep:commissioning";
|
|
@@ -12,3 +13,13 @@ export declare function sgtinUri(companyPrefix: string, itemRef: string, serial:
|
|
|
12
13
|
* 트레일러 제조 라인: 자재→프레임 절단→용접→도장→조립→완성차(kernel ROUTE 와 일치). */
|
|
13
14
|
export declare const MES_LOCATION_TYPES: readonly ["raw-store", "cut-station", "weld-station", "paint-booth", "assembly-line", "fg-store"];
|
|
14
15
|
export declare const MES_TYPES: TwinTypeInfo[];
|
|
16
|
+
/**
|
|
17
|
+
* 제조 도메인이 내리는 조치 — **어댑터가 이름을 지어내지 않게 여기서 정한다.**
|
|
18
|
+
*
|
|
19
|
+
* 전역 목록으로 두지 않는 이유는 조치가 도메인마다 다르기 때문이다. bizStep 과 자리 타입을 이 파일이
|
|
20
|
+
* 선언하는 것과 같은 결이다.
|
|
21
|
+
*
|
|
22
|
+
* 지금 하나다. **소비처가 있는 것만 적는다** — 쓰지 않는 종류를 미리 늘리면 그 이름이 무엇을 뜻하는지
|
|
23
|
+
* 아무도 모르는 채로 계약에 남는다.
|
|
24
|
+
*/
|
|
25
|
+
export declare const MES_COMMANDS: CommandTypeSpec[];
|
package/dist/mes-profile.js
CHANGED
|
@@ -57,3 +57,28 @@ export const MES_TYPES = [
|
|
|
57
57
|
{ key: 'painter', role: 'equipment', label: 'twin.type.painter', standardClass: { isa95: 'Equipment', iso55000: 'Asset' }, identity: { scheme: 'gs1:GIAI' }, capabilities: ['processable', 'operable'] },
|
|
58
58
|
{ key: 'assembler', role: 'equipment', label: 'twin.type.assembler', standardClass: { isa95: 'Equipment', iso55000: 'Asset' }, identity: { scheme: 'gs1:GIAI' }, capabilities: ['processable', 'operable'] }
|
|
59
59
|
];
|
|
60
|
+
/**
|
|
61
|
+
* 제조 도메인이 내리는 조치 — **어댑터가 이름을 지어내지 않게 여기서 정한다.**
|
|
62
|
+
*
|
|
63
|
+
* 전역 목록으로 두지 않는 이유는 조치가 도메인마다 다르기 때문이다. bizStep 과 자리 타입을 이 파일이
|
|
64
|
+
* 선언하는 것과 같은 결이다.
|
|
65
|
+
*
|
|
66
|
+
* 지금 하나다. **소비처가 있는 것만 적는다** — 쓰지 않는 종류를 미리 늘리면 그 이름이 무엇을 뜻하는지
|
|
67
|
+
* 아무도 모르는 채로 계약에 남는다.
|
|
68
|
+
*/
|
|
69
|
+
export const MES_COMMANDS = [
|
|
70
|
+
{
|
|
71
|
+
type: 'production.order',
|
|
72
|
+
label: '생산 지시',
|
|
73
|
+
/*
|
|
74
|
+
* 되돌릴 수 없다. 지시가 나가면 사람과 설비가 그것으로 움직이고, 취소해도 이미 만든 것은 남는다.
|
|
75
|
+
* 그래서 사람 승인을 지난다(§`requiresApproval`).
|
|
76
|
+
*/
|
|
77
|
+
reversible: false,
|
|
78
|
+
/*
|
|
79
|
+
* 단위를 요구하는 이유 — 단위 없는 수량은 지시가 되지 못한다. 우리가 기본 단위를 지어내면 그것이
|
|
80
|
+
* 현장의 단위와 다른 날 수량이 조용히 틀린다.
|
|
81
|
+
*/
|
|
82
|
+
required: ['materialDefinitionId', 'quantity', 'uom']
|
|
83
|
+
}
|
|
84
|
+
];
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -27,6 +27,9 @@ __export(index_exports, {
|
|
|
27
27
|
CAPABILITY_KEYS: () => CAPABILITY_KEYS,
|
|
28
28
|
CBV_BIZSTEP: () => CBV_BIZSTEP,
|
|
29
29
|
CMD: () => CMD,
|
|
30
|
+
COMMAND_EVENT: () => COMMAND_EVENT,
|
|
31
|
+
COMMAND_ORIGIN: () => COMMAND_ORIGIN,
|
|
32
|
+
COMMAND_STATE: () => COMMAND_STATE,
|
|
30
33
|
DISP: () => DISP,
|
|
31
34
|
DISPOSITION_DECISION: () => DISPOSITION_DECISION,
|
|
32
35
|
DOMAIN_CATALOG: () => DOMAIN_CATALOG,
|
|
@@ -40,11 +43,14 @@ __export(index_exports, {
|
|
|
40
43
|
EPCIS_CONTEXT: () => EPCIS_CONTEXT,
|
|
41
44
|
EPCIS_EVENT: () => EPCIS_EVENT,
|
|
42
45
|
EQUIPMENT_LEVEL: () => EQUIPMENT_LEVEL,
|
|
46
|
+
ERP_CAPABILITY: () => ERP_CAPABILITY,
|
|
47
|
+
ERP_MASTER_AXIS: () => ERP_MASTER_AXIS,
|
|
43
48
|
GUARD_PRAGMA: () => GUARD_PRAGMA,
|
|
44
49
|
ILMD_ATTR: () => ILMD_ATTR,
|
|
45
50
|
LOCATION_SATURATION_NEAR: () => LOCATION_SATURATION_NEAR,
|
|
46
51
|
MATERIAL_PROPERTY: () => MATERIAL_PROPERTY,
|
|
47
52
|
MES_BIZSTEP: () => MES_BIZSTEP,
|
|
53
|
+
MES_COMMANDS: () => MES_COMMANDS,
|
|
48
54
|
MES_LOCATION_TYPES: () => MES_LOCATION_TYPES,
|
|
49
55
|
MES_TYPES: () => MES_TYPES,
|
|
50
56
|
METER_DIRECTION: () => METER_DIRECTION,
|
|
@@ -54,6 +60,7 @@ __export(index_exports, {
|
|
|
54
60
|
ORDER_TERMINAL_STATUS: () => ORDER_TERMINAL_STATUS,
|
|
55
61
|
PRIORITY_UNSET: () => PRIORITY_UNSET,
|
|
56
62
|
RETIRED_VOCABULARY: () => RETIRED_VOCABULARY,
|
|
63
|
+
SCHEDULE_STATUS: () => SCHEDULE_STATUS,
|
|
57
64
|
TWIN_AXES: () => TWIN_AXES,
|
|
58
65
|
TWIN_PROPERTIES: () => TWIN_PROPERTIES,
|
|
59
66
|
TWIN_RELATIONS: () => TWIN_RELATIONS,
|
|
@@ -67,10 +74,13 @@ __export(index_exports, {
|
|
|
67
74
|
YARD_BIZSTEP: () => YARD_BIZSTEP,
|
|
68
75
|
YMS_LOCATION_TYPES: () => YMS_LOCATION_TYPES,
|
|
69
76
|
YMS_TYPES: () => YMS_TYPES,
|
|
77
|
+
acceptsPerformance: () => acceptsPerformance,
|
|
70
78
|
activeShiftAt: () => activeShiftAt,
|
|
71
79
|
activeShiftOf: () => activeShiftOf,
|
|
72
80
|
aggregationEvent: () => aggregationEvent,
|
|
73
81
|
analyzeCapacity: () => analyzeCapacity,
|
|
82
|
+
approveCommand: () => approveCommand,
|
|
83
|
+
asApproved: () => asApproved,
|
|
74
84
|
axesOfSystem: () => axesOfSystem,
|
|
75
85
|
axisAppliesTo: () => axisAppliesTo,
|
|
76
86
|
axisInfo: () => axisInfo,
|
|
@@ -82,6 +92,9 @@ __export(index_exports, {
|
|
|
82
92
|
checkSequenceRun: () => checkSequenceRun,
|
|
83
93
|
classClosure: () => classClosure,
|
|
84
94
|
classIdentifierViolation: () => classIdentifierViolation,
|
|
95
|
+
commandFromSpec: () => commandFromSpec,
|
|
96
|
+
commandSpecGaps: () => commandSpecGaps,
|
|
97
|
+
commandTypeOf: () => commandTypeOf,
|
|
85
98
|
commandsOf: () => commandsOf,
|
|
86
99
|
computeOee: () => computeOee,
|
|
87
100
|
conversionFactorOf: () => conversionFactorOf,
|
|
@@ -90,6 +103,7 @@ __export(index_exports, {
|
|
|
90
103
|
dueStatusOf: () => dueStatusOf,
|
|
91
104
|
effectivityAt: () => effectivityAt,
|
|
92
105
|
electricalUpstreamOf: () => electricalUpstreamOf,
|
|
106
|
+
erpCapabilityGaps: () => erpCapabilityGaps,
|
|
93
107
|
expiryFromAttributes: () => expiryFromAttributes,
|
|
94
108
|
gdtiUri: () => gdtiUri,
|
|
95
109
|
generationFractionAt: () => generationFractionAt,
|
|
@@ -111,6 +125,7 @@ __export(index_exports, {
|
|
|
111
125
|
ingestMasterData: () => ingestMasterData,
|
|
112
126
|
ingestOperationalRecords: () => ingestOperationalRecords,
|
|
113
127
|
isAggregationRecord: () => isAggregationRecord,
|
|
128
|
+
isCommandTerminal: () => isCommandTerminal,
|
|
114
129
|
isElectricalLocationType: () => isElectricalLocationType,
|
|
115
130
|
isEnergyBillRecord: () => isEnergyBillRecord,
|
|
116
131
|
isEnergyEquipmentRecord: () => isEnergyEquipmentRecord,
|
|
@@ -137,8 +152,11 @@ __export(index_exports, {
|
|
|
137
152
|
lotFromAttributes: () => lotFromAttributes,
|
|
138
153
|
mapRecord: () => mapRecord,
|
|
139
154
|
mapRecordChecked: () => mapRecordChecked,
|
|
155
|
+
materialMovementKey: () => materialMovementKey,
|
|
140
156
|
meetsTests: () => meetsTests,
|
|
141
157
|
minuteOfDayAt: () => minuteOfDayAt,
|
|
158
|
+
missingCommandPayload: () => missingCommandPayload,
|
|
159
|
+
nextCommandState: () => nextCommandState,
|
|
142
160
|
objectEvent: () => objectEvent,
|
|
143
161
|
objectUri: () => objectUri,
|
|
144
162
|
observationAt: () => observationAt,
|
|
@@ -149,6 +167,8 @@ __export(index_exports, {
|
|
|
149
167
|
outsideLimit: () => outsideLimit,
|
|
150
168
|
parseEpc: () => parseEpc,
|
|
151
169
|
parseIsoDuration: () => parseIsoDuration,
|
|
170
|
+
passThroughReversible: () => passThroughReversible,
|
|
171
|
+
performanceKey: () => performanceKey,
|
|
152
172
|
periodFactId: () => periodFactId,
|
|
153
173
|
priorityRank: () => priorityRank,
|
|
154
174
|
procedureViolations: () => procedureViolations,
|
|
@@ -160,8 +180,10 @@ __export(index_exports, {
|
|
|
160
180
|
relationsFrom: () => relationsFrom,
|
|
161
181
|
relationsTo: () => relationsTo,
|
|
162
182
|
requiredTestsFor: () => requiredTestsFor,
|
|
183
|
+
requiresApproval: () => requiresApproval,
|
|
163
184
|
resolveSubject: () => resolveSubject,
|
|
164
185
|
retiredVocabularyIn: () => retiredVocabularyIn,
|
|
186
|
+
reversalKey: () => reversalKey,
|
|
165
187
|
sgtinClass: () => sgtinClass,
|
|
166
188
|
sgtinUri: () => sgtinUri,
|
|
167
189
|
ssccUri: () => ssccUri,
|
|
@@ -173,6 +195,7 @@ __export(index_exports, {
|
|
|
173
195
|
transformationEvent: () => transformationEvent,
|
|
174
196
|
validateDomainDefinition: () => validateDomainDefinition,
|
|
175
197
|
validateEpcisEvent: () => validateEpcisEvent,
|
|
198
|
+
validatePerformance: () => validatePerformance,
|
|
176
199
|
validateScenario: () => validateScenario,
|
|
177
200
|
webhookSenderAction: () => webhookSenderAction,
|
|
178
201
|
weekdayAt: () => weekdayAt,
|
|
@@ -1201,6 +1224,22 @@ var MES_TYPES = [
|
|
|
1201
1224
|
{ key: "painter", role: "equipment", label: "twin.type.painter", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] },
|
|
1202
1225
|
{ key: "assembler", role: "equipment", label: "twin.type.assembler", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] }
|
|
1203
1226
|
];
|
|
1227
|
+
var MES_COMMANDS = [
|
|
1228
|
+
{
|
|
1229
|
+
type: "production.order",
|
|
1230
|
+
label: "\uC0DD\uC0B0 \uC9C0\uC2DC",
|
|
1231
|
+
/*
|
|
1232
|
+
* 되돌릴 수 없다. 지시가 나가면 사람과 설비가 그것으로 움직이고, 취소해도 이미 만든 것은 남는다.
|
|
1233
|
+
* 그래서 사람 승인을 지난다(§`requiresApproval`).
|
|
1234
|
+
*/
|
|
1235
|
+
reversible: false,
|
|
1236
|
+
/*
|
|
1237
|
+
* 단위를 요구하는 이유 — 단위 없는 수량은 지시가 되지 못한다. 우리가 기본 단위를 지어내면 그것이
|
|
1238
|
+
* 현장의 단위와 다른 날 수량이 조용히 틀린다.
|
|
1239
|
+
*/
|
|
1240
|
+
required: ["materialDefinitionId", "quantity", "uom"]
|
|
1241
|
+
}
|
|
1242
|
+
];
|
|
1204
1243
|
|
|
1205
1244
|
// src/ems-profile.ts
|
|
1206
1245
|
var EMS_LOCATION_TYPES = ["incoming", "feeder", "submeter-zone"];
|
|
@@ -3853,6 +3892,166 @@ function computeOee(c, nowMs) {
|
|
|
3853
3892
|
scrapCount: c.scrapCount
|
|
3854
3893
|
};
|
|
3855
3894
|
}
|
|
3895
|
+
|
|
3896
|
+
// src/erp.ts
|
|
3897
|
+
var SCHEDULE_STATUS = ["draft", "released", "started", "completed", "closed", "cancelled"];
|
|
3898
|
+
function acceptsPerformance(status) {
|
|
3899
|
+
return status === "released" || status === "started";
|
|
3900
|
+
}
|
|
3901
|
+
function performanceKey(e) {
|
|
3902
|
+
const parts = [e.orderId, e.operationId ?? "-", e.from, e.to];
|
|
3903
|
+
if (parts.some((p) => !String(p ?? "").trim())) {
|
|
3904
|
+
throw new Error(`performanceKey: \uC624\uB354 \xB7 \uAD6C\uAC04\uC774 \uC5C6\uC73C\uBA74 \uBA71\uB4F1 \uD0A4\uB97C \uB9CC\uB4E4 \uC218 \uC5C6\uB2E4 \u2014 ${JSON.stringify(e)}`);
|
|
3905
|
+
}
|
|
3906
|
+
return parts.join("|");
|
|
3907
|
+
}
|
|
3908
|
+
function materialMovementKey(e) {
|
|
3909
|
+
return `${performanceKey(e)}|goods`;
|
|
3910
|
+
}
|
|
3911
|
+
function validatePerformance(e, nowMs) {
|
|
3912
|
+
const errors = [];
|
|
3913
|
+
if (!String(e?.orderId ?? "").trim()) errors.push("orderId \uC5C6\uC74C \u2014 \uC5B4\uB290 \uC9C0\uC2DC\uC5D0 \uB300\uD55C \uC2E4\uC801\uC778\uC9C0 \uC9C0\uC5B4\uB0BC \uC218 \uC5C6\uB2E4");
|
|
3914
|
+
if (!String(e?.uom ?? "").trim()) errors.push("uom \uC5C6\uC74C \u2014 \uB2E8\uC704 \uC5C6\uB294 \uC218\uB7C9\uC740 \uC815\uC0B0\uC5D0 \uC4F8 \uC218 \uC5C6\uB2E4");
|
|
3915
|
+
for (const [name, v] of [
|
|
3916
|
+
["goodQuantity", e?.goodQuantity],
|
|
3917
|
+
["scrapQuantity", e?.scrapQuantity]
|
|
3918
|
+
]) {
|
|
3919
|
+
if (typeof v !== "number" || !Number.isFinite(v)) errors.push(`${name} \uC774 \uC218\uAC00 \uC544\uB2C8\uB2E4`);
|
|
3920
|
+
else if (v < 0) errors.push(`${name} \uC774 \uC74C\uC218\uB2E4 \u2014 \uB418\uB3CC\uB9BC\uC740 \uC5ED\uBD84\uAC1C\uB85C \uD558\uACE0 \uC74C\uC218 \uC2E4\uC801\uC73C\uB85C \uD558\uC9C0 \uC54A\uB294\uB2E4`);
|
|
3921
|
+
}
|
|
3922
|
+
if (typeof e?.goodQuantity === "number" && typeof e?.scrapQuantity === "number" && e.goodQuantity + e.scrapQuantity <= 0) {
|
|
3923
|
+
errors.push("\uB9CC\uB4E0 \uAC83\uC774 \uD558\uB098\uB3C4 \uC5C6\uB2E4 \u2014 \uC62C\uB9B4 \uC2E4\uC801\uC774 \uC544\uB2C8\uB2E4");
|
|
3924
|
+
}
|
|
3925
|
+
const from = Date.parse(String(e?.from ?? ""));
|
|
3926
|
+
const to = Date.parse(String(e?.to ?? ""));
|
|
3927
|
+
if (!Number.isFinite(from) || !Number.isFinite(to)) errors.push("\uAD6C\uAC04\uC774 \uC5C6\uB2E4 \u2014 from \xB7 to \uAC00 \uC788\uC5B4\uC57C \uD55C\uB2E4");
|
|
3928
|
+
else {
|
|
3929
|
+
if (to < from) errors.push("\uAD6C\uAC04\uC774 \uAC70\uAFB8\uB85C\uB2E4");
|
|
3930
|
+
if (to > nowMs) errors.push("\uC544\uC9C1 \uC624\uC9C0 \uC54A\uC740 \uC2DC\uAC01\uAE4C\uC9C0\uC758 \uC2E4\uC801\uC774\uB2E4");
|
|
3931
|
+
}
|
|
3932
|
+
for (const m of e?.materialActual ?? []) {
|
|
3933
|
+
if (!String(m?.materialDefinitionId ?? "").trim()) errors.push("\uC790\uC7AC \uC904\uC5D0 materialDefinitionId \uAC00 \uC5C6\uB2E4");
|
|
3934
|
+
if (!String(m?.uom ?? "").trim()) errors.push(`${m?.materialDefinitionId}: uom \uC5C6\uC74C \u2014 \uB2E8\uC704\uB294 ERP \uC758 \uC0AC\uC2E4\uC774\uBBC0\uB85C \uC9C0\uC5B4\uB0B4\uC9C0 \uC54A\uB294\uB2E4`);
|
|
3935
|
+
if (typeof m?.quantity !== "number" || !(m.quantity > 0)) errors.push(`${m?.materialDefinitionId}: \uC218\uB7C9\uC774 0 \uC774\uD558\uB2E4`);
|
|
3936
|
+
if (m?.direction !== "consumed" && m?.direction !== "produced") {
|
|
3937
|
+
errors.push(`${m?.materialDefinitionId}: direction \uC774 consumed \xB7 produced \uAC00 \uC544\uB2C8\uB2E4`);
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
return errors;
|
|
3941
|
+
}
|
|
3942
|
+
function reversalKey(performanceKey2) {
|
|
3943
|
+
if (!String(performanceKey2 ?? "").trim()) throw new Error("reversalKey: \uB418\uB3CC\uB9B4 \uC2E4\uC801\uC758 \uD0A4\uAC00 \uC5C6\uB2E4");
|
|
3944
|
+
return `${performanceKey2}|reversal`;
|
|
3945
|
+
}
|
|
3946
|
+
var ERP_CAPABILITY = ["schedule", "performance", "material-movement", "reversal", "master"];
|
|
3947
|
+
var ERP_MASTER_AXIS = ["product", "bom", "routing", "work-center"];
|
|
3948
|
+
function erpCapabilityGaps(adapter) {
|
|
3949
|
+
const needs = {
|
|
3950
|
+
schedule: "readSchedules",
|
|
3951
|
+
performance: "postPerformance",
|
|
3952
|
+
"material-movement": "postMaterialMovement",
|
|
3953
|
+
reversal: "reversePerformance",
|
|
3954
|
+
master: "readMaster"
|
|
3955
|
+
};
|
|
3956
|
+
const gaps = [];
|
|
3957
|
+
for (const cap of adapter.capabilities ?? []) {
|
|
3958
|
+
const fn = needs[cap];
|
|
3959
|
+
if (typeof adapter[fn] !== "function") gaps.push(`${cap} \uC744 \uC120\uC5B8\uD588\uB294\uB370 ${String(fn)} \uAD6C\uD604\uC774 \uC5C6\uB2E4`);
|
|
3960
|
+
}
|
|
3961
|
+
if (adapter.traits?.reversal === "api" && typeof adapter.reversePerformance !== "function") {
|
|
3962
|
+
gaps.push("traits.reversal \uC774 api \uC778\uB370 reversePerformance \uAD6C\uD604\uC774 \uC5C6\uB2E4");
|
|
3963
|
+
}
|
|
3964
|
+
if (adapter.traits?.reversal !== "api" && adapter.capabilities?.includes("reversal")) {
|
|
3965
|
+
gaps.push("reversal \uB2A5\uB825\uC744 \uC120\uC5B8\uD588\uB294\uB370 traits.reversal \uC774 api \uAC00 \uC544\uB2C8\uB2E4 \u2014 \uB458 \uC911 \uD558\uB098\uAC00 \uD2C0\uB838\uB2E4");
|
|
3966
|
+
}
|
|
3967
|
+
if (adapter.traits?.materialWithPerformance && typeof adapter.postMaterialMovement === "function") {
|
|
3968
|
+
gaps.push("traits.materialWithPerformance \uAC00 \uCC38\uC778\uB370 postMaterialMovement \uAC00 \uB530\uB85C \uC788\uB2E4 \u2014 \uC18C\uBE44\uAC00 \uB450 \uBC88 \uC549\uB294\uB2E4");
|
|
3969
|
+
}
|
|
3970
|
+
return gaps;
|
|
3971
|
+
}
|
|
3972
|
+
|
|
3973
|
+
// src/actuation.ts
|
|
3974
|
+
var COMMAND_STATE = ["proposed", "approved", "rejected", "dispatched", "acked", "failed"];
|
|
3975
|
+
var COMMAND_ORIGIN = ["operator", "rule", "ai", "schedule"];
|
|
3976
|
+
function requiresApproval(command) {
|
|
3977
|
+
return command?.reversible !== true;
|
|
3978
|
+
}
|
|
3979
|
+
var COMMAND_EVENT = ["approve", "reject", "dispatch", "ack", "fail"];
|
|
3980
|
+
var ALLOWED = {
|
|
3981
|
+
proposed: { approve: "approved", reject: "rejected" },
|
|
3982
|
+
approved: { dispatch: "dispatched", reject: "rejected" },
|
|
3983
|
+
/* 넘기다 실패한 것은 다시 넘길 수 있다 — 승인은 그대로 살아 있다. */
|
|
3984
|
+
failed: { dispatch: "dispatched", reject: "rejected" },
|
|
3985
|
+
dispatched: { ack: "acked", fail: "failed" },
|
|
3986
|
+
acked: {},
|
|
3987
|
+
rejected: {}
|
|
3988
|
+
};
|
|
3989
|
+
function nextCommandState(from, event) {
|
|
3990
|
+
return ALLOWED[from]?.[event];
|
|
3991
|
+
}
|
|
3992
|
+
function isCommandTerminal(state) {
|
|
3993
|
+
return Object.keys(ALLOWED[state] ?? {}).length === 0;
|
|
3994
|
+
}
|
|
3995
|
+
function approveCommand(command, approval) {
|
|
3996
|
+
if (!approval?.by?.trim()) throw new Error("\uC2B9\uC778\uC5D0\uB294 \uB204\uAC00 \uC2B9\uC778\uD588\uB294\uC9C0\uAC00 \uC788\uC5B4\uC57C \uD55C\uB2E4");
|
|
3997
|
+
if (!approval?.at?.trim()) throw new Error("\uC2B9\uC778\uC5D0\uB294 \uC5B8\uC81C \uC2B9\uC778\uD588\uB294\uC9C0\uAC00 \uC788\uC5B4\uC57C \uD55C\uB2E4");
|
|
3998
|
+
const to = nextCommandState(command.state, "approve");
|
|
3999
|
+
if (!to) throw new Error(`${command.state} \uC0C1\uD0DC\uC758 \uCEE4\uB9E8\uB4DC\uB294 \uC2B9\uC778\uD560 \uC218 \uC5C6\uB2E4 (${command.id})`);
|
|
4000
|
+
return { ...command, state: to, approval };
|
|
4001
|
+
}
|
|
4002
|
+
function passThroughReversible(command) {
|
|
4003
|
+
if (requiresApproval(command)) {
|
|
4004
|
+
throw new Error(`\uB418\uB3CC\uB9B4 \uC218 \uC5C6\uB294 \uCEE4\uB9E8\uB4DC\uB294 \uC0AC\uB78C \uC2B9\uC778\uC744 \uC9C0\uB098\uC57C \uD55C\uB2E4 (${command.id} \xB7 ${command.type})`);
|
|
4005
|
+
}
|
|
4006
|
+
const to = nextCommandState(command.state, "approve");
|
|
4007
|
+
if (!to) throw new Error(`${command.state} \uC0C1\uD0DC\uC758 \uCEE4\uB9E8\uB4DC\uB294 \uB118\uAE38 \uC218 \uC5C6\uB2E4 (${command.id})`);
|
|
4008
|
+
return { ...command, state: to };
|
|
4009
|
+
}
|
|
4010
|
+
function asApproved(command) {
|
|
4011
|
+
if (command.state !== "approved" && command.state !== "failed") {
|
|
4012
|
+
throw new Error(`\uC2B9\uC778\uB418\uC9C0 \uC54A\uC740 \uCEE4\uB9E8\uB4DC\uB97C \uC870\uCE58\uB85C \uB118\uAE38 \uC218 \uC5C6\uB2E4 (${command.id} \xB7 ${command.state})`);
|
|
4013
|
+
}
|
|
4014
|
+
if (requiresApproval(command) && !command.approval?.by) {
|
|
4015
|
+
throw new Error(`\uC2B9\uC778 \uAE30\uB85D\uC774 \uC5C6\uB294 \uCEE4\uB9E8\uB4DC\uB97C \uC870\uCE58\uB85C \uB118\uAE38 \uC218 \uC5C6\uB2E4 (${command.id})`);
|
|
4016
|
+
}
|
|
4017
|
+
return command;
|
|
4018
|
+
}
|
|
4019
|
+
function commandTypeOf(specs, type) {
|
|
4020
|
+
return specs.find((s) => s.type === type);
|
|
4021
|
+
}
|
|
4022
|
+
function commandFromSpec(spec, seed) {
|
|
4023
|
+
const missing = missingCommandPayload(spec, seed.payload);
|
|
4024
|
+
if (missing.length) {
|
|
4025
|
+
throw new Error(`${spec.type}: \uC694\uAD6C\uD558\uB294 \uCE78\uC774 \uC5C6\uB2E4 \u2014 ${missing.join(", ")}`);
|
|
4026
|
+
}
|
|
4027
|
+
return {
|
|
4028
|
+
id: seed.id,
|
|
4029
|
+
instanceId: seed.instanceId,
|
|
4030
|
+
type: spec.type,
|
|
4031
|
+
...seed.payload ? { payload: seed.payload } : {},
|
|
4032
|
+
origin: seed.origin,
|
|
4033
|
+
proposedAt: seed.proposedAt,
|
|
4034
|
+
reversible: spec.reversible,
|
|
4035
|
+
state: "proposed"
|
|
4036
|
+
};
|
|
4037
|
+
}
|
|
4038
|
+
function missingCommandPayload(spec, payload) {
|
|
4039
|
+
const has = (k) => {
|
|
4040
|
+
const v = payload?.[k];
|
|
4041
|
+
if (v === void 0 || v === null) return false;
|
|
4042
|
+
return typeof v === "string" ? v.trim().length > 0 : true;
|
|
4043
|
+
};
|
|
4044
|
+
return spec.required.filter((k) => !has(k));
|
|
4045
|
+
}
|
|
4046
|
+
function commandSpecGaps(specs, command) {
|
|
4047
|
+
const spec = commandTypeOf(specs, command.type);
|
|
4048
|
+
if (!spec) return [`\uBAA8\uB974\uB294 \uC870\uCE58 \uC885\uB958\uB2E4 \u2014 ${command.type}`];
|
|
4049
|
+
const gaps = missingCommandPayload(spec, command.payload).map((k) => `\uC694\uAD6C\uD558\uB294 \uCE78\uC774 \uC5C6\uB2E4 \u2014 ${k}`);
|
|
4050
|
+
if (command.reversible === true && spec.reversible === false) {
|
|
4051
|
+
gaps.push(`${command.type} \uC740 \uB418\uB3CC\uB9B4 \uC218 \uC5C6\uB294 \uC885\uB958\uC778\uB370 \uC774 \uCEE4\uB9E8\uB4DC\uAC00 \uB418\uB3CC\uB9B4 \uC218 \uC788\uB2E4\uACE0 \uD55C\uB2E4 \u2014 \uC2B9\uC778\uC744 \uAC74\uB108\uB6F4\uB2E4`);
|
|
4052
|
+
}
|
|
4053
|
+
return gaps;
|
|
4054
|
+
}
|
|
3856
4055
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3857
4056
|
0 && (module.exports = {
|
|
3858
4057
|
BIZSTEP,
|
|
@@ -3863,6 +4062,9 @@ function computeOee(c, nowMs) {
|
|
|
3863
4062
|
CAPABILITY_KEYS,
|
|
3864
4063
|
CBV_BIZSTEP,
|
|
3865
4064
|
CMD,
|
|
4065
|
+
COMMAND_EVENT,
|
|
4066
|
+
COMMAND_ORIGIN,
|
|
4067
|
+
COMMAND_STATE,
|
|
3866
4068
|
DISP,
|
|
3867
4069
|
DISPOSITION_DECISION,
|
|
3868
4070
|
DOMAIN_CATALOG,
|
|
@@ -3876,11 +4078,14 @@ function computeOee(c, nowMs) {
|
|
|
3876
4078
|
EPCIS_CONTEXT,
|
|
3877
4079
|
EPCIS_EVENT,
|
|
3878
4080
|
EQUIPMENT_LEVEL,
|
|
4081
|
+
ERP_CAPABILITY,
|
|
4082
|
+
ERP_MASTER_AXIS,
|
|
3879
4083
|
GUARD_PRAGMA,
|
|
3880
4084
|
ILMD_ATTR,
|
|
3881
4085
|
LOCATION_SATURATION_NEAR,
|
|
3882
4086
|
MATERIAL_PROPERTY,
|
|
3883
4087
|
MES_BIZSTEP,
|
|
4088
|
+
MES_COMMANDS,
|
|
3884
4089
|
MES_LOCATION_TYPES,
|
|
3885
4090
|
MES_TYPES,
|
|
3886
4091
|
METER_DIRECTION,
|
|
@@ -3890,6 +4095,7 @@ function computeOee(c, nowMs) {
|
|
|
3890
4095
|
ORDER_TERMINAL_STATUS,
|
|
3891
4096
|
PRIORITY_UNSET,
|
|
3892
4097
|
RETIRED_VOCABULARY,
|
|
4098
|
+
SCHEDULE_STATUS,
|
|
3893
4099
|
TWIN_AXES,
|
|
3894
4100
|
TWIN_PROPERTIES,
|
|
3895
4101
|
TWIN_RELATIONS,
|
|
@@ -3903,10 +4109,13 @@ function computeOee(c, nowMs) {
|
|
|
3903
4109
|
YARD_BIZSTEP,
|
|
3904
4110
|
YMS_LOCATION_TYPES,
|
|
3905
4111
|
YMS_TYPES,
|
|
4112
|
+
acceptsPerformance,
|
|
3906
4113
|
activeShiftAt,
|
|
3907
4114
|
activeShiftOf,
|
|
3908
4115
|
aggregationEvent,
|
|
3909
4116
|
analyzeCapacity,
|
|
4117
|
+
approveCommand,
|
|
4118
|
+
asApproved,
|
|
3910
4119
|
axesOfSystem,
|
|
3911
4120
|
axisAppliesTo,
|
|
3912
4121
|
axisInfo,
|
|
@@ -3918,6 +4127,9 @@ function computeOee(c, nowMs) {
|
|
|
3918
4127
|
checkSequenceRun,
|
|
3919
4128
|
classClosure,
|
|
3920
4129
|
classIdentifierViolation,
|
|
4130
|
+
commandFromSpec,
|
|
4131
|
+
commandSpecGaps,
|
|
4132
|
+
commandTypeOf,
|
|
3921
4133
|
commandsOf,
|
|
3922
4134
|
computeOee,
|
|
3923
4135
|
conversionFactorOf,
|
|
@@ -3926,6 +4138,7 @@ function computeOee(c, nowMs) {
|
|
|
3926
4138
|
dueStatusOf,
|
|
3927
4139
|
effectivityAt,
|
|
3928
4140
|
electricalUpstreamOf,
|
|
4141
|
+
erpCapabilityGaps,
|
|
3929
4142
|
expiryFromAttributes,
|
|
3930
4143
|
gdtiUri,
|
|
3931
4144
|
generationFractionAt,
|
|
@@ -3947,6 +4160,7 @@ function computeOee(c, nowMs) {
|
|
|
3947
4160
|
ingestMasterData,
|
|
3948
4161
|
ingestOperationalRecords,
|
|
3949
4162
|
isAggregationRecord,
|
|
4163
|
+
isCommandTerminal,
|
|
3950
4164
|
isElectricalLocationType,
|
|
3951
4165
|
isEnergyBillRecord,
|
|
3952
4166
|
isEnergyEquipmentRecord,
|
|
@@ -3973,8 +4187,11 @@ function computeOee(c, nowMs) {
|
|
|
3973
4187
|
lotFromAttributes,
|
|
3974
4188
|
mapRecord,
|
|
3975
4189
|
mapRecordChecked,
|
|
4190
|
+
materialMovementKey,
|
|
3976
4191
|
meetsTests,
|
|
3977
4192
|
minuteOfDayAt,
|
|
4193
|
+
missingCommandPayload,
|
|
4194
|
+
nextCommandState,
|
|
3978
4195
|
objectEvent,
|
|
3979
4196
|
objectUri,
|
|
3980
4197
|
observationAt,
|
|
@@ -3985,6 +4202,8 @@ function computeOee(c, nowMs) {
|
|
|
3985
4202
|
outsideLimit,
|
|
3986
4203
|
parseEpc,
|
|
3987
4204
|
parseIsoDuration,
|
|
4205
|
+
passThroughReversible,
|
|
4206
|
+
performanceKey,
|
|
3988
4207
|
periodFactId,
|
|
3989
4208
|
priorityRank,
|
|
3990
4209
|
procedureViolations,
|
|
@@ -3996,8 +4215,10 @@ function computeOee(c, nowMs) {
|
|
|
3996
4215
|
relationsFrom,
|
|
3997
4216
|
relationsTo,
|
|
3998
4217
|
requiredTestsFor,
|
|
4218
|
+
requiresApproval,
|
|
3999
4219
|
resolveSubject,
|
|
4000
4220
|
retiredVocabularyIn,
|
|
4221
|
+
reversalKey,
|
|
4001
4222
|
sgtinClass,
|
|
4002
4223
|
sgtinUri,
|
|
4003
4224
|
ssccUri,
|
|
@@ -4009,6 +4230,7 @@ function computeOee(c, nowMs) {
|
|
|
4009
4230
|
transformationEvent,
|
|
4010
4231
|
validateDomainDefinition,
|
|
4011
4232
|
validateEpcisEvent,
|
|
4233
|
+
validatePerformance,
|
|
4012
4234
|
validateScenario,
|
|
4013
4235
|
webhookSenderAction,
|
|
4014
4236
|
weekdayAt,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@operato/ops-contract",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Operations domain contract — the standard vocabulary that producers and readers agree on (EPCIS 2.0/GS1, ISA-95, IEC 61850/ISO 50001). Types, guards, validation. No state, no engine.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist-cjs/index.cjs",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"dist-cjs"
|
|
23
23
|
],
|
|
24
24
|
"scripts": {
|
|
25
|
-
"build": "npm run build:esm && npm run build:cjs",
|
|
25
|
+
"build": "rm -rf dist dist-cjs && npm run build:esm && npm run build:cjs",
|
|
26
26
|
"build:esm": "tsc -p tsconfig.build.json",
|
|
27
27
|
"build:cjs": "esbuild src/index.ts --bundle --platform=node --format=cjs --outfile=dist-cjs/index.cjs && esbuild src/webhook-signature.ts --bundle --platform=node --format=cjs --outfile=dist-cjs/webhook-signature.cjs",
|
|
28
28
|
"typecheck": "tsc -p tsconfig.check.json",
|
package/dist/version.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const CONTRACT_VERSION = "0.7.0";
|
package/dist/version.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* 이 계약의 **판**.
|
|
3
|
-
*
|
|
4
|
-
* ── 왜 내보내나 (2026-08-30) ─────────────────────────────────────────────
|
|
5
|
-
* 소비처가 「내가 어느 판의 계약을 지키고 있나」를 사실로 남겨야 한다. MES 의 아웃박스가 그 값을
|
|
6
|
-
* 레코드에 싣는데, 계약이 내보내지 않아 **상수로 손으로 적고 있었다.** `exports` 맵이 `package.json`
|
|
7
|
-
* 을 막아 런타임에 읽을 수도 없다.
|
|
8
|
-
*
|
|
9
|
-
* 손으로 적은 판은 계약이 올라갈 때 같이 올라가지 않는다. 그러면 그 값은 「지킨다고 말한 판」이지
|
|
10
|
-
* 「실제로 지킨 판」이 아니게 되고, 그것이 어긋난 날 아무도 모른다.
|
|
11
|
-
*
|
|
12
|
-
* **정본은 `package.json` 이다.** 이 값이 그것과 같은지는 시험이 지킨다(§`version-matches`).
|
|
13
|
-
*/
|
|
14
|
-
export const CONTRACT_VERSION = '0.7.0';
|