@operato/twin-kernel 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/class-master-table.d.ts +1 -1
- package/dist/counterfactual.d.ts +1 -1
- package/dist/divergence.d.ts +1 -1
- package/dist/divergence.js +1 -1
- package/dist/ems-kernel.d.ts +1 -1
- package/dist/ems-kernel.js +4 -4
- package/dist/event-journal.d.ts +1 -1
- package/dist/flow-engine.d.ts +6 -6
- package/dist/flow-engine.js +8 -8
- package/dist/forecast.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/job-response.d.ts +1 -1
- package/dist/kernel.d.ts +1 -1
- package/dist/kernel.js +4 -4
- package/dist/make-to-order.d.ts +1 -1
- package/dist/mes-kernel.d.ts +3 -3
- package/dist/mes-kernel.js +4 -4
- package/dist/observed-reducer.d.ts +2 -2
- package/dist/observed-reducer.js +3 -3
- package/dist/runtime.d.ts +1 -1
- package/dist/task-fold.d.ts +1 -1
- package/dist/twin-observer.d.ts +1 -1
- package/dist/yms-kernel.d.ts +1 -1
- package/dist/yms-kernel.js +3 -3
- package/dist-cjs/index.cjs +15 -15
- package/package.json +2 -2
- package/dist/capability.d.ts +0 -142
- package/dist/capability.js +0 -127
- package/dist/capacity.d.ts +0 -99
- package/dist/capacity.js +0 -172
- package/dist/contract.d.ts +0 -3489
- package/dist/contract.js +0 -1216
- package/dist/domain-catalog.d.ts +0 -280
- package/dist/domain-catalog.js +0 -322
- package/dist/domain-definition.d.ts +0 -306
- package/dist/domain-definition.js +0 -102
- package/dist/ems-profile.d.ts +0 -147
- package/dist/ems-profile.js +0 -367
- package/dist/energy-ingest.d.ts +0 -214
- package/dist/energy-ingest.js +0 -801
- package/dist/epcis.d.ts +0 -458
- package/dist/epcis.js +0 -640
- package/dist/face2-adapter.d.ts +0 -191
- package/dist/face2-adapter.js +0 -284
- package/dist/iso-duration.d.ts +0 -5
- package/dist/iso-duration.js +0 -43
- package/dist/master-data.d.ts +0 -80
- package/dist/master-data.js +0 -168
- package/dist/mes-profile.d.ts +0 -14
- package/dist/mes-profile.js +0 -59
- package/dist/operational-ingest.d.ts +0 -44
- package/dist/operational-ingest.js +0 -379
- package/dist/operations-capability.d.ts +0 -117
- package/dist/operations-capability.js +0 -120
- package/dist/scenario-validate.d.ts +0 -15
- package/dist/scenario-validate.js +0 -72
- package/dist/vocabulary.d.ts +0 -28
- package/dist/vocabulary.js +0 -81
- package/dist/wms-profile.d.ts +0 -20
- package/dist/wms-profile.js +0 -58
- package/dist/yms-profile.d.ts +0 -15
- package/dist/yms-profile.js +0 -39
|
@@ -1,379 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* 운영 사실 인제스트 — **작업·설비·사람·자산·오더·품질이 들어오는 문.** (ADR-0029 어휘 넓히기)
|
|
3
|
-
*
|
|
4
|
-
* ── 무엇이 없었나 (2026-08-19) ──────────────────────────────────────────────
|
|
5
|
-
* 관측 리듀서는 이 여섯을 **이미 다룬다**(`observed-reducer.ts` 의 `OP_EVENT.*` 분기). 그런데 라이브
|
|
6
|
-
* 인제스트 문은 어휘를 셋만 알았다: EPCIS 품목 사실, 에너지 계량, 설비 에너지 상태. 그래서 원본이
|
|
7
|
-
* 「이 작업이 끝났다」·「이 설비가 고장이다」를 말할 **길이 없었다** — 넣으면 `epc` 가 없어 EPCIS
|
|
8
|
-
* 검증에서 거부됐다.
|
|
9
|
-
*
|
|
10
|
-
* 그 결과가 이 프로젝트가 가장 싫어하는 모양이었다: **시뮬만 아는 상태.** 시뮬 커널은 작업과 설비를
|
|
11
|
-
* 알고 미러는 영원히 몰랐다. 그러면 같은 화면이 두 구동에서 다른 것을 말하고, 미러 위에 세운 예측은
|
|
12
|
-
* 「진행 중인 일이 하나도 없는 현장」에서 출발한다.
|
|
13
|
-
*
|
|
14
|
-
* ── 어휘는 델타의 이름이다 ──────────────────────────────────────────────────
|
|
15
|
-
* 필드 이름을 새로 짓지 않는다. `TaskStatusDelta`·`EquipmentStatusDelta`… 가 이미 계약이고, 리듀서가
|
|
16
|
-
* 그 이름으로 읽는다. 여기서 다른 이름을 받아 옮기면 **같은 사실에 두 어휘**가 생긴다(에너지가 그
|
|
17
|
-
* 규율을 먼저 세웠다: "필드 이름이 계약이다").
|
|
18
|
-
*
|
|
19
|
-
* ── 무엇을 거부하나 ─────────────────────────────────────────────────────────
|
|
20
|
-
* 지어낼 수 없는 것이 빠지면 거부한다 — 정체(누구의 상태인가)와 상태다. 그리고 **계산할 수 없는 낱말**도
|
|
21
|
-
* 거부한다: 설비 상태를 `'RUNNING'` 으로 받으면 아무 오류 없이 가동률이 0% 가 되고(누적기는 `busy`·
|
|
22
|
-
* `down` 만 센다), 사람이 `'available'` 이면 배정에서 조용히 사라진다. 그 실패는 화면에서 「일이 없는
|
|
23
|
-
* 공장」으로 보이고 원인을 되짚을 수 없다. 그래서 커널이 다룰 수 있는 낱말만 받고, **받는 낱말을 이유에
|
|
24
|
-
* 적어** 커넥터가 매핑을 고칠 수 있게 한다(매핑=밖, 검증=커널).
|
|
25
|
-
*
|
|
26
|
-
* 오더의 상태·종류는 **열려 있다** — 도메인이 소유한다(`picking`·`packed`·`shipped`…). 커널이 그 낱말로
|
|
27
|
-
* 무엇을 계산하지 않으므로 닫을 근거가 없다.
|
|
28
|
-
*
|
|
29
|
-
* ── 파생은 받아도 커널이 다시 계산한다 ──────────────────────────────────────
|
|
30
|
-
* 작업의 진척(`progress`)은 계약에 있어 받지만, 상태에 앉는 값은 커널이 **소요·남은 시간에서 다시
|
|
31
|
-
* 계산한 것**이다(`progressOf`). 그러니 원본이 진척을 보이게 하려면 `durationMs`·`remainingMs` 를 보내야
|
|
32
|
-
* 한다 — 파생을 사실로 삼지 않는 규율이고, 이 문을 붙이는 사람이 알아야 하는 사실이라 여기 적는다.
|
|
33
|
-
*
|
|
34
|
-
* 모르는 필드는 **조용히 버리지 않고 거부한다.** `taskID` 처럼 한 글자 틀린 이름은 통과시키면 영원히
|
|
35
|
-
* 보이지 않는 손실이 된다(이 문에는 아직 옛 발신자가 없어 호환 부담도 없다).
|
|
36
|
-
*/
|
|
37
|
-
import { OP_EVENT } from "./contract.js";
|
|
38
|
-
/**
|
|
39
|
-
* 닫아 둔 낱말과 그 이유.
|
|
40
|
-
* · 작업 상태 — 성과 폴드가 `completed`·`in-progress` 로 갈린다(`kpi-fold`).
|
|
41
|
-
* · 설비 상태 — OEE 누적기가 `busy`·`down` 만 센다. 그 밖의 낱말은 가동률 0% 로 조용히 앉는다.
|
|
42
|
-
* · 사람·자산 상태 — 배정이 `idle` 을 찾는다. 다른 낱말이면 있는 자원이 없는 것이 된다.
|
|
43
|
-
*/
|
|
44
|
-
const TASK_STATUS = ['created', 'assigned', 'in-progress', 'completed'];
|
|
45
|
-
const EQUIPMENT_STATUS = ['idle', 'busy', 'down'];
|
|
46
|
-
const PERSON_STATUS = ['idle', 'busy'];
|
|
47
|
-
const ASSET_STATUS = ['idle', 'in-use'];
|
|
48
|
-
const SPECS = {
|
|
49
|
-
task: {
|
|
50
|
-
eventType: OP_EVENT.task,
|
|
51
|
-
identity: 'taskId',
|
|
52
|
-
/* 종류가 없으면 성과를 종류별로 모을 수 없고(선언된 시간·수율이 종류로 붙는다) 지어낼 수도 없다. */
|
|
53
|
-
required: ['taskId', 'kind', 'status'],
|
|
54
|
-
fields: {
|
|
55
|
-
taskId: 'string', kind: 'string', status: 'string', fromNode: 'string', toNode: 'string',
|
|
56
|
-
itemRefs: 'string[]', resourceRef: 'string', resources: 'string[]', personnel: 'string[]', assets: 'string[]',
|
|
57
|
-
orderId: 'string', intent: 'string', progress: 'number', remainingMs: 'number', durationMs: 'number',
|
|
58
|
-
startedAtSimMs: 'number', outcome: 'string', priority: 'number', startTime: 'string', endTime: 'string',
|
|
59
|
-
materialActual: 'object[]', recordTime: 'string'
|
|
60
|
-
},
|
|
61
|
-
enums: {
|
|
62
|
-
status: TASK_STATUS,
|
|
63
|
-
intent: ['transport', 'process', 'dwell'],
|
|
64
|
-
/* 품질 판정은 **있었던 작업만** — 없음은 「양품」이 아니라 「판정하지 않았다」다. */
|
|
65
|
-
outcome: ['good', 'scrap']
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
equipment: {
|
|
69
|
-
eventType: OP_EVENT.equipment,
|
|
70
|
-
identity: 'moverId', // vocabulary-guard: allow 저널 와이어 필드(델타의 이름이 계약이다)
|
|
71
|
-
required: ['moverId', 'kind', 'status'], // vocabulary-guard: allow 위와 같은 이유
|
|
72
|
-
fields: {
|
|
73
|
-
moverId: 'string', kind: 'string', status: 'string', location: 'string', homeLocation: 'string', // vocabulary-guard: allow
|
|
74
|
-
taskId: 'string', held: 'boolean', effectiveStart: 'string', effectiveEnd: 'string', recordTime: 'string',
|
|
75
|
-
/* 이동 구간 — 실 시스템도 줄 수 있는 사실이다(AGV·RTLS 가 출발·도착·소요를 낸다). 안쪽 필드까지
|
|
76
|
-
재검사하지는 않는다: 그 모양은 `EquipmentMotion` 계약이고, 여기서 두 번 지키면 두 벌이 된다. */
|
|
77
|
-
motion: 'object'
|
|
78
|
-
},
|
|
79
|
-
enums: { status: EQUIPMENT_STATUS }
|
|
80
|
-
},
|
|
81
|
-
person: {
|
|
82
|
-
eventType: OP_EVENT.person,
|
|
83
|
-
identity: 'personId',
|
|
84
|
-
required: ['personId', 'status'],
|
|
85
|
-
fields: {
|
|
86
|
-
personId: 'string', status: 'string', personnelClassIds: 'string[]', taskId: 'string', location: 'string',
|
|
87
|
-
offShift: 'boolean', effectiveStart: 'string', effectiveEnd: 'string', recordTime: 'string'
|
|
88
|
-
},
|
|
89
|
-
enums: { status: PERSON_STATUS }
|
|
90
|
-
},
|
|
91
|
-
asset: {
|
|
92
|
-
eventType: OP_EVENT.asset,
|
|
93
|
-
identity: 'assetId',
|
|
94
|
-
required: ['assetId', 'status'],
|
|
95
|
-
fields: {
|
|
96
|
-
assetId: 'string', status: 'string', assetClassIds: 'string[]', location: 'string', taskId: 'string',
|
|
97
|
-
carrying: 'string', effectiveStart: 'string', effectiveEnd: 'string', recordTime: 'string'
|
|
98
|
-
},
|
|
99
|
-
enums: { status: ASSET_STATUS }
|
|
100
|
-
},
|
|
101
|
-
order: {
|
|
102
|
-
eventType: OP_EVENT.order,
|
|
103
|
-
identity: 'orderId',
|
|
104
|
-
/*
|
|
105
|
-
* 요청량·이행량을 **함께** 받는다. 없으면 리듀서가 진척을 0 으로 적는데(`requested ? … : 0`),
|
|
106
|
-
* 그것은 「모른다」가 아니라 「아무것도 안 됐다」로 읽힌다 — 결측을 0 으로 메우지 않는다.
|
|
107
|
-
*/
|
|
108
|
-
required: ['orderId', 'kind', 'status', 'requested', 'fulfilled'],
|
|
109
|
-
fields: {
|
|
110
|
-
orderId: 'string', kind: 'string', status: 'string', requested: 'number', fulfilled: 'number',
|
|
111
|
-
gtin: 'string', held: 'boolean', lines: 'object[]', priority: 'number', startTime: 'string', endTime: 'string',
|
|
112
|
-
allocated: 'string[]', bizTransaction: 'string', operationsRequestId: 'string', dockDoor: 'string', windowStartMs: 'number', recordTime: 'string'
|
|
113
|
-
}
|
|
114
|
-
/* 상태·종류는 도메인이 소유한다 — 닫지 않는다. */
|
|
115
|
-
},
|
|
116
|
-
quality: {
|
|
117
|
-
eventType: OP_EVENT.quality,
|
|
118
|
-
identity: 'moverId', // vocabulary-guard: allow 저널 와이어 필드
|
|
119
|
-
/* 누적 카운터가 없으면 OEE 가 양품률을 못 센다 — 판정 하나만으로는 비율이 나오지 않는다. */
|
|
120
|
-
required: ['moverId', 'good', 'goodCount', 'scrapCount'], // vocabulary-guard: allow
|
|
121
|
-
fields: { moverId: 'string', good: 'boolean', goodCount: 'number', scrapCount: 'number', recordTime: 'string' } // vocabulary-guard: allow
|
|
122
|
-
},
|
|
123
|
-
/*
|
|
124
|
-
* **시험 결과** — 대상을 가리켜 들어온다(표준 `TestResult.TestableObjectID`).
|
|
125
|
-
*
|
|
126
|
-
* `result` 를 **요구하지 않는다**: 재기만 하고 판정하지 않는 원천이 정상이고, 요구하면 그 원천의
|
|
127
|
-
* 사실을 아예 담을 수 없다(§`TestResult.result`). 비어 있으면 커널이 선언된 기준으로 판정하고
|
|
128
|
-
* `derived` 를 세운다 — 판정하지 못하면 비워 둔다.
|
|
129
|
-
*
|
|
130
|
-
* `propertyMeasurements` 안쪽은 재검사하지 않는다 — 그 모양은 `PropertyMeasurement` 계약이고,
|
|
131
|
-
* 여기서 두 번 지키면 두 벌이 된다(설비 `motion` 과 같은 규율).
|
|
132
|
-
*/
|
|
133
|
-
test: {
|
|
134
|
-
eventType: OP_EVENT.test,
|
|
135
|
-
identity: 'testableObjectId',
|
|
136
|
-
/* 무엇을 어느 기준으로 시험했나 — 둘 중 하나가 없으면 그 결과는 아무 데도 붙지 못한다. */
|
|
137
|
-
required: ['testableObjectId', 'specId'],
|
|
138
|
-
fields: {
|
|
139
|
-
testableObjectId: 'string', specId: 'string', result: 'string', at: 'string', expiresAt: 'string',
|
|
140
|
-
derived: 'boolean', propertyMeasurements: 'object[]', recordTime: 'string'
|
|
141
|
-
},
|
|
142
|
-
enums: { result: ['pass', 'fail'] }
|
|
143
|
-
},
|
|
144
|
-
/*
|
|
145
|
-
* **자리의 물리 관측** — 어느 자리의 어느 속성을 언제 얼마로 쟀나.
|
|
146
|
-
*
|
|
147
|
-
* 값에 **단위를 함께** 받는다(`ValueType`). 단위 없는 물리량은 판정의 재료가 못 된다 — 3 이 섭씨인지
|
|
148
|
-
* 화씨인지 모르면 어떤 기준으로도 판정할 수 없다. 다만 **요구하지는 않는다**: 단위를 비우는 실 원본이
|
|
149
|
-
* 흔하고, 요구하면 그 원본의 관측을 아예 담지 못한다(그때 판정은 커널이 거부한다 — `outsideLimit`).
|
|
150
|
-
*
|
|
151
|
-
* `effectiveTime` 도 요구하지 않는다. 없으면 봉투의 시각이 그 자리를 대신한다(§`resolve`) — 원본이
|
|
152
|
-
* 시각을 말하지 않는 수기 점검이 실재하고, 그때 지어낸 시각보다 폴링 시각이 정직하다.
|
|
153
|
-
*/
|
|
154
|
-
/*
|
|
155
|
-
* **이 목록이 전부다** — 연결된 시스템이 현재 목록을 한 바퀴 다 보낸 뒤 알린다.
|
|
156
|
-
*
|
|
157
|
-
* 둘 다 요구한다: 어느 목록인지와 그 주기를 **시작한** 시각. 시각이 없으면 무엇을 지울지 가릴 수
|
|
158
|
-
* 없고, 그때 지우면 전부 지운다 — 받지 않는 것이 옳다.
|
|
159
|
-
*/
|
|
160
|
-
complete: {
|
|
161
|
-
eventType: OP_EVENT.complete,
|
|
162
|
-
identity: 'completeAxis',
|
|
163
|
-
required: ['completeAxis', 'since'],
|
|
164
|
-
fields: { completeAxis: 'string', since: 'string', recordTime: 'string' }
|
|
165
|
-
},
|
|
166
|
-
observation: {
|
|
167
|
-
eventType: OP_EVENT.observation,
|
|
168
|
-
identity: 'locationId',
|
|
169
|
-
/* 자리와 속성 — 둘 중 하나가 없으면 그 관측은 아무 데도 붙지 못한다. */
|
|
170
|
-
required: ['locationId', 'propertyId'],
|
|
171
|
-
fields: {
|
|
172
|
-
locationId: 'string', propertyId: 'string',
|
|
173
|
-
value: 'string', dataType: 'string', uom: 'string',
|
|
174
|
-
effectiveTime: 'string', effectiveEndTime: 'string', recordTime: 'string',
|
|
175
|
-
source: 'string', derived: 'boolean'
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
/**
|
|
180
|
-
* 이 레코드가 어느 운영 사실인가 — **라우팅 판정을 한 곳에 둔다**(소비처가 각자 짐작하지 않게).
|
|
181
|
-
*
|
|
182
|
-
* EPCIS·에너지와 겹치지 않게 본다: `epc`·`meterId` 가 있으면 그쪽 어휘이고, `equipmentId` 는 설비
|
|
183
|
-
* **에너지** 상태의 이름이다(운영 설비는 `moverId`). 품질은 설비와 정체 필드를 공유하므로 `good` 으로
|
|
184
|
-
* 가른다 — 둘 다 아니면 어느 쪽인지 모르는 것이고, 모르면 받지 않는다.
|
|
185
|
-
*
|
|
186
|
-
* ── 순서가 뜻을 갖는다: **주체와 참조는 다르다** ────────────────────────────
|
|
187
|
-
* 정체 필드는 하나만 오지 않는다. 설비·사람·자산 델타는 「지금 붙어 있는 작업」(`taskId`)을 함께 싣고,
|
|
188
|
-
* 작업 델타는 「소속 오더」(`orderId`)를 함께 싣는다. 그래서 아무 정체 필드나 먼저 보면 **참조를 주체로
|
|
189
|
-
* 읽는다** — 실제로 그랬다: `moverId` + `taskId` 인 설비 사실을 작업으로 읽어 「계약에 없는 필드」로
|
|
190
|
-
* 거부했다. 자원(설비·사람·자산)을 먼저 보고, 작업을 오더보다 먼저 본다.
|
|
191
|
-
*/
|
|
192
|
-
export function operationalKindOf(record) {
|
|
193
|
-
if (!record || typeof record !== 'object')
|
|
194
|
-
return undefined;
|
|
195
|
-
const r = record;
|
|
196
|
-
if (r.epc !== undefined || r.meterId !== undefined || r.equipmentId !== undefined)
|
|
197
|
-
return undefined;
|
|
198
|
-
const has = (k) => typeof r[k] === 'string' && r[k].trim().length > 0;
|
|
199
|
-
/* vocabulary-guard: allow 저널 와이어 필드로 가른다 */
|
|
200
|
-
if (has('moverId'))
|
|
201
|
-
return r.good !== undefined ? 'quality' : 'equipment';
|
|
202
|
-
if (has('personId'))
|
|
203
|
-
return 'person';
|
|
204
|
-
if (has('assetId'))
|
|
205
|
-
return 'asset';
|
|
206
|
-
if (has('taskId'))
|
|
207
|
-
return 'task'; // 작업이 든 `orderId` 는 소속(참조)이다
|
|
208
|
-
if (has('orderId'))
|
|
209
|
-
return 'order';
|
|
210
|
-
/*
|
|
211
|
-
* ── ★ **채널을 열고 들어오는 길을 내지 않았다** (2026-08-24) ─────────────────
|
|
212
|
-
* `OP_EVENT.test` 를 계약에 냈는데 이 라우팅이 `testableObjectId` 를 보지 않았다. 그래서 커넥터의
|
|
213
|
-
* 시험 결과가 **어느 통도 아니어서 조용히 버려졌다** — 거부 목록에도 남지 않았다(운영 경로를 아예
|
|
214
|
-
* 지나지 않으므로).
|
|
215
|
-
*
|
|
216
|
-
* 같은 부류가 하루에 세 번 났다: `ilmd`(매핑에 자리 없음) · 사건 시각(이름 어긋남) · 그리고 이것.
|
|
217
|
-
* **계약에 자리를 만드는 것과 그 자리로 가는 길을 내는 것은 다른 일이다.** 앞의 것만 하면 보내는
|
|
218
|
-
* 쪽에는 「실었다」로 보이고 화면에는 「없다」로 보인다.
|
|
219
|
-
*
|
|
220
|
-
* 순서상 뒤에 둔다 — 시험 결과가 작업·오더를 함께 가리킬 수 있고, 그때 그것은 **그 작업의 사실**이다.
|
|
221
|
-
*/
|
|
222
|
-
if (has('testableObjectId'))
|
|
223
|
-
return 'test';
|
|
224
|
-
/*
|
|
225
|
-
* ── ★ **채널을 열고 또 길을 내지 않았다** (2026-08-24) ──────────────────────
|
|
226
|
-
* `OP_EVENT.observation`(`location.measured`)을 내고 상태(`LocationState.observations`)와 조회
|
|
227
|
-
* (`observationAt`)까지 붙였는데 **이 라우팅이 `locationId` 를 보지 않았다.** 커넥터가 방의 온습도를
|
|
228
|
-
* 실어 보내면 「어느 운영 사실인지 모른다」로 거부됐다.
|
|
229
|
-
*
|
|
230
|
-
* 같은 부류를 하루에 일곱 번 만났다. 다만 이번엔 **거부되고 이유가 남았다** — 시험 결과 때는 어느
|
|
231
|
-
* 통도 아니어서 조용히 사라졌다. 그 차이가 이것을 5분 만에 찾게 했다(§`isOperationalRecord`).
|
|
232
|
-
*
|
|
233
|
-
* **둘을 함께 요구한다.** `locationId` 만으로는 자리를 말하는 다른 사실과 섞인다. 관측은 「어느
|
|
234
|
-
* 자리의 **무엇**을 쟀나」이므로 속성 없이는 담을 곳이 없다 — 그때는 받지 않는 것이 옳다.
|
|
235
|
-
*/
|
|
236
|
-
if (has('locationId') && has('propertyId'))
|
|
237
|
-
return 'observation';
|
|
238
|
-
/* 「이 목록이 전부다」 — 어느 목록인지를 스스로 말하므로 다른 사실과 섞이지 않는다. */
|
|
239
|
-
if (has('completeAxis'))
|
|
240
|
-
return 'complete';
|
|
241
|
-
return undefined;
|
|
242
|
-
}
|
|
243
|
-
/** 이 레코드가 운영 사실인가 — 호스트의 라우팅이 묻는 자리. */
|
|
244
|
-
export function isOperationalRecord(record) {
|
|
245
|
-
return operationalKindOf(record) !== undefined;
|
|
246
|
-
}
|
|
247
|
-
/**
|
|
248
|
-
* 운영 레코드들을 봉투로 — 유효한 것만 통과하고 나머지는 **이유와 함께** 남는다.
|
|
249
|
-
*
|
|
250
|
-
* 봉투는 다른 어휘와 같은 것을 쓴다(`CanonicalEnvelope`) — 그래서 저널·리플레이·시간여행·성과 폴드를
|
|
251
|
-
* 그대로 얻는다. 어휘만 자기 것이다.
|
|
252
|
-
*/
|
|
253
|
-
export function ingestOperationalRecords(records, opts) {
|
|
254
|
-
const arr = Array.isArray(records) ? records : records ? [records] : [];
|
|
255
|
-
const accepted = [];
|
|
256
|
-
const rejected = [];
|
|
257
|
-
let seq = 0;
|
|
258
|
-
for (const record of arr) {
|
|
259
|
-
const kind = operationalKindOf(record);
|
|
260
|
-
if (!kind) {
|
|
261
|
-
rejected.push({
|
|
262
|
-
record,
|
|
263
|
-
errors: ['어느 운영 사실인지 모른다 — 정체 필드가 필요하다(taskId · moverId(+good=품질) · personId · assetId · orderId)'] // vocabulary-guard: allow 거부 이유가 계약 필드 이름을 말한다
|
|
264
|
-
});
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
const spec = SPECS[kind];
|
|
268
|
-
const r = record;
|
|
269
|
-
const errors = [];
|
|
270
|
-
/*
|
|
271
|
-
* 모르는 이름은 거부한다 — 한 글자 틀린 필드가 오류 없이 사라지는 것을 막는다.
|
|
272
|
-
*
|
|
273
|
-
* 둘은 뺀다. **사실의 칸이 아니라 봉투의 칸**이다: `at` 은 그 사실이 일어난 시각이고,
|
|
274
|
-
* `description` 은 사람이 그때 적어 둔 말이다(§`CanonicalEnvelope.description`). 사실 칸으로
|
|
275
|
-
* 검사하면 명세마다 같은 두 줄을 적어야 하고, 한 곳을 빠뜨리면 그 통로만 거부한다.
|
|
276
|
-
*/
|
|
277
|
-
const ENVELOPE_FIELDS = ['at', 'description'];
|
|
278
|
-
const unknown = Object.keys(r).filter(k => !ENVELOPE_FIELDS.includes(k) && spec.fields[k] === undefined);
|
|
279
|
-
if (unknown.length)
|
|
280
|
-
errors.push(`${kind}: 계약에 없는 필드 — ${unknown.join(', ')}`);
|
|
281
|
-
for (const name of spec.required) {
|
|
282
|
-
const v = r[name];
|
|
283
|
-
if (v === undefined || v === null || v === '')
|
|
284
|
-
errors.push(`${kind}: ${name} 없음 — 지어낼 수 없는 값이다`);
|
|
285
|
-
}
|
|
286
|
-
const data = {};
|
|
287
|
-
for (const [name, type] of Object.entries(spec.fields)) {
|
|
288
|
-
const v = r[name];
|
|
289
|
-
if (v === undefined || v === null || v === '')
|
|
290
|
-
continue;
|
|
291
|
-
switch (type) {
|
|
292
|
-
case 'string': {
|
|
293
|
-
if (typeof v !== 'string') {
|
|
294
|
-
errors.push(`${kind}.${name} 이 문자열이 아니다: ${JSON.stringify(v)}`);
|
|
295
|
-
break;
|
|
296
|
-
}
|
|
297
|
-
const allowed = spec.enums?.[name];
|
|
298
|
-
if (allowed && !allowed.includes(v)) {
|
|
299
|
-
errors.push(`${kind}.${name} 이 커널이 다루는 낱말이 아니다: ${JSON.stringify(v)} — 받는 값은 ${allowed.join(' · ')}`);
|
|
300
|
-
break;
|
|
301
|
-
}
|
|
302
|
-
data[name] = v;
|
|
303
|
-
break;
|
|
304
|
-
}
|
|
305
|
-
case 'number': {
|
|
306
|
-
const n = Number(v);
|
|
307
|
-
if (typeof v === 'boolean' || !Number.isFinite(n)) {
|
|
308
|
-
errors.push(`${kind}.${name} 가 수가 아니다: ${JSON.stringify(v)}`);
|
|
309
|
-
break;
|
|
310
|
-
}
|
|
311
|
-
/* 진척은 비율이다 — 백분율(95)을 그대로 받으면 화면이 9,500% 를 말한다. */
|
|
312
|
-
if (name === 'progress' && (n < 0 || n > 1)) {
|
|
313
|
-
errors.push(`${kind}.progress 는 0~1 비율이다: ${n}`);
|
|
314
|
-
break;
|
|
315
|
-
}
|
|
316
|
-
if ((name === 'requested' || name === 'fulfilled' || name === 'goodCount' || name === 'scrapCount') && n < 0) {
|
|
317
|
-
errors.push(`${kind}.${name} 가 음수다: ${n}`);
|
|
318
|
-
break;
|
|
319
|
-
}
|
|
320
|
-
data[name] = n;
|
|
321
|
-
break;
|
|
322
|
-
}
|
|
323
|
-
case 'boolean': {
|
|
324
|
-
if (typeof v !== 'boolean') {
|
|
325
|
-
errors.push(`${kind}.${name} 가 참/거짓이 아니다: ${JSON.stringify(v)}`);
|
|
326
|
-
break;
|
|
327
|
-
}
|
|
328
|
-
data[name] = v;
|
|
329
|
-
break;
|
|
330
|
-
}
|
|
331
|
-
case 'string[]': {
|
|
332
|
-
if (!Array.isArray(v) || v.some(x => typeof x !== 'string')) {
|
|
333
|
-
errors.push(`${kind}.${name} 가 문자열 배열이 아니다: ${JSON.stringify(v)}`);
|
|
334
|
-
break;
|
|
335
|
-
}
|
|
336
|
-
data[name] = v.slice();
|
|
337
|
-
break;
|
|
338
|
-
}
|
|
339
|
-
case 'object': {
|
|
340
|
-
if (Array.isArray(v) || typeof v !== 'object') {
|
|
341
|
-
errors.push(`${kind}.${name} 가 객체가 아니다: ${JSON.stringify(v)}`);
|
|
342
|
-
break;
|
|
343
|
-
}
|
|
344
|
-
data[name] = { ...v };
|
|
345
|
-
break;
|
|
346
|
-
}
|
|
347
|
-
case 'object[]': {
|
|
348
|
-
if (!Array.isArray(v) || v.some(x => !x || typeof x !== 'object')) {
|
|
349
|
-
errors.push(`${kind}.${name} 가 객체 배열이 아니다: ${JSON.stringify(v)}`);
|
|
350
|
-
break;
|
|
351
|
-
}
|
|
352
|
-
data[name] = v.map(x => ({ ...x }));
|
|
353
|
-
break;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
const at = String(r.at ?? '').trim() || opts.defaultEventTime;
|
|
358
|
-
const atMs = at ? Date.parse(at) : Number.NaN;
|
|
359
|
-
if (!Number.isFinite(atMs)) {
|
|
360
|
-
/* 시각이 없으면 순서를 판정할 수 없다 — 늦게 온 옛 사실이 최신 상태를 덮어써 위치가 과거로 튄다. */
|
|
361
|
-
errors.push(`${kind}: at 없음/형식 오류 — 시각 없이는 늦게 온 옛 사실을 걸러낼 수 없다`);
|
|
362
|
-
}
|
|
363
|
-
if (errors.length) {
|
|
364
|
-
rejected.push({ record, errors });
|
|
365
|
-
continue;
|
|
366
|
-
}
|
|
367
|
-
/* 사람이 적어 둔 말 — 봉투에 싣는다(§`CanonicalEnvelope.description`). 물류 쪽과 같은 자리다. */
|
|
368
|
-
const note = typeof r.description === 'string' ? String(r.description).trim() : '';
|
|
369
|
-
accepted.push({
|
|
370
|
-
eventId: `${opts.tenantId}-op-${kind}-${++seq}`,
|
|
371
|
-
eventType: spec.eventType,
|
|
372
|
-
eventTime: new Date(atMs).toISOString(),
|
|
373
|
-
tenantId: opts.tenantId,
|
|
374
|
-
...(note ? { description: note } : {}),
|
|
375
|
-
data
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
return { accepted, rejected };
|
|
379
|
-
}
|
|
@@ -1,117 +0,0 @@
|
|
|
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;
|
|
@@ -1,120 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
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;
|