@things-factory/headless-twin 10.1.31 → 10.1.32
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-server/engine/kpi-fold.js +9 -0
- package/dist-server/engine/kpi-fold.js.map +1 -1
- package/dist-server/service/twin-forecast/forecast-metrics.js +26 -3
- package/dist-server/service/twin-forecast/forecast-metrics.js.map +1 -1
- package/package.json +2 -2
- package/server/engine/kpi-fold.ts +10 -1
- package/server/service/twin-forecast/forecast-metrics.ts +28 -3
- package/test/forecast-metrics.test.ts +43 -0
- package/test/kpi-fold.test.ts +28 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -18,14 +18,37 @@
|
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
19
|
exports.FLOW_METRICS = exports.ENERGY_METRICS = exports.FORECAST_METRICS = void 0;
|
|
20
20
|
exports.metricsForKind = metricsForKind;
|
|
21
|
+
const ops_contract_1 = require("@operato/ops-contract");
|
|
21
22
|
exports.FORECAST_METRICS = {
|
|
22
23
|
items: s => (s?.items || []).length,
|
|
23
24
|
occupancy: s => (s?.locations || []).reduce((a, n) => a + (n.occupancy || 0), 0),
|
|
24
25
|
tasks: s => (s?.tasks || []).length,
|
|
25
|
-
|
|
26
|
+
/*
|
|
27
|
+
* ── 세는 것은 **흐름 오더**다 (2026-09-19, ADR-0076 보탬 ⑥) ────────────────
|
|
28
|
+
*
|
|
29
|
+
* 받는 오더(입고 · 반품 입고)는 도착이 세우고 도착이 이행한다. 그대로 세면 「나간 것」이 받을 때마다
|
|
30
|
+
* 한 건씩 늘고 백로그는 영영 안 줄어든다 — 예측 그래프가 있지도 않은 일을 그린다. 무엇이 흐름
|
|
31
|
+
* 오더인지는 계약의 `isFlowOrder` 한 곳이 정한다(여기서 `kind` 를 다시 적지 않는다).
|
|
32
|
+
*/
|
|
33
|
+
orders: s => (s?.orders || []).filter(ops_contract_1.isFlowOrder).length,
|
|
26
34
|
// 이행 지표 — 라이브 예측의 핵심 질문("언제 다 나가나·백로그 언제 풀리나").
|
|
27
|
-
shipped: s => (s?.orders || []).filter((o) => o.status === 'shipped').length,
|
|
28
|
-
|
|
35
|
+
shipped: s => (s?.orders || []).filter(ops_contract_1.isFlowOrder).filter((o) => o.status === 'shipped').length,
|
|
36
|
+
/*
|
|
37
|
+
* ── 백로그는 **「종결인가」**로 묻는다 (2026-09-19) ─────────────────────────
|
|
38
|
+
*
|
|
39
|
+
* 낱말 둘(`shipped`·`cancelled`)로 물으면 **원본이 다른 낱말을 쓰는 미러에서 끝난 오더가 영영
|
|
40
|
+
* 백로그에 남는다.** `status` 는 열린 축이고(표준도 `RequestState` 를 열거하지 않는다) 실 시스템은
|
|
41
|
+
* `completed`·`FINISHED` 를 쓴다 — 그 트윈의 백로그 곡선은 내려가지 않는다. 종결 판정은 커널이
|
|
42
|
+
* 한 곳에서 한다(`isOrderTerminal` — 낱말이 아니라 **양**을 1차 근거로 본다).
|
|
43
|
+
*
|
|
44
|
+
* `shipped` 한 낱말은 남는다. 그것은 이 커널이 **자기가 쓰는 말**이고(WMS 의 출하 완료), 계약의
|
|
45
|
+
* 종결 목록(`completed` · `cancelled`)에는 없다. 양을 말하지 않는 스냅샷에서 그 오더가 백로그로
|
|
46
|
+
* 되돌아오지 않게 둘을 함께 본다 — 빼는 쪽이라 이 셈이 전보다 커지는 일은 없다. 커널의 낱말이
|
|
47
|
+
* 계약의 종결 목록에 들어오는 날 이 줄이 지워진다(아키텍트에게 올림).
|
|
48
|
+
*/
|
|
49
|
+
backlog: s => (s?.orders || [])
|
|
50
|
+
.filter(ops_contract_1.isFlowOrder)
|
|
51
|
+
.filter((o) => !(0, ops_contract_1.isOrderTerminal)(o) && o.status !== 'shipped').length,
|
|
29
52
|
/*
|
|
30
53
|
* ── 에너지 ────────────────────────────────────────────────────────────────
|
|
31
54
|
* `peakKW` 는 **마감된 구간의 최대**다 — 단조 증가라 지평선 위에서 「최대수요가 어디까지 오르나」로
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forecast-metrics.js","sourceRoot":"","sources":["../../../server/service/twin-forecast/forecast-metrics.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;
|
|
1
|
+
{"version":3,"file":"forecast-metrics.js","sourceRoot":"","sources":["../../../server/service/twin-forecast/forecast-metrics.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;AAmEH,wCAEC;AAnED,wDAAoE;AAMvD,QAAA,gBAAgB,GAA6B;IACxD,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM;IACnC,SAAS,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAS,EAAE,CAAM,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC;IAC7F,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM;IACnC;;;;;;OAMG;IACH,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,0BAAW,CAAC,CAAC,MAAM;IACzD,gDAAgD;IAChD,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,0BAAW,CAAC,CAAC,MAAM,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,MAAM;IACrG;;;;;;;;;;;;OAYG;IACH,OAAO,EAAE,CAAC,CAAC,EAAE,CACX,CAAC,CAAC,EAAE,MAAM,IAAI,EAAE,CAAC;SACd,MAAM,CAAC,0BAAW,CAAC;SACnB,MAAM,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,IAAA,8BAAe,EAAC,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,MAAM;IAE7E;;;;OAIG;IACH,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,CAAC,IAAI,CAAC;IAClD;;;OAGG;IACH,WAAW,EAAE,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC;IACtF;;;OAGG;IACH,WAAW,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,CAAC,CAAC,MAAM;CAChG,CAAA;AAED,gDAAgD;AACnC,QAAA,cAAc,GAAG,CAAC,aAAa,EAAE,QAAQ,EAAE,aAAa,CAAU,CAAA;AAClE,QAAA,YAAY,GAAG,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,CAAU,CAAA;AAEpG;;;;GAIG;AACH,SAAgB,cAAc,CAAC,IAAa;IAC1C,OAAO,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,sBAAc,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,oBAAY,CAAC,CAAA;AACjE,CAAC","sourcesContent":["/*\n * 예측 지표 — **스냅샷에서 수 하나를 뽑는 규칙.**\n *\n * ── 왜 순수 모듈인가 (2026-08-15) ───────────────────────────────────────────\n * 이 표가 리졸버 안에 있어서 시험이 불러올 수 없었다(type-graphql·shell 을 물고 있다). 그런데 여기서\n * 틀리면 예측 그래프 전체가 알리지 않고 다른 것을 그린다 — 오류 없이. 규칙은 시험이 닿는 자리에 둔다.\n *\n * ── 에너지 지표를 더한 이유 ─────────────────────────────────────────────────\n * 예측 화면의 지표가 물류 여섯(재고·점유·작업·오더·출고·백로그)으로 고정돼 있어서, 에너지 트윈을\n * 고르면 **남의 질문에 답하는 그래프**가 떴다. 에너지 트윈에는 재고도 오더도 없으므로 그 값들은\n * 전부 0 이고, 0 은 「없다」가 아니라 「이 트윈의 질문이 아니다」이다.\n *\n * 여기 더한 셋은 **커널이 이미 상태로 들고 있는 것**이다 — 새 계산을 만들지 않았다. 특히 부하 합은\n * 커널이 뿌리 계량기로만 세는데(계층 계량의 이중 계상 방지), 그 규칙을 여기서 다시 쓰면 두 벌이 된다.\n * 그래서 지점을 직접 더하지 않고 **구간 값**을 읽는다.\n */\n\nimport { isFlowOrder, isOrderTerminal } from '@operato/ops-contract'\n\n/** 스냅샷 → 수 하나. 값이 없으면 0 이 아니라 **그 지표를 그릴 수 없다**는 뜻이지만, 그래프 계약이\n * 수를 요구하므로 0 을 낸다 — 그래서 에너지 지표는 「없음」이 0 과 헷갈리지 않는 것만 골랐다. */\nexport type MetricFn = (s: any) => number\n\nexport const FORECAST_METRICS: Record<string, MetricFn> = {\n items: s => (s?.items || []).length,\n occupancy: s => (s?.locations || []).reduce((a: number, n: any) => a + (n.occupancy || 0), 0),\n tasks: s => (s?.tasks || []).length,\n /*\n * ── 세는 것은 **흐름 오더**다 (2026-09-19, ADR-0076 보탬 ⑥) ────────────────\n *\n * 받는 오더(입고 · 반품 입고)는 도착이 세우고 도착이 이행한다. 그대로 세면 「나간 것」이 받을 때마다\n * 한 건씩 늘고 백로그는 영영 안 줄어든다 — 예측 그래프가 있지도 않은 일을 그린다. 무엇이 흐름\n * 오더인지는 계약의 `isFlowOrder` 한 곳이 정한다(여기서 `kind` 를 다시 적지 않는다).\n */\n orders: s => (s?.orders || []).filter(isFlowOrder).length,\n // 이행 지표 — 라이브 예측의 핵심 질문(\"언제 다 나가나·백로그 언제 풀리나\").\n shipped: s => (s?.orders || []).filter(isFlowOrder).filter((o: any) => o.status === 'shipped').length,\n /*\n * ── 백로그는 **「종결인가」**로 묻는다 (2026-09-19) ─────────────────────────\n *\n * 낱말 둘(`shipped`·`cancelled`)로 물으면 **원본이 다른 낱말을 쓰는 미러에서 끝난 오더가 영영\n * 백로그에 남는다.** `status` 는 열린 축이고(표준도 `RequestState` 를 열거하지 않는다) 실 시스템은\n * `completed`·`FINISHED` 를 쓴다 — 그 트윈의 백로그 곡선은 내려가지 않는다. 종결 판정은 커널이\n * 한 곳에서 한다(`isOrderTerminal` — 낱말이 아니라 **양**을 1차 근거로 본다).\n *\n * `shipped` 한 낱말은 남는다. 그것은 이 커널이 **자기가 쓰는 말**이고(WMS 의 출하 완료), 계약의\n * 종결 목록(`completed` · `cancelled`)에는 없다. 양을 말하지 않는 스냅샷에서 그 오더가 백로그로\n * 되돌아오지 않게 둘을 함께 본다 — 빼는 쪽이라 이 셈이 전보다 커지는 일은 없다. 커널의 낱말이\n * 계약의 종결 목록에 들어오는 날 이 줄이 지워진다(아키텍트에게 올림).\n */\n backlog: s =>\n (s?.orders || [])\n .filter(isFlowOrder)\n .filter((o: any) => !isOrderTerminal(o) && o.status !== 'shipped').length,\n\n /*\n * ── 에너지 ────────────────────────────────────────────────────────────────\n * `peakKW` 는 **마감된 구간의 최대**다 — 단조 증가라 지평선 위에서 「최대수요가 어디까지 오르나」로\n * 읽힌다. 열린 구간은 넣지 않는다(아직 확정이 아니다 — 커널이 지키는 규율을 여기서도 지킨다).\n */\n peakKW: s => Number(s?.energy?.peakSince?.kW) || 0,\n /*\n * 이번 구간이 이대로 가면 얼마로 마감되나 — 커널이 낸 투영(`mean-so-far`)을 그대로 읽는다.\n * 우리가 다시 계산하지 않는다: 투영 규칙이 두 벌이 되면 화면과 판정이 다른 말을 한다.\n */\n projectedKW: s => Number(s?.energy?.open?.projectedKW ?? s?.energy?.open?.meanKW) || 0,\n /*\n * 계약을 넘긴 구간 수 — 「몇 번 넘나」는 요금이 걸린 질문이라 최대치와 따로 본다.\n * 계약을 모르는 트윈에서는 늘 0 이다(넘김을 판정할 근거가 없다 — 지어내지 않는다).\n */\n overWindows: s => (s?.energy?.closed || []).filter((w: any) => w?.overContract === true).length\n}\n\n/** 이 지표가 그 종류의 트윈에서 뜻이 있나 — 화면이 목록을 고를 때 쓴다. */\nexport const ENERGY_METRICS = ['projectedKW', 'peakKW', 'overWindows'] as const\nexport const FLOW_METRICS = ['items', 'occupancy', 'tasks', 'orders', 'shipped', 'backlog'] as const\n\n/**\n * 트윈 종류에 맞는 지표 목록 — **한 곳에서 정한다.**\n *\n * 화면이 각자 목록을 들고 있으면 종류가 늘 때 어느 화면이 뒤처졌는지 알 수 없다.\n */\nexport function metricsForKind(kind?: string): string[] {\n return kind === 'ems' ? [...ENERGY_METRICS] : [...FLOW_METRICS]\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/headless-twin",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.32",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"things-factory": true,
|
|
6
6
|
"author": "heartyoh <heartyoh@hatiolab.com>",
|
|
@@ -35,5 +35,5 @@
|
|
|
35
35
|
"@things-factory/ingest": "^10.1.31",
|
|
36
36
|
"@things-factory/shell": "^10.1.31"
|
|
37
37
|
},
|
|
38
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "67b211e001da3a923e562cb048e4691bd961315c"
|
|
39
39
|
}
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import { attributeEnergy, electricityCost, energyIntensity, energyOfWindows, foldTaskRecords, type ElectricityCost, type TariffDeclaration, type TaskFacets } from '@operato/twin-kernel'
|
|
29
29
|
import { type WorkCalendarEntry } from '@operato/ops-contract'
|
|
30
|
-
import { ENERGY_EVENT, isOrderTerminal, activeShiftAt } from '@operato/ops-contract'
|
|
30
|
+
import { ENERGY_EVENT, isFlowOrder, isOrderTerminal, activeShiftAt } from '@operato/ops-contract'
|
|
31
31
|
|
|
32
32
|
/** 저널 한 줄 — 필요한 것만(엔티티·typeorm 비의존). */
|
|
33
33
|
export interface KpiEvent {
|
|
@@ -1133,6 +1133,15 @@ export function foldKpi(events: KpiEvent[], window: KpiWindow, options: KpiFoldO
|
|
|
1133
1133
|
* 「됐나」는 **실적**에서 읽는다(`JobResponse`, 요구는 `SegmentRequirement.Quantity`). 그래서
|
|
1134
1134
|
* 판정의 1차 근거는 **양**이고 낱말은 양으로 말할 수 없는 종결(취소·중도 종료)에만 쓴다.
|
|
1135
1135
|
*/
|
|
1136
|
+
/*
|
|
1137
|
+
* ── 받는 오더는 처리량이 아니다 (2026-09-19, ADR-0076 보탬 ⑥) ────────────
|
|
1138
|
+
*
|
|
1139
|
+
* 입고 · 반품 입고는 세우는 순간 `fulfilled >= requested` 다 — 받았다는 사실이 곧 그 오더의
|
|
1140
|
+
* 완료이기 때문이다. 그래서 세면 **받을 때마다 처리량이 한 건씩 는다.** 판정은 계약의
|
|
1141
|
+
* `isFlowOrder` 한 곳이 한다(여기서 `kind` 를 다시 적지 않는다).
|
|
1142
|
+
*/
|
|
1143
|
+
if (!isFlowOrder(d)) continue
|
|
1144
|
+
|
|
1136
1145
|
const status = String(d.status ?? '').toLowerCase()
|
|
1137
1146
|
const done = isOrderTerminal({ status: d.status, requested: d.requested, fulfilled: d.fulfilled })
|
|
1138
1147
|
if (at >= window.fromMs && at <= window.toMs) {
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* 그래서 지점을 직접 더하지 않고 **구간 값**을 읽는다.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { isFlowOrder, isOrderTerminal } from '@operato/ops-contract'
|
|
19
|
+
|
|
18
20
|
/** 스냅샷 → 수 하나. 값이 없으면 0 이 아니라 **그 지표를 그릴 수 없다**는 뜻이지만, 그래프 계약이
|
|
19
21
|
* 수를 요구하므로 0 을 낸다 — 그래서 에너지 지표는 「없음」이 0 과 헷갈리지 않는 것만 골랐다. */
|
|
20
22
|
export type MetricFn = (s: any) => number
|
|
@@ -23,10 +25,33 @@ export const FORECAST_METRICS: Record<string, MetricFn> = {
|
|
|
23
25
|
items: s => (s?.items || []).length,
|
|
24
26
|
occupancy: s => (s?.locations || []).reduce((a: number, n: any) => a + (n.occupancy || 0), 0),
|
|
25
27
|
tasks: s => (s?.tasks || []).length,
|
|
26
|
-
|
|
28
|
+
/*
|
|
29
|
+
* ── 세는 것은 **흐름 오더**다 (2026-09-19, ADR-0076 보탬 ⑥) ────────────────
|
|
30
|
+
*
|
|
31
|
+
* 받는 오더(입고 · 반품 입고)는 도착이 세우고 도착이 이행한다. 그대로 세면 「나간 것」이 받을 때마다
|
|
32
|
+
* 한 건씩 늘고 백로그는 영영 안 줄어든다 — 예측 그래프가 있지도 않은 일을 그린다. 무엇이 흐름
|
|
33
|
+
* 오더인지는 계약의 `isFlowOrder` 한 곳이 정한다(여기서 `kind` 를 다시 적지 않는다).
|
|
34
|
+
*/
|
|
35
|
+
orders: s => (s?.orders || []).filter(isFlowOrder).length,
|
|
27
36
|
// 이행 지표 — 라이브 예측의 핵심 질문("언제 다 나가나·백로그 언제 풀리나").
|
|
28
|
-
shipped: s => (s?.orders || []).filter((o: any) => o.status === 'shipped').length,
|
|
29
|
-
|
|
37
|
+
shipped: s => (s?.orders || []).filter(isFlowOrder).filter((o: any) => o.status === 'shipped').length,
|
|
38
|
+
/*
|
|
39
|
+
* ── 백로그는 **「종결인가」**로 묻는다 (2026-09-19) ─────────────────────────
|
|
40
|
+
*
|
|
41
|
+
* 낱말 둘(`shipped`·`cancelled`)로 물으면 **원본이 다른 낱말을 쓰는 미러에서 끝난 오더가 영영
|
|
42
|
+
* 백로그에 남는다.** `status` 는 열린 축이고(표준도 `RequestState` 를 열거하지 않는다) 실 시스템은
|
|
43
|
+
* `completed`·`FINISHED` 를 쓴다 — 그 트윈의 백로그 곡선은 내려가지 않는다. 종결 판정은 커널이
|
|
44
|
+
* 한 곳에서 한다(`isOrderTerminal` — 낱말이 아니라 **양**을 1차 근거로 본다).
|
|
45
|
+
*
|
|
46
|
+
* `shipped` 한 낱말은 남는다. 그것은 이 커널이 **자기가 쓰는 말**이고(WMS 의 출하 완료), 계약의
|
|
47
|
+
* 종결 목록(`completed` · `cancelled`)에는 없다. 양을 말하지 않는 스냅샷에서 그 오더가 백로그로
|
|
48
|
+
* 되돌아오지 않게 둘을 함께 본다 — 빼는 쪽이라 이 셈이 전보다 커지는 일은 없다. 커널의 낱말이
|
|
49
|
+
* 계약의 종결 목록에 들어오는 날 이 줄이 지워진다(아키텍트에게 올림).
|
|
50
|
+
*/
|
|
51
|
+
backlog: s =>
|
|
52
|
+
(s?.orders || [])
|
|
53
|
+
.filter(isFlowOrder)
|
|
54
|
+
.filter((o: any) => !isOrderTerminal(o) && o.status !== 'shipped').length,
|
|
30
55
|
|
|
31
56
|
/*
|
|
32
57
|
* ── 에너지 ────────────────────────────────────────────────────────────────
|
|
@@ -25,6 +25,49 @@ test('물류 지표 — 기존 여섯은 그대로 센다', () => {
|
|
|
25
25
|
assert.equal(FORECAST_METRICS.backlog(s), 1)
|
|
26
26
|
})
|
|
27
27
|
|
|
28
|
+
/*
|
|
29
|
+
* ── 받는 오더는 흐름 지표에서 빠진다 (2026-09-19, ADR-0076 보탬 ⑥) ──────────
|
|
30
|
+
*
|
|
31
|
+
* 입고 · 반품 입고는 도착이 세우고 도착이 이행한다. 세면 「나간 것」이 받을 때마다 늘고 백로그는
|
|
32
|
+
* 영영 안 줄어든다 — 그래프가 있지도 않은 일을 그린다.
|
|
33
|
+
*/
|
|
34
|
+
test('★ 입고 · 반품 입고는 세지 않는다 — 그 오더는 나가지 않는다', () => {
|
|
35
|
+
const s = {
|
|
36
|
+
orders: [
|
|
37
|
+
{ kind: 'outbound', status: 'shipped' },
|
|
38
|
+
{ kind: 'outbound', status: 'picking' },
|
|
39
|
+
{ kind: 'inbound', status: 'received', requested: 10, fulfilled: 10 },
|
|
40
|
+
{ kind: 'return', status: 'received', requested: 3, fulfilled: 3 }
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
assert.equal(FORECAST_METRICS.orders(s), 2, '흐름 오더 둘만 센다')
|
|
44
|
+
assert.equal(FORECAST_METRICS.shipped(s), 1)
|
|
45
|
+
assert.equal(FORECAST_METRICS.backlog(s), 1, '받은 오더가 백로그에 남으면 곡선이 안 내려간다')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
/*
|
|
49
|
+
* ── 백로그는 「종결인가」로 묻는다 ──────────────────────────────────────────
|
|
50
|
+
*
|
|
51
|
+
* 낱말 둘로 물을 때 원본이 `completed` 를 쓰는 미러에서 끝난 오더가 영영 백로그에 남았다. `status` 는
|
|
52
|
+
* 열린 축이고 실 시스템은 자기 낱말을 쓴다 — 이 셈은 낱말이 아니라 양을 1차 근거로 본다.
|
|
53
|
+
*/
|
|
54
|
+
test('★ 원본의 낱말로 끝난 오더도 백로그에서 빠진다', () => {
|
|
55
|
+
const s = {
|
|
56
|
+
orders: [
|
|
57
|
+
{ kind: 'outbound', status: 'completed', requested: 5, fulfilled: 5 },
|
|
58
|
+
{ kind: 'outbound', status: 'FINISHED', requested: 2, fulfilled: 2 },
|
|
59
|
+
{ kind: 'outbound', status: 'picking', requested: 4, fulfilled: 1 }
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
assert.equal(FORECAST_METRICS.backlog(s), 1, '아직 안 끝난 하나만 백로그다')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('낱말을 말하지 않은 오더는 그대로 센다 — 지금까지의 거동이다', () => {
|
|
66
|
+
const s = { orders: [{ status: 'picking' }, { status: 'shipped' }] }
|
|
67
|
+
assert.equal(FORECAST_METRICS.orders(s), 2)
|
|
68
|
+
assert.equal(FORECAST_METRICS.shipped(s), 1)
|
|
69
|
+
})
|
|
70
|
+
|
|
28
71
|
test('에너지 지표 — 커널이 낸 값을 그대로 읽는다(다시 계산하지 않는다)', () => {
|
|
29
72
|
const s = {
|
|
30
73
|
energy: {
|
package/test/kpi-fold.test.ts
CHANGED
|
@@ -156,6 +156,34 @@ test('오더 완료: 양이 답한다 — 낱말이 진행 중이라 말해도',
|
|
|
156
156
|
assert.equal(r.throughput.orders, 1, '10 중 10 은 끝났고 10 중 9 는 아니다')
|
|
157
157
|
})
|
|
158
158
|
|
|
159
|
+
/*
|
|
160
|
+
* ── 받는 오더는 처리량이 아니다 (2026-09-19, ADR-0076 보탬 ⑥) ────────────────
|
|
161
|
+
*
|
|
162
|
+
* 입고 · 반품 입고는 세우는 순간 `fulfilled >= requested` 다 — 받았다는 사실이 곧 그 오더의 완료이기
|
|
163
|
+
* 때문이다. 그대로 세면 도착마다 처리량이 한 건씩 부풀고, 그 수는 화면에서 「많이 해냈다」로 읽힌다.
|
|
164
|
+
*/
|
|
165
|
+
test('★ 입고 · 반품 입고는 처리량에서 빠진다 — 받은 것은 해낸 것이 아니다', () => {
|
|
166
|
+
const kinded = (orderId: string, kind: string, sec: number): KpiEvent => ({
|
|
167
|
+
eventType: 'order.status',
|
|
168
|
+
eventTime: at(sec),
|
|
169
|
+
payload: { data: { orderId, kind, status: 'received', requested: 10, fulfilled: 10 } }
|
|
170
|
+
})
|
|
171
|
+
const out = (orderId: string, sec: number): KpiEvent => ({
|
|
172
|
+
eventType: 'order.status',
|
|
173
|
+
eventTime: at(sec),
|
|
174
|
+
payload: { data: { orderId, kind: 'outbound', status: 'completed', requested: 5, fulfilled: 5 } }
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
const r = foldKpi([kinded('i1', 'inbound', 10), kinded('r1', 'return', 20), out('o1', 30)], WINDOW)
|
|
178
|
+
|
|
179
|
+
assert.equal(r.throughput.orders, 1, '나간 오더 하나만 센다')
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test('낱말을 말하지 않은 오더는 그대로 센다 — 지금까지의 거동이다', () => {
|
|
183
|
+
const r = foldKpi([order('o1', 'completed', 10)], WINDOW)
|
|
184
|
+
assert.equal(r.throughput.orders, 1)
|
|
185
|
+
})
|
|
186
|
+
|
|
159
187
|
test('오더 완료: 같은 오더를 한 번만 센다 — 폴링 원본이 다시 보내도', () => {
|
|
160
188
|
/*
|
|
161
189
|
* 폴링하는 원본은 같은 오더의 종결을 여러 주기에 걸쳐 다시 보낼 수 있다(결함이 아니라 폴링의 성질).
|