@things-factory/headless-twin 10.0.12 → 10.0.13
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/canonical-ingest.d.ts +57 -2
- package/dist-server/engine/canonical-ingest.js +43 -2
- package/dist-server/engine/canonical-ingest.js.map +1 -1
- package/dist-server/engine/command-routing.js +1 -1
- package/dist-server/engine/command-routing.js.map +1 -1
- package/dist-server/engine/index.d.ts +3 -0
- package/dist-server/engine/index.js +4 -0
- package/dist-server/engine/index.js.map +1 -1
- package/dist-server/engine/ingest-health.d.ts +335 -0
- package/dist-server/engine/ingest-health.js +434 -0
- package/dist-server/engine/ingest-health.js.map +1 -0
- package/dist-server/engine/integration-coverage.d.ts +76 -0
- package/dist-server/engine/integration-coverage.js +73 -0
- package/dist-server/engine/integration-coverage.js.map +1 -0
- package/dist-server/engine/integration-probes.d.ts +65 -0
- package/dist-server/engine/integration-probes.js +100 -0
- package/dist-server/engine/integration-probes.js.map +1 -0
- package/dist-server/engine/integration-runner.d.ts +45 -0
- package/dist-server/engine/integration-runner.js +59 -0
- package/dist-server/engine/integration-runner.js.map +1 -0
- package/dist-server/engine/integration-target-profile.d.ts +57 -0
- package/dist-server/engine/integration-target-profile.js +79 -0
- package/dist-server/engine/integration-target-profile.js.map +1 -0
- package/dist-server/engine/kpi-fold.d.ts +27 -0
- package/dist-server/engine/kpi-fold.js +47 -4
- package/dist-server/engine/kpi-fold.js.map +1 -1
- package/dist-server/engine/kpi-query.js +128 -14
- package/dist-server/engine/kpi-query.js.map +1 -1
- package/dist-server/engine/live-feed-registry.js +2 -16
- package/dist-server/engine/live-feed-registry.js.map +1 -1
- package/dist-server/engine/load-meter.d.ts +1 -1
- package/dist-server/engine/load-meter.js +12 -3
- package/dist-server/engine/load-meter.js.map +1 -1
- package/dist-server/engine/log.d.ts +18 -0
- package/dist-server/engine/log.js +80 -0
- package/dist-server/engine/log.js.map +1 -0
- package/dist-server/engine/loop-lag.d.ts +54 -0
- package/dist-server/engine/loop-lag.js +87 -0
- package/dist-server/engine/loop-lag.js.map +1 -0
- package/dist-server/engine/model-gap.d.ts +108 -0
- package/dist-server/engine/model-gap.js +95 -0
- package/dist-server/engine/model-gap.js.map +1 -0
- package/dist-server/engine/restart-policy.d.ts +3 -3
- package/dist-server/engine/restart-policy.js +4 -4
- package/dist-server/engine/restart-policy.js.map +1 -1
- package/dist-server/engine/runtime-key.js +1 -1
- package/dist-server/engine/runtime-key.js.map +1 -1
- package/dist-server/engine/stage-path.d.ts +83 -0
- package/dist-server/engine/stage-path.js +118 -0
- package/dist-server/engine/stage-path.js.map +1 -0
- package/dist-server/engine/twin-engine.d.ts +352 -16
- package/dist-server/engine/twin-engine.js +1037 -166
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/index.js +34 -2
- package/dist-server/index.js.map +1 -1
- package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.d.ts +5 -0
- package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js +58 -0
- package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js.map +1 -0
- package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.d.ts +5 -0
- package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js +55 -0
- package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js.map +1 -0
- package/dist-server/migrations/index.js +7 -1
- package/dist-server/migrations/index.js.map +1 -1
- package/dist-server/service/index.d.ts +5 -2
- package/dist-server/service/index.js +18 -7
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/reference/discovery-result.d.ts +1 -1
- package/dist-server/service/reference/discovery-result.js +1 -1
- package/dist-server/service/reference/discovery-result.js.map +1 -1
- package/dist-server/service/reference/index.d.ts +4 -1
- package/dist-server/service/reference/index.js +6 -1
- package/dist-server/service/reference/index.js.map +1 -1
- package/dist-server/service/reference/reference-adapter.d.ts +184 -3
- package/dist-server/service/reference/reference-adapter.js +28 -4
- package/dist-server/service/reference/reference-adapter.js.map +1 -1
- package/dist-server/service/reference/reference-assessment.d.ts +68 -0
- package/dist-server/service/reference/reference-assessment.js +136 -0
- package/dist-server/service/reference/reference-assessment.js.map +1 -0
- package/dist-server/service/reference/reference-live.d.ts +12 -1
- package/dist-server/service/reference/reference-live.js +116 -11
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-master.d.ts +47 -1
- package/dist-server/service/reference/reference-master.js +34 -4
- package/dist-server/service/reference/reference-master.js.map +1 -1
- package/dist-server/service/reference/reference-probe.d.ts +92 -0
- package/dist-server/service/reference/reference-probe.js +186 -0
- package/dist-server/service/reference/reference-probe.js.map +1 -0
- package/dist-server/service/reference/reference-progress-subscription.d.ts +17 -0
- package/dist-server/service/reference/reference-progress-subscription.js +94 -0
- package/dist-server/service/reference/reference-progress-subscription.js.map +1 -0
- package/dist-server/service/reference/reference-progress.d.ts +38 -0
- package/dist-server/service/reference/reference-progress.js +71 -0
- package/dist-server/service/reference/reference-progress.js.map +1 -0
- package/dist-server/service/reference/reference-resolver.js +210 -43
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/reference/twin-reference.d.ts +32 -0
- package/dist-server/service/reference/twin-reference.js +10 -0
- package/dist-server/service/reference/twin-reference.js.map +1 -1
- package/dist-server/service/twin-control/twin-control-mutation.js +4 -3
- package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
- package/dist-server/service/twin-event/journal-count.d.ts +21 -0
- package/dist-server/service/twin-event/journal-count.js +26 -0
- package/dist-server/service/twin-event/journal-count.js.map +1 -0
- package/dist-server/service/twin-event/twin-event-keys.d.ts +9 -0
- package/dist-server/service/twin-event/twin-event-keys.js +15 -18
- package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
- package/dist-server/service/twin-event/twin-event-type.d.ts +3 -1
- package/dist-server/service/twin-event/twin-event-type.js +18 -1
- package/dist-server/service/twin-event/twin-event-type.js.map +1 -1
- package/dist-server/service/twin-event/twin-event.d.ts +1 -0
- package/dist-server/service/twin-event/twin-event.js +9 -1
- package/dist-server/service/twin-event/twin-event.js.map +1 -1
- package/dist-server/service/twin-forecast/twin-forecast-query.js +2 -1
- package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
- package/dist-server/service/twin-ingest-window/index.d.ts +5 -0
- package/dist-server/service/twin-ingest-window/index.js +10 -0
- package/dist-server/service/twin-ingest-window/index.js.map +1 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-query.d.ts +12 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js +123 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js.map +1 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.d.ts +1 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js +51 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js.map +1 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window.d.ts +16 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window.js +108 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window.js.map +1 -0
- package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
- package/dist-server/service/twin-journal/twin-journal-query.js +58 -5
- package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +14 -0
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +72 -2
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
- package/dist-server/service/twin-metrics/twin-metrics-query.d.ts +1 -0
- package/dist-server/service/twin-metrics/twin-metrics-query.js +78 -3
- package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
- package/dist-server/service/twin-model/axis-journal-evidence.d.ts +2 -0
- package/dist-server/service/twin-model/axis-journal-evidence.js +89 -0
- package/dist-server/service/twin-model/axis-journal-evidence.js.map +1 -0
- package/dist-server/service/twin-model/epcis-coverage.d.ts +1 -1
- package/dist-server/service/twin-model/epcis-coverage.js +19 -7
- package/dist-server/service/twin-model/epcis-coverage.js.map +1 -1
- package/dist-server/service/twin-model/iec61850-coverage.d.ts +1 -1
- package/dist-server/service/twin-model/iec61850-coverage.js +18 -7
- package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -1
- package/dist-server/service/twin-model/isa95-coverage.d.ts +14 -0
- package/dist-server/service/twin-model/isa95-coverage.js +23 -9
- package/dist-server/service/twin-model/isa95-coverage.js.map +1 -1
- package/dist-server/service/twin-model/item-ref.d.ts +24 -0
- package/dist-server/service/twin-model/item-ref.js +88 -0
- package/dist-server/service/twin-model/item-ref.js.map +1 -0
- package/dist-server/service/twin-model/name-index.d.ts +36 -0
- package/dist-server/service/twin-model/name-index.js +116 -0
- package/dist-server/service/twin-model/name-index.js.map +1 -0
- package/dist-server/service/twin-model/project-structure.js +1 -1
- package/dist-server/service/twin-model/project-structure.js.map +1 -1
- package/dist-server/service/twin-model/status-tally.d.ts +9 -0
- package/dist-server/service/twin-model/status-tally.js +37 -0
- package/dist-server/service/twin-model/status-tally.js.map +1 -0
- package/dist-server/service/twin-model/twin-lineage-query.js +40 -12
- package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-item-query.js +38 -39
- package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-query.js +156 -4
- package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-tree-query.js +7 -0
- package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
- package/dist-server/service/twin-readiness/index.d.ts +2 -0
- package/dist-server/service/twin-readiness/index.js +6 -0
- package/dist-server/service/twin-readiness/index.js.map +1 -0
- package/dist-server/service/twin-readiness/twin-readiness-query.d.ts +3 -0
- package/dist-server/service/twin-readiness/twin-readiness-query.js +103 -0
- package/dist-server/service/twin-readiness/twin-readiness-query.js.map +1 -0
- package/dist-server/service/twin-space/twin-space-resolver.d.ts +2 -2
- package/dist-server/service/twin-space/twin-space-resolver.js +5 -4
- package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
- package/dist-shared/entity-delta.d.ts +23 -3
- package/dist-shared/entity-delta.js +12 -8
- package/dist-shared/entity-delta.js.map +1 -1
- package/dist-shared/kpi-broadcast.js +1 -1
- package/dist-shared/kpi-broadcast.js.map +1 -1
- package/dist-shared/touched-items.d.ts +7 -0
- package/dist-shared/touched-items.js +80 -0
- package/dist-shared/touched-items.js.map +1 -0
- package/package.json +7 -7
- package/server/engine/canonical-ingest.ts +92 -7
- package/server/engine/command-routing.ts +1 -1
- package/server/engine/index.ts +4 -0
- package/server/engine/ingest-health.ts +704 -0
- package/server/engine/integration-coverage.ts +147 -0
- package/server/engine/integration-probes.ts +144 -0
- package/server/engine/integration-runner.ts +95 -0
- package/server/engine/integration-target-profile.ts +103 -0
- package/server/engine/kpi-fold.ts +72 -4
- package/server/engine/kpi-query.ts +129 -15
- package/server/engine/live-feed-registry.ts +2 -1
- package/server/engine/load-meter.ts +12 -3
- package/server/engine/log.ts +72 -0
- package/server/engine/loop-lag.ts +120 -0
- package/server/engine/model-gap.ts +168 -0
- package/server/engine/restart-policy.ts +4 -4
- package/server/engine/runtime-key.ts +1 -1
- package/server/engine/stage-path.ts +172 -0
- package/server/engine/twin-engine.ts +1135 -162
- package/server/index.ts +35 -2
- package/server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.ts +54 -0
- package/server/migrations/1786200000000-CarryLiveFeedCursor.ts +53 -0
- package/server/migrations/index.ts +7 -1
- package/server/service/index.ts +11 -0
- package/server/service/reference/discovery-result.ts +1 -1
- package/server/service/reference/index.ts +6 -1
- package/server/service/reference/reference-adapter.ts +197 -5
- package/server/service/reference/reference-assessment.ts +215 -0
- package/server/service/reference/reference-live.ts +123 -11
- package/server/service/reference/reference-master.ts +64 -4
- package/server/service/reference/reference-probe.ts +264 -0
- package/server/service/reference/reference-progress-subscription.ts +73 -0
- package/server/service/reference/reference-progress.ts +95 -0
- package/server/service/reference/reference-resolver.ts +200 -12
- package/server/service/reference/twin-reference.ts +39 -1
- package/server/service/twin-control/twin-control-mutation.ts +4 -3
- package/server/service/twin-event/journal-count.ts +53 -0
- package/server/service/twin-event/twin-event-keys.ts +16 -1
- package/server/service/twin-event/twin-event-type.ts +26 -2
- package/server/service/twin-event/twin-event.ts +7 -0
- package/server/service/twin-forecast/twin-forecast-query.ts +2 -1
- package/server/service/twin-ingest-window/index.ts +7 -0
- package/server/service/twin-ingest-window/twin-ingest-window-query.ts +125 -0
- package/server/service/twin-ingest-window/twin-ingest-window-writer.ts +57 -0
- package/server/service/twin-ingest-window/twin-ingest-window.ts +113 -0
- package/server/service/twin-instance/twin-instance.ts +1 -1
- package/server/service/twin-journal/twin-journal-query.ts +59 -5
- package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +77 -3
- package/server/service/twin-metrics/twin-metrics-query.ts +81 -3
- package/server/service/twin-model/axis-journal-evidence.ts +57 -0
- package/server/service/twin-model/epcis-coverage.ts +2 -8
- package/server/service/twin-model/iec61850-coverage.ts +2 -8
- package/server/service/twin-model/isa95-coverage.ts +22 -9
- package/server/service/twin-model/item-ref.ts +80 -0
- package/server/service/twin-model/name-index.ts +94 -0
- package/server/service/twin-model/project-structure.ts +1 -1
- package/server/service/twin-model/status-tally.ts +37 -0
- package/server/service/twin-model/twin-lineage-query.ts +38 -9
- package/server/service/twin-model/twin-model-item-query.ts +26 -27
- package/server/service/twin-model/twin-model-query.ts +155 -4
- package/server/service/twin-model/twin-model-tree-query.ts +7 -0
- package/server/service/twin-readiness/index.ts +3 -0
- package/server/service/twin-readiness/twin-readiness-query.ts +96 -0
- package/server/service/twin-space/twin-space-resolver.ts +5 -4
- package/shared/entity-delta.ts +31 -7
- package/shared/kpi-broadcast.ts +1 -1
- package/shared/touched-items.ts +73 -0
- package/test/axis-journal-evidence.test.ts +71 -0
- package/test/axis-read.test.ts +101 -1
- package/test/boot-resume.test.ts +92 -26
- package/test/broadcast-cost-baseline.test.ts +200 -0
- package/test/broadcast-period.test.ts +58 -0
- package/test/canonical-ingest-vocabularies.test.ts +36 -1
- package/test/canonical-quantity-door.test.ts +61 -0
- package/test/command-routing.test.ts +1 -1
- package/test/declared-location-types.test.ts +89 -0
- package/test/discovery-result.test.ts +1 -1
- package/test/duration-estimators.test.ts +1 -1
- package/test/entity-delta.test.ts +2 -2
- package/test/ingest-bench.test.ts +3 -3
- package/test/ingest-health-engine.test.ts +248 -0
- package/test/ingest-health-wiring.test.ts +119 -0
- package/test/ingest-health.test.ts +306 -0
- package/test/ingest-history.test.ts +247 -0
- package/test/integration-probes.test.ts +103 -0
- package/test/integration-runner.test.ts +95 -0
- package/test/item-ref.test.ts +78 -0
- package/test/journal-read-discipline.test.ts +177 -0
- package/test/journal-retention.test.ts +133 -0
- package/test/journal-sort-axis.test.ts +142 -0
- package/test/journal-write-door.test.ts +110 -0
- package/test/journal-write-trend.test.ts +115 -0
- package/test/kernel-kind-guard.test.ts +4 -4
- package/test/kpi-fold.test.ts +90 -3
- package/test/kpi-query-bench.test.ts +2 -2
- package/test/lineage-survives-restart.test.ts +17 -1
- package/test/live-cursor-wiring.test.ts +87 -0
- package/test/live-feed-registry.test.ts +9 -1
- package/test/live-kernel-facts.test.ts +7 -7
- package/test/live-mirror-parity.test.ts +6 -0
- package/test/load-meter.test.ts +29 -15
- package/test/local-declarations.test.ts +1 -1
- package/test/log-stamp.test.ts +59 -0
- package/test/loop-lag.test.ts +82 -0
- package/test/model-gap.test.ts +153 -0
- package/test/oee-accumulator.test.ts +4 -0
- package/test/operational-vocabulary.test.ts +4 -4
- package/test/read-failure-visible.test.ts +145 -0
- package/test/reference-grounding.test.ts +70 -0
- package/test/resolve-ts-siblings.mjs +52 -0
- package/test/restart-policy.test.ts +7 -7
- package/test/revision-axis.test.ts +93 -0
- package/test/runtime-key.test.ts +2 -2
- package/test/scale-twin-bench.test.ts +2 -2
- package/test/spec-coverage.test.ts +1 -1
- package/test/stage-path.test.ts +95 -0
- package/test/standard-coverage.test.ts +15 -3
- package/test/status-tally.test.ts +55 -0
- package/test/structure-revision-db.test.ts +4 -0
- package/test/tenant-registry-db.test.ts +1 -1
- package/test/time-range.test.ts +152 -0
- package/test/touched-items.test.ts +61 -0
- package/test/twin-event-keys.test.ts +10 -2
- package/test/twin-model-item-db.test.ts +2 -0
- package/test/warm-start-seam.test.ts +4 -0
- package/test/yield-loop.test.ts +57 -2
- package/tsconfig.shared.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -333,6 +333,10 @@ function foldKpi(events, window, options = {}) {
|
|
|
333
333
|
/* 작업 짝맞춤은 **커널 규칙**으로(중복 구현 금지 — 두 곳에 적으면 같은 저널로 다른 답이 나온다). */
|
|
334
334
|
const { records: tasks } = (0, twin_kernel_1.foldTaskRecords)(events.filter(e => e.eventType === 'task.status'));
|
|
335
335
|
let completedOrders = 0;
|
|
336
|
+
/* 완료로 읽지 못한 오더 상태 낱말 → 건수(§`orderStatusNotCounted`). */
|
|
337
|
+
const orderStatusNotCounted = new Map();
|
|
338
|
+
/* 이미 완료로 센 오더 — 폴링 원본이 같은 종결을 다시 보내도 한 번만 센다. */
|
|
339
|
+
const completedOrderIds = new Set();
|
|
336
340
|
let seen = 0;
|
|
337
341
|
let inWindow = 0;
|
|
338
342
|
for (const e of events) {
|
|
@@ -344,11 +348,48 @@ function foldKpi(events, window, options = {}) {
|
|
|
344
348
|
inWindow++;
|
|
345
349
|
const d = e.payload?.data ?? e.payload ?? {};
|
|
346
350
|
if (e.eventType === 'order.status') {
|
|
347
|
-
/*
|
|
348
|
-
*
|
|
351
|
+
/*
|
|
352
|
+
* **끝났나는 커널이 답한다** (2026-08-23) — `isOrderTerminal`.
|
|
353
|
+
*
|
|
354
|
+
* 예전에는 여기서 **동의어 목록**으로 추론했다(`completed`·`fulfilled`·`done`·`finished`).
|
|
355
|
+
* 그 방식이 낸 대가가 둘이었고 둘 다 조용했다.
|
|
356
|
+
*
|
|
357
|
+
* ① 원본의 낱말이 목록에 없으면 완료가 0 이 된다. 실제로 그랬다 — 첫 실 시스템의 `FINISHED`
|
|
358
|
+
* 가 빠져 있었고, 그때 목록에 낱말을 더한 것은 진짜 빈 자리(그 커넥터가 자기 상태 표를
|
|
359
|
+
* 안 쓰고 있었다)를 가린 땜빵이었다.
|
|
360
|
+
* ② **시뮬 트윈에서는 언제나 0 이었다.** 시뮬은 오더를 이행하며 `fulfilled` 만 올리고
|
|
361
|
+
* `status` 는 바꾸지 않는다 — 낱말로 묻는 판정은 시뮬의 완료를 한 건도 세지 못했다.
|
|
362
|
+
*
|
|
363
|
+
* 표준이 그 축을 갈라 두었다: `orders.status` ↔ `RequestState`(표준도 열거하지 않는다)이고,
|
|
364
|
+
* 「됐나」는 **실적**에서 읽는다(`JobResponse`, 요구는 `SegmentRequirement.Quantity`). 그래서
|
|
365
|
+
* 판정의 1차 근거는 **양**이고 낱말은 양으로 말할 수 없는 종결(취소·중도 종료)에만 쓴다.
|
|
366
|
+
*/
|
|
349
367
|
const status = String(d.status ?? '').toLowerCase();
|
|
350
|
-
|
|
351
|
-
|
|
368
|
+
const done = (0, twin_kernel_1.isOrderTerminal)({ status: d.status, requested: d.requested, fulfilled: d.fulfilled });
|
|
369
|
+
if (at >= window.fromMs && at <= window.toMs) {
|
|
370
|
+
/*
|
|
371
|
+
* **오더당 한 번만 센다.** 폴링하는 원본은 같은 오더의 종결을 여러 주기에 걸쳐 다시 보낼 수
|
|
372
|
+
* 있고(그것은 결함이 아니라 폴링의 성질이다), 사건마다 세면 처리량이 조용히 부풀려진다.
|
|
373
|
+
* 작업 쪽은 커널의 짝맞춤이 이미 그 규율을 갖고 있다(「완료는 작업당 하나」) — 오더만 없었다.
|
|
374
|
+
*/
|
|
375
|
+
if (done) {
|
|
376
|
+
const id = String(d.orderId ?? '');
|
|
377
|
+
if (id && !completedOrderIds.has(id)) {
|
|
378
|
+
completedOrderIds.add(id);
|
|
379
|
+
completedOrders++;
|
|
380
|
+
}
|
|
381
|
+
else if (!id)
|
|
382
|
+
completedOrders++; // 정체를 모르면 셈에서 빼지 않는다(조용히 줄이지 않는다)
|
|
383
|
+
}
|
|
384
|
+
/*
|
|
385
|
+
* **완료로 세지 않은 낱말의 인구조사.** 「못 읽었다」가 아니다 — `picking` 처럼 완료가 아닌
|
|
386
|
+
* 낱말이 여기 오는 것은 정상이다. 완료가 0 인데 여기 낱말이 쌓여 있으면 원인이 곧 보인다.
|
|
387
|
+
*
|
|
388
|
+
* 빈 낱말은 세지 않는다: 상태를 말하지 않은 사건은 다른 결함이고(유입 문이 상태를 필수로
|
|
389
|
+
* 받는다) 여기서 섞으면 이 값의 뜻이 흐려진다.
|
|
390
|
+
*/
|
|
391
|
+
else if (status)
|
|
392
|
+
orderStatusNotCounted.set(status, (orderStatusNotCounted.get(status) ?? 0) + 1);
|
|
352
393
|
}
|
|
353
394
|
}
|
|
354
395
|
}
|
|
@@ -462,6 +503,8 @@ function foldKpi(events, window, options = {}) {
|
|
|
462
503
|
waitTime: stats(waits),
|
|
463
504
|
utilization,
|
|
464
505
|
unpaired: { leadTime: unpairedLead, workTime: unpairedWork },
|
|
506
|
+
/* 못 읽은 오더 상태 낱말 — 비면 빈 객체다(§`orderStatusNotCounted`). */
|
|
507
|
+
orderStatusNotCounted: Object.fromEntries(orderStatusNotCounted),
|
|
465
508
|
eventsSeen: seen,
|
|
466
509
|
eventsInWindow: inWindow,
|
|
467
510
|
...(options.groupBy ? { groups: groupRecords(records, options) } : {})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kpi-fold.js","sourceRoot":"","sources":["../../server/engine/kpi-fold.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 업무 KPI 폴드 — **이벤트 저널을 접어** 시간창 지표를 만든다(순수).\n *\n * ── 왜 필요한가 ─────────────────────────────────────────────────────────────\n * 지금 트윈이 답할 수 있는 것은 \"이 순간의 값\"(점유 92%)과 \"인프라 부하\"(초당 이벤트 수)뿐이다.\n * 운영이 묻는 것은 다르다 — **지난 한 시간에 몇 건 처리했나, 한 건에 얼마나 걸리나, 자원은 얼마나\n * 일했나.** 그 값이 없으면 AI 는 \"점유 92%\" 는 말해도 \"목표보다 나쁜가\" 는 답할 수 없다.\n *\n * ── 원료는 이미 있다 ────────────────────────────────────────────────────────\n * 저널(`TwinEvent`)에 `task.status`·`order.status`·`equipment.status` 전이가 `eventTime`(ISO)과 함께\n * append-only 로 쌓인다. 그래서 새 계측을 심을 필요가 없다 — **접기만 하면 된다.**\n * 같은 모양의 선례가 이 폴더에 있다(`oee-accumulator.ts` — equipment 전이를 접어 OEE 카운터 생산).\n *\n * ── 이 파일이 하지 않는 것 ──────────────────────────────────────────────────\n * · 목표와 비교하지 않는다. \"95% 목표 대비\" 는 목표를 저장하는 별개 층(경영 KPI)의 몫이고,\n * 이 층은 **사실만** 생산한다. 두 층을 섞으면 목표를 정의하지 않은 현장은 사실조차 못 본다.\n * · 체류시간(dwell)은 아직 내지 않는다. 물건이 노드에 머문 시간은 item 단위 이벤트(epcis.*)를\n * 짝지어야 하고, 그 짝맞춤 규칙이 도메인마다 다르다. **없는 것을 0 으로 꾸미지 않는다.**\n * · DB·커널을 모른다. 입력은 평범한 배열이고 출력은 평범한 객체다(테스트가 값싸다).\n */\n\n/*\n * 짝맞춤은 **커널이 소유한다**(`foldTaskRecords`). 완료는 작업당 하나·나중 것이 사실, 착수는 처음 것,\n * 종류·오더는 어느 전이에서든 — 그 규칙은 커널 상태 기계에서 나오는 지식이다. 여기서 다시 적었더니\n * 공정 타임라인과 **다른 답**을 냈다(재전송된 완료를 두 건으로 세어 처리량·점유가 부풀려졌다).\n * 이 파일은 이제 \"접힌 기록 → 창 지표\" 만 한다.\n */\nimport {\n ENERGY_EVENT,\n attributeEnergy,\n electricityCost,\n energyIntensity,\n energyOfWindows,\n foldTaskRecords,\n activeShiftAt,\n type ElectricityCost,\n type TariffDeclaration,\n type TaskFacets,\n type WorkCalendarEntry\n} from '@operato/twin-kernel'\n\n/** 저널 한 줄 — 필요한 것만(엔티티·typeorm 비의존). */\nexport interface KpiEvent {\n eventType?: string\n /** ISO 시각. 파싱 불가하면 그 줄은 버린다(조용히 0 으로 세지 않는다). */\n eventTime?: string\n payload?: any\n}\n\nexport interface KpiWindow {\n /** 창의 시작·끝(epoch ms). 완료 시각이 이 안에 든 것만 성과로 센다. */\n fromMs: number\n toMs: number\n}\n\nexport interface DurationStats {\n /** 짝이 맞아 계산된 건수. */\n count: number\n /**\n * 하위 꼬리 — **분포를 양쪽에서 접지하기 위해** 낸다.\n * p50·p90 만 있으면 아래쪽 퍼짐을 위쪽에서 거울처럼 베껴 쓸 수밖에 없다(발명). 관측이 말하게 한다.\n */\n p10Ms: number\n p50Ms: number\n p90Ms: number\n avgMs: number\n}\n\n/**\n * 관점 축 — **같은 사실을 다른 각으로** 본다.\n *\n * 고정된 한 렌즈(자원별)만 낼 수 있으면 사용자가 \"구역별로는?\" 이라고 물어도 답이 없다. 사실 계산이\n * 축을 못 내면 AI 도 못 낸다 — 그래서 축은 계산 층의 능력이어야 한다.\n *\n * resource 자원별(누가 했나) taskKind 작업 종류별(무엇을 했나)\n * location 도착 지점별(어디서) order 오더별(무엇을 위해)\n * area 구역별 — 노드→구역 지도가 필요하다(이벤트에 없다. 호출부가 준다)\n * shift 교대별(언제) — 완료 시각 + 현장의 시각 기준에서 **계산한다**(이벤트에 없다)\n */\nexport type KpiGroupBy = 'resource' | 'taskKind' | 'location' | 'order' | 'area' | 'shift'\n\nexport interface KpiGroup {\n /** 축의 값(자원 id·작업 종류·노드 id·오더 id·구역 id). 값이 없던 기록은 'unknown'. */\n key: string\n /** 이 축에서 완료된 건수. */\n tasks: number\n leadTime: DurationStats\n workTime: DurationStats\n waitTime: DurationStats\n /** 이 축에 붙어 있던 시간의 합(자원 축에서 특히 의미 있다). */\n busyMs: number\n /**\n * **품질 판정이 있었던 건수만** 센다 (2026-08-19).\n *\n * 판정이 없는 작업(이동·체류·중간 스테이션)은 어느 쪽에도 세지 않는다 — 모름을 양품으로 세면 양품률이\n * 조용히 100% 로 올라간다. 그래서 `good + scrap` 은 `tasks` 보다 작을 수 있고, 그것이 정상이다.\n *\n * 판정이 하나도 없으면 **필드를 만들지 않는다**(0/0 을 「양품률 0%」로 읽히게 두지 않는다).\n */\n quality?: { good: number; scrap: number }\n}\n\nexport interface KpiResult {\n window: { fromMs: number; toMs: number }\n /** 창 안에 **완료된** 건수 — 성과는 완료 시점으로 센다(시작 시점으로 세면 진행 중인 것이 섞인다). */\n throughput: { tasks: number; orders: number }\n /**\n * 에너지 — **이 창의 전력량과 원단위.** 에너지 트윈이 없으면 `undefined`(0 이 아니다).\n *\n * ── 왜 KPI 폴드 안에 있나 (2026-08-14, §10 7단계) ─────────────────────────\n * 원단위는 분자(전력량)와 분모(산출)를 **같은 창**에서 나눈 값이다. 질의를 따로 두면 두 창을 맞추는\n * 일이 소비처의 몫이 되고, 어긋난 두 사실을 나눈 숫자는 아무것도 뜻하지 않는다(커널의 규칙이 그것을\n * 거절한다). 같은 폴드 안에 두면 창이 하나뿐이라 어긋날 수 없다.\n *\n * 분자는 **저널의 구간 마감 사건**에서 온다(`energy.demand.window`) — 커널 상태가 아니라 저널이므로\n * 멈춘 트윈·과거 구간에도 답할 수 있다.\n */\n energy?: {\n /** 이 창의 전력량. 셀 수 있는 구간이 없으면 `undefined`. */\n kWh?: number\n /**\n * 이 창의 **최대수요**(kW)와 그것이 난 구간의 시작.\n *\n * 전력량과 다른 질문이다: 전력량은 요금의 사용량 부분이고, 최대수요는 **기본요금**과 계약 위반을\n * 정한다. 평균으로 갈음할 수 없어서 커널이 구간의 최대들 중 가장 큰 값을 따로 낸다.\n */\n peakKW?: number\n peakStartMs?: number\n /**\n * 그 피크 구간에서 **설비가 끌어당긴** 최대(발전·방전 차감 전)와 그 차이(깎인 양).\n *\n * 요금은 계통 수요(`peakKW`)로 매겨지지만, 「무엇이 그 수요를 만들었나」와 「무엇이 덜어냈나」는\n * 이 두 수의 간격이 답한다. 발전·축전을 선언하지 않은 현장에서는 없다(차감할 것이 없다).\n */\n peakGrossKW?: number\n shavedKW?: number\n /** 파생의 근거 — 구간 평균부하 × 길이(`mean-kw`). */\n basis: string\n /** 센 구간 수 · 못 센 구간 수(계측이 끊긴 구간) — 조용히 자르지 않는다. */\n counted: number\n skipped: number\n /** 완료 오더당 전력량 — 답할 수 없으면 `null` 과 이유. */\n intensity: { value: number | null; unit?: string; reason?: string }\n /**\n * 배전 계통의 **자리별 몫** — 「이 분기가 얼마 썼나」.\n *\n * ── 이것은 측정이 아니라 배분이다 ──────────────────────────────────────\n * 저널에 있는 에너지 사실은 **현장 총량 한 줄**이다(뿌리 계량기 합). 계량기별 구간 사실이 없으므로\n * 자리별 몫은 **나눈 것**이고, 무엇으로 나눴는지(`weightKind`)를 값과 함께 낸다. 근거를 감추면\n * 나눈 숫자가 잰 숫자와 같은 무게로 읽힌다 — CBAM·ESG 보고에서 첫 질문에 깨지는 지점이다.\n *\n * 가중치는 **작업 가동시간**이다. 정격(kW)으로 나누지 않는다: 시뮬 트윈의 총부하가 정격의 합으로\n * 파생되므로, 그 총량을 다시 정격으로 나누면 넣은 값이 그대로 나오는 동어반복이 된다.\n *\n * 그래서 이 값에는 **가정이 하나 실려 있다**: 전기가 가동시간에 비례한다. 공조·조명처럼 공정에\n * 붙지 않는 부하도 그 비율에 섞인다 — 화면이 그 문장을 함께 말해야 한다.\n */\n byFeeder?: {\n /** 자리(분기·수전) id. 자리를 모르는 자원은 `unknown` 으로 모인다(조용히 버리지 않는다). */\n key: string\n kWh: number\n /**\n * 이 몫의 근거 — **커널이 정한다.**\n *\n * 선언된 부하가 여럿이면 배분(`apportioned`)이다. 선언된 부하가 **하나뿐인** 현장에서는 커널이\n * 「그 계량기는 그것만 잰다」고 읽어 `measured` 를 낸다 — 모델이 그렇게 말했으므로 그것도 사실이다.\n * 우리가 어느 쪽으로도 낙인찍지 않는다.\n */\n basis: 'apportioned' | 'measured'\n /**\n * 무엇으로 나눴나 — **배분일 때만** 있다.\n *\n * 분기마다 계량기가 있는 현장에서는 나누지 않는다(그 값이 곧 계량이다). 그때 이 칸을\n * `runtimeMs` 로 채우면 「가동시간으로 나눴다」는 없는 사실을 말하게 된다.\n */\n weightKind?: 'runtimeMs'\n /** 이 자리의 몫이 전체의 얼마였나 — 배분일 때만. */\n weightShare?: number\n /**\n * **피크가 찍힌 순간** 이 분기가 얼마였나(kW) — 요금이 걸린 수다.\n *\n * 창의 평균이나 합이 아니다: 최대수요를 낸 구간의 그 순간 값이다(다른 순간을 섞으면 「어느 분기를\n * 깎아야 피크가 내려가나」의 답이 흐려진다). 그 구간에 이 분기의 표본이 없었으면 **없다**.\n */\n peakKW?: number\n }[]\n /**\n * 나누지 못한 양과 그 이유 — **조용히 0 으로 만들지 않는다.**\n * `no-weights` = 그 창에 돌아간 자원이 없다(가동시간이 전부 0). 그러면 총량은 그대로 남는다.\n */\n unattributedKWh?: number\n /**\n * 왜 나누지 못했나(커널 코드) — `no-weights`(가동시간이 전부 0) · `no-consumers`(덮는 대상 미선언) ·\n * `not-submetered`(뿌리에서 재었으나 하위 계량이 덮지 않는 부분 — 결손이 아니라 계량 범위의 사실).\n */\n unattributedReason?: string\n /**\n * 분기 계량의 합이 뿌리 계량보다 **큰** 양 — 두 계량기의 회계가 다르다는 사실이다.\n *\n * 값을 깎아 맞추지 않는다: 그것은 계량을 우리가 고쳐 쓰는 일이고, 어느 쪽이 맞는지는 현장이 안다.\n */\n submeterExcessKWh?: number\n /**\n * 배분에서 **빠진 자원 수** — 그 창에 가동 기록이 없는 설비다. 0kWh 를 주지 않는 대신 몇 대가\n * 빠졌는지 센다(대기전력이 있는 설비에서 0 은 곧 거짓이다).\n */\n excludedResources?: number\n /**\n * 이 창의 전기요금 — **청구서가 아니다.** 계산과 그 한계(TOU·래칫·최소청구액 미모델)는 커널\n * (`electricityCost`)이 갖고 여기서는 나른다. 단가가 없으면 `reason: 'no-tariff'` 로 온다.\n */\n cost?: ElectricityCost\n }\n /** 리드타임 — 생성(created)에서 완료까지. 사용자가 체감하는 소요 시간. */\n leadTime: DurationStats\n /** 작업시간 — 착수(in-progress)에서 완료까지. 대기를 뺀 실작업. */\n workTime: DurationStats\n /** 대기시간 — 생성에서 착수까지. 자원이 부족하면 여기가 늘어난다. */\n waitTime: DurationStats\n /** 자원별 점유 — 작업에 붙어 있던 시간의 합과 창 대비 비율(작업으로 설명되는 부분만). */\n utilization: { resourceId: string; busyMs: number; ratio: number }[]\n /**\n * **셈에서 빠진 것** — 완료는 창 안이지만 시작 이벤트를 못 찾은 건수.\n * 조회 구간이 짧아 시작이 잘렸거나 저널이 유실된 경우다. 이 값을 숨기면 평균이 조용히 거짓이 된다.\n */\n unpaired: { leadTime: number; workTime: number }\n /**\n * 소비한 이벤트 수 — **넘겨받은 배열 전체**(창 앞 lookback 포함). 진단용이다.\n *\n * \"측정된 기록이 있는가\" 의 판단에는 쓰지 말 것 — 호출부는 창보다 앞까지 읽어 넘기므로(완료된 작업의\n * 시작이 창 밖일 수 있다) 창 안이 비어 있어도 이 값은 0 이 아니다. 그 판단은 `eventsInWindow` 로 한다.\n */\n eventsSeen: number\n /**\n * **창 안에** 들어온 이벤트 수 — \"이 구간에 측정할 기록이 있었는가\" 의 정답.\n *\n * 이것이 0 이면 \"처리량 0\" 이 아니라 **\"기록이 없다\"** 다. 두 상황을 섞으면 사용자가 잘못된 결론을\n * 낸다(앞 구간의 기록만 읽어 놓고 \"0건 처리\" 라고 말하는 일이 실제로 가능했다).\n */\n eventsInWindow: number\n /** 요청한 관점으로 쪼갠 값(요청했을 때만). */\n groups?: { by: KpiGroupBy; items: KpiGroup[]; truncated?: number; truncatedTasks?: number }\n}\n\n/**\n * 창의 전력량을 **배전 계통의 자리별로 나눈다** — 근거를 함께.\n *\n * ── 왜 커널 함수를 쓰나 ─────────────────────────────────────────────────────\n * 「나눌 근거가 없으면 나누지 않는다」·「합은 보존된다」·「몫이 0 이면 0kWh 를 주지 않는다」는 규칙이\n * 커널의 `attributeEnergy` 에 이미 있다. 여기서 다시 쓰면 두 벌이 되고, 그때부터 두 화면이 다른 합을\n * 말한다. 이 함수가 하는 일은 **재료를 모아 넘기고 답을 자리별로 묶는 것**뿐이다.\n *\n * ── 가중치는 가동시간이다 ───────────────────────────────────────────────────\n * 정격(kW)으로 나누지 않는다: 시뮬 트윈의 총부하가 정격의 합에서 파생되므로 그 총량을 다시 정격으로\n * 나누면 넣은 값이 그대로 나온다(새 정보 0의 동어반복). 가동시간은 **저널이 관측한 사실**이다.\n *\n * 자리를 모르는 자원은 `unknown` 으로 모인다 — 빼면 합이 줄고, 아무 자리에 넣으면 거짓이 된다.\n */\nfunction feederShares(\n kWh: number | undefined,\n utilization: { resourceId: string; busyMs: number }[],\n resourceFeeder?: Record<string, string>\n): Partial<NonNullable<KpiResult['energy']>> {\n /* 선언이 없으면 분해하지 않는다 — 자리를 모르는 채 나눈 숫자는 아무 것의 몫도 아니다. */\n if (!resourceFeeder || !Object.keys(resourceFeeder).length) return {}\n if (kWh === undefined || !(kWh > 0)) return {}\n\n /*\n * ── 풀이 **무엇을 덮는지 사실대로** 말한다 (2026-08-17) ────────────────────\n *\n * 처음에는 「그 창에 돌아간 자원」만 덮는다고 넘겼다. 그러자 돌아간 자원이 하나인 창에서 커널이\n * 그것을 **`measured`(전용 계량)** 로 판정했다 — 커널의 규칙은 옳다(「덮는 소비처가 하나면 그\n * 계량기는 그것만 잰다는 선언이다」). 틀린 것은 내 선언이었다: 현장 계량기는 돌아간 것만 재지 않는다.\n *\n * 그래서 **선언된 부하 전부**를 덮는다고 말한다(`resourceFeeder` 의 키가 곧 그 선언이다).\n * 그러면 그 창에 돌지 않은 설비는 몫 0 으로 **배분에서 빠지고 그 수가 보고된다** — 0kWh 를 주는\n * 대신 「이 설비는 이 창에 가동 기록이 없다」를 말하는 쪽이다.\n */\n /*\n * 덮는 대상은 **선언된 부하 ∪ 그 창에 돌아간 자원**이다.\n *\n * 선언만 쓰면, 돌았는데 전기 계통에 선언되지 않은 설비의 몫이 나머지 설비에 얹힌다 — 그 설비가\n * 안 쓴 전기를 쓴 것으로 만든다. 돌아간 것만 쓰면, 선언됐지만 쉰 설비가 사라져 「이 창에 안 돌았다」를\n * 말할 수 없다. 둘을 합해야 두 사실이 다 남는다(자리를 모르는 몫은 `unknown` 으로 모인다).\n */\n const busyOf = new Map(utilization.map(u => [u.resourceId, u.busyMs]))\n const ids = new Set<string>([...Object.keys(resourceFeeder), ...busyOf.keys()])\n const consumers = [...ids].map(id => ({ id, weight: busyOf.get(id) ?? 0 }))\n if (!consumers.length) return {}\n\n const r = attributeEnergy({\n pools: [{ meterId: 'site', kWh, consumerIds: consumers.map(c => c.id) }],\n consumers,\n weightKind: 'runtimeMs'\n })\n\n const byKey = new Map<string, { kWh: number; weightShare: number; apportioned: boolean }>()\n for (const sh of r.shares) {\n /* 자리를 모르면 `unknown` — 그 몫이 있다는 사실은 남기고, 어디의 것인지는 지어내지 않는다. */\n const key = resourceFeeder[sh.consumerId] ?? 'unknown'\n const cur = byKey.get(key) ?? { kWh: 0, weightShare: 0, apportioned: false }\n cur.kWh += sh.kWh\n cur.weightShare += sh.weightShare ?? 0\n /* **근거는 커널이 정한다.** 여기서 낙인찍으면 커널이 「쟀다」고 한 것을 「나눴다」고 바꿔 말하거나\n 그 반대가 된다. 섞이면 약한 쪽(배분)으로 적는다 — 강한 주장을 올려 잡지 않는다. */\n if (sh.basis === 'apportioned') cur.apportioned = true\n byKey.set(key, cur)\n }\n\n const byFeeder = [...byKey.entries()]\n .map(([key, v]) => ({\n key,\n kWh: v.kWh,\n basis: (v.apportioned ? 'apportioned' : 'measured') as 'apportioned' | 'measured',\n weightKind: 'runtimeMs' as const,\n weightShare: v.weightShare\n }))\n .sort((a, b) => b.kWh - a.kWh)\n\n const un = r.unattributed.reduce((sum, u) => sum + u.kWh, 0)\n return {\n ...(byFeeder.length ? { byFeeder } : {}),\n ...(un > 0 ? { unattributedKWh: un, unattributedReason: r.unattributed[0]?.reason } : {}),\n ...(r.excluded.length ? { excludedResources: r.excluded.length } : {})\n }\n}\n\n/**\n * 분기별 전력량을 **계량으로** 읽는다 — 계량이 있으면 배분하지 않는다.\n *\n * ── 왜 필요했나 (2026-08-18) ────────────────────────────────────────────────\n * 실 게이트웨이는 분기마다 계량기를 보낸다. 그런데 화면은 「나누지 못함」으로 답하고 있었다 — 여기에\n * 가동시간 배분 경로만 있었기 때문이다. 계량이 있는데 배분으로 말하면 **근거를 실제보다 약하게** 보고하는\n * 것이고, 그 위에 세운 SEU 판단이 무거운 값을 놓친다. 미러 트윈에는 설비 가동 기록조차 없어서\n * (원천이 계량만 준다) 그 경로로는 분기별 값이 영원히 나오지 않았다.\n *\n * 값의 출처는 커널이 구간 마감 사실에 실어 주는 **지점별 몫**이다(적산의 뺄셈). 표본을 직접 읽지 않는\n * 이유는 조회 상한이다 — 계측은 초당 여러 건이라 표본으로는 창을 채우기 전에 상한에 걸린다.\n *\n * 나머지(뿌리 − 분기 합)는 **버리지도 얹지도 않는다**: 수전 직결 부하·손실·계량되지 않은 회로가 그\n * 차이이므로, 어느 분기에 얹으면 그 분기가 쓰지 않은 전기를 쓴 것으로 만든다. 커널의 `remainder` 풀로\n * 넘겨 「하위 계량이 덮지 않는다」로 남긴다.\n *\n * 분기 계량 합이 뿌리보다 크면(회계가 다른 두 계량기) **그 사실을 낸다** — 값을 깎아 맞추면 계량을\n * 우리가 고쳐 쓰는 것이다.\n */\nfunction measuredFeeders(\n windows: readonly {\n startMs: number\n endMs: number\n maxKW?: number\n points: { id: string; kWh: number; root: boolean }[]\n pointsAtPeak?: { id: string; kW: number }[]\n }[],\n range: { startMs: number; endMs: number },\n totalKWh: number | undefined,\n meterFeeder?: Record<string, string>\n): Partial<NonNullable<KpiResult['energy']>> | undefined {\n if (!meterFeeder || !Object.keys(meterFeeder).length) return undefined\n if (totalKWh === undefined || !(totalKWh > 0)) return undefined\n\n /* 창에 온전히 든 구간만 — 전체 전력량과 같은 규칙이어야 몫과 합이 같은 창을 말한다. */\n const byFeederKWh = new Map<string, number>()\n let sawPoints = false\n for (const w of windows) {\n if (!(w.startMs >= range.startMs && w.endMs <= range.endMs)) continue\n for (const p of w.points ?? []) {\n const feeder = meterFeeder[p.id]\n if (!feeder) continue // 뿌리 계량기·모르는 지점 — 분기의 몫이 아니다\n sawPoints = true\n byFeederKWh.set(feeder, (byFeederKWh.get(feeder) ?? 0) + p.kWh)\n }\n }\n if (!sawPoints) return undefined // 계량이 없으면 이 경로를 쓰지 않는다(배분으로 돌아간다)\n\n const feeders = [...byFeederKWh.entries()].filter(([, kWh]) => kWh > 0)\n if (!feeders.length) return undefined\n const submetered = feeders.reduce((sum, [, kWh]) => sum + kWh, 0)\n const remainder = totalKWh - submetered\n\n const r = attributeEnergy({\n pools: [\n ...feeders.map(([feeder, kWh]) => ({ meterId: `${feeder}:meter`, kWh, consumerIds: [feeder] })),\n ...(remainder > 0 ? [{ meterId: 'root:remainder', kWh: remainder, consumerIds: [], remainder: true }] : [])\n ],\n consumers: feeders.map(([feeder]) => ({ id: feeder }))\n })\n\n /*\n * ── 피크를 만든 분기 (2026-08-18) ─────────────────────────────────────────\n *\n * 요금은 kW 로 매겨지므로 「어느 분기를 깎아야 피크가 내려가나」가 실무의 질문이다. 그 답은 **피크가\n * 찍힌 구간의 그 순간** 값이고, 창 안의 여러 구간을 더하거나 평균하면 뜻이 사라진다(다른 순간의 값을\n * 섞는 것이다). 그래서 **최대수요를 낸 구간 하나**만 골라 그 순간의 지점 값을 쓴다.\n */\n const peakWindow = windows\n .filter(w => w.startMs >= range.startMs && w.endMs <= range.endMs && Number.isFinite(Number(w.maxKW)))\n .reduce<(typeof windows)[number] | undefined>((best, w) => (!best || Number(w.maxKW) > Number(best.maxKW) ? w : best), undefined)\n const peakOf = new Map<string, number>()\n for (const p of peakWindow?.pointsAtPeak ?? []) {\n const feeder = meterFeeder[p.id]\n if (!feeder) continue // 뿌리 계량기·모르는 지점 — 분기의 몫이 아니다\n peakOf.set(feeder, (peakOf.get(feeder) ?? 0) + p.kW)\n }\n\n const byFeeder = r.shares\n .map(sh => ({\n key: sh.consumerId,\n kWh: sh.kWh,\n basis: sh.basis,\n weightKind: undefined,\n weightShare: undefined,\n /* 피크 순간의 값이 없으면 만들지 않는다 — 그 구간에 이 분기의 표본이 없었다는 뜻이다. */\n ...(peakOf.has(sh.consumerId) ? { peakKW: peakOf.get(sh.consumerId) } : {})\n }))\n .sort((a, b) => b.kWh - a.kWh) as NonNullable<KpiResult['energy']>['byFeeder']\n\n const un = r.unattributed.reduce((sum, u) => sum + u.kWh, 0)\n return {\n ...(byFeeder?.length ? { byFeeder } : {}),\n ...(un > 0 ? { unattributedKWh: un, unattributedReason: r.unattributed[0]?.reason } : {}),\n /* 분기 합이 뿌리를 넘었다 — 두 계량기의 회계가 다르다는 사실이다(깎아 맞추지 않는다). */\n ...(remainder < 0 ? { submeterExcessKWh: -remainder } : {})\n }\n}\n\n/** 폴드 옵션 — 관점 축과, 그 축을 만들기 위해 이벤트 밖에서 와야 하는 지도. */\nexport interface KpiFoldOptions {\n groupBy?: KpiGroupBy\n /**\n * 노드 → 구역 지도. **이벤트에는 구역이 없다**(노드까지만 있다) — 스냅샷·마스터가 아는 값이라\n * 호출부가 넘긴다. 없으면 구역 축은 'unknown' 으로 모인다(조용히 다른 축으로 바꾸지 않는다).\n */\n locationArea?: Record<string, string>\n /**\n * 자원 → **배전 계통의 자리**(분기·수전) 지도.\n *\n * ── 왜 밖에서 오나 ───────────────────────────────────────────────────────\n * 이벤트에는 전기 계통이 없다. 「이 설비가 어느 분기에서 먹는가」는 에너지 트윈의 모델이 아는\n * 선언이고(설비 하나에 사실 둘 — 공장에서는 자원, 전기에서는 부하), 그 선언을 읽는 것은 호출부의 일이다.\n *\n * 없으면 분기 분해를 **하지 않는다** — 자리를 모르는 채 나누면 그 숫자는 아무 것의 몫도 아니다.\n */\n resourceFeeder?: Record<string, string>\n /**\n * **계량 지점 → 그것이 재는 분기** 지도.\n *\n * 있으면 분기별 전력량을 계량으로 읽는다(배분보다 강한 근거). 이 지도는 모델의 선언에서 오고\n * (계량기의 자리), 그것을 읽는 것은 호출부의 일이다 — 폴드는 저널만 본다.\n */\n meterFeeder?: Record<string, string>\n /**\n * 선언된 **전기요금 단가** — 수를 금액으로 바꾸는 유일한 입력.\n *\n * 이벤트에는 요금이 없다. 단가는 현장의 계약이고(에너지 트윈 모델의 수전 자리에 선언한다), 그것을\n * 읽는 것은 호출부의 일이다. 없으면 금액을 만들지 않는다 — 짐작한 단가로 낸 금액은 숫자가 있다는\n * 것만으로 사람을 결정으로 밀어붙인다.\n */\n tariff?: TariffDeclaration\n /**\n * 교대 선언 — **이벤트에는 교대가 없다**(시각만 있다). 현장 마스터가 아는 값이라 호출부가 넘긴다.\n * 없으면 교대 축은 'unknown' 으로 모인다(다른 축으로 조용히 바꾸지 않는다).\n */\n shifts?: WorkCalendarEntry[]\n /**\n * 현장의 시각 기준(분) — 교대 축이 `HH:MM` 을 어느 기준으로 읽을지. **공간이 갖는 값**이다\n * (테넌트가 아니다: 한 테넌트가 다른 시간대의 공장을 함께 가질 수 있다). 없으면 UTC.\n */\n utcOffsetMinutes?: number\n /**\n * 축의 상한(기본 20). 넘치면 잘라내고 **잘린 축의 수와 그 안의 건수를 함께 알린다**.\n *\n * 축의 수만 알리면 부족하다 — \"그 외 89개\" 로는 그것이 3건인지 300건인지 모른다. 보여준 축이 전부인\n * 것처럼 읽히면 사용자가 꼬리를 놓친다.\n */\n groupLimit?: number\n}\n\n/* ── 관점 축의 재료 ─────────────────────────────────────────────────────────\n * 축의 값은 이벤트 payload 에 이미 들어 있다(`TaskStatusDelta`: kind·orderId·fromNode·toNode·\n * resourceRef). 새 계측을 심을 필요가 없다 — **이미 쌓인 저널을 다르게 묶기만** 한다.\n */\n/** 창 안에 완료된 한 건의 기록 — 통계의 원료이자 관점 축의 원료. */\ninterface CompletionRecord {\n taskId: string\n at: number\n facets: TaskFacets\n leadMs?: number\n workMs?: number\n waitMs?: number\n}\n\n/** 이 기록이 요청한 축에서 어느 값에 속하는가. 값이 없으면 'unknown'(조용히 버리지 않는다). */\nfunction facetKey(rec: CompletionRecord, options: KpiFoldOptions): string {\n const f = rec.facets\n switch (options.groupBy) {\n case 'resource':\n return f.resource ?? 'unknown'\n case 'taskKind':\n return f.kind ?? 'unknown'\n case 'location':\n return f.location ?? 'unknown'\n case 'order':\n return f.order ?? 'unknown'\n case 'area':\n /* 구역은 이벤트 밖의 지식이다 — 지도가 없으면 없다고 말한다(노드 id 로 대체하면 사용자가\n * 그것을 구역으로 오해한다). */\n return (f.location && options.locationArea?.[f.location]) ?? 'unknown'\n case 'shift':\n /*\n * 교대는 **저널에 찍혀 있지 않다 — 계산한다.**\n *\n * 이벤트에 교대 이름을 스탬프하면 두 가지가 무너진다: ① 같은 사실이 두 곳에 생기고,\n * ② 나중에 교대 시간을 고치면 **과거가 옛 구분으로 굳어** 다시 셀 수 없다. 교대는\n * `완료 시각 + 현장의 시각 기준 + 교대 선언`에서 나오는 순수 파생이므로 여기서 낸다 —\n * 라이브·히스토리·예측이 **커널과 같은 함수**(`activeShiftOf`)를 쓴다.\n *\n * 선언이 없으면 'unknown' 이다. 교대를 나눠 놓지 않은 현장에 이름을 지어내지 않는다.\n */\n if (!options.shifts?.length) return 'unknown'\n /* 시각까지 넘긴다 — **휴일에 일어난 일은 어느 교대에도 속하지 않는다.** 되풀이만 보면 그날 서지도\n 않은 교대에 집계돼, 교대별 성과가 조용히 거짓이 된다. */\n return activeShiftAt(options.shifts, rec.at, options.utcOffsetMinutes) ?? 'unknown'\n default:\n return 'unknown'\n }\n}\n\n/**\n * 이 축의 품질 — **판정이 있었던 것만** 센다(없으면 필드를 만들지 않는다).\n *\n * 양품률의 이력이 여기서 나온다: 커널이 작업 완료에 결과를 적고(`TaskStatusDelta.outcome`), 폴드가 축으로\n * 들고(`TaskFacets.outcome`), 여기서 종류별로 센다. 그 사슬이 없으면 「이 공정의 양품률」은 저널에\n * 남지 않는다(설비 누적 카운터는 종류를 모른다).\n */\nfunction qualityOf(rows: CompletionRecord[]): { quality?: { good: number; scrap: number } } {\n let good = 0\n let scrap = 0\n for (const r of rows) {\n const o = r.facets.outcome\n if (o === 'good') good++\n else if (o === 'scrap') scrap++\n }\n return good + scrap > 0 ? { quality: { good, scrap } } : {}\n}\n\n/**\n * 기록을 축으로 묶어 축별 통계를 낸다 — **전체와 같은 규칙**으로(같은 stats 함수, 같은 창).\n *\n * 축의 값이 아주 많을 수 있다(오더 축은 수천 개). 그래서 건수 큰 것부터 상한까지만 돌려주고\n * **잘라낸 수를 함께 알린다** — 조용한 절단은 \"이게 전부\" 라는 거짓을 만든다.\n */\nfunction groupRecords(\n records: CompletionRecord[],\n options: KpiFoldOptions\n): { by: KpiGroupBy; items: KpiGroup[]; truncated?: number; truncatedTasks?: number } {\n const by = options.groupBy as KpiGroupBy\n const bins = new Map<string, CompletionRecord[]>()\n for (const rec of records) {\n const key = facetKey(rec, options)\n const bin = bins.get(key)\n if (bin) bin.push(rec)\n else bins.set(key, [rec])\n }\n\n const all = [...bins.entries()]\n .map(([key, rows]) => ({\n key,\n tasks: rows.length,\n leadTime: stats(rows.map(r => r.leadMs).filter((v): v is number => v !== undefined)),\n workTime: stats(rows.map(r => r.workMs).filter((v): v is number => v !== undefined)),\n waitTime: stats(rows.map(r => r.waitMs).filter((v): v is number => v !== undefined)),\n busyMs: rows.reduce((a, r) => a + (r.workMs ?? 0), 0),\n ...qualityOf(rows)\n }))\n .sort((a, b) => b.tasks - a.tasks || (a.key < b.key ? -1 : 1))\n\n const limit = Math.max(1, Math.min(100, options.groupLimit ?? 20))\n const items = all.slice(0, limit)\n const dropped = all.slice(limit)\n /* 축별로 \"창 대비 비율\" 은 내지 않는다 — 자원 아닌 축(오더·작업종류)에서 그 비율은 의미가 없고,\n * 의미 없는 숫자를 내면 사용자가 그것으로 판단한다. 자원 점유는 `utilization` 이 이미 낸다. */\n return {\n by,\n items,\n ...(dropped.length\n ? { truncated: dropped.length, truncatedTasks: dropped.reduce((a, g) => a + g.tasks, 0) }\n : {})\n }\n}\n\nfunction ms(value: string | undefined): number | null {\n if (!value) return null\n const t = Date.parse(value)\n return Number.isFinite(t) ? t : null\n}\n\n/** 분위수 — 정렬된 값에서 선형 보간 없이 가장 가까운 아래 값(작은 표본에서 과장하지 않는다). */\nfunction quantile(sorted: number[], q: number): number {\n if (sorted.length === 0) return 0\n const idx = Math.min(sorted.length - 1, Math.max(0, Math.floor(q * sorted.length)))\n return sorted[idx]\n}\n\nfunction stats(values: number[]): DurationStats {\n if (values.length === 0) return { count: 0, p10Ms: 0, p50Ms: 0, p90Ms: 0, avgMs: 0 }\n const sorted = [...values].sort((a, b) => a - b)\n const sum = sorted.reduce((a, b) => a + b, 0)\n return {\n count: sorted.length,\n p10Ms: quantile(sorted, 0.1),\n p50Ms: quantile(sorted, 0.5),\n p90Ms: quantile(sorted, 0.9),\n avgMs: Math.round(sum / sorted.length)\n }\n}\n\n/**\n * 이벤트를 접어 시간창 KPI 를 만든다.\n *\n * **호출부 규약**: `events` 는 창보다 **넉넉히 앞까지** 담아 넘긴다(예: 창 시작 − 리드타임 상한).\n * 완료된 작업의 시작 이벤트가 창 밖에 있을 수 있기 때문이다. 못 찾은 것은 `unpaired` 로 알린다 —\n * 평균에서 조용히 빠지면 지표가 거짓이 된다.\n *\n * 시간축은 `eventTime`(ISO) 차분이다. sim 클록이 실제 시각과 오프셋이 있어도 **차분은 정확**하다\n * (고정 오프셋은 상쇄된다). 파싱 불가한 줄은 버린다.\n */\nexport function foldKpi(events: KpiEvent[], window: KpiWindow, options: KpiFoldOptions = {}): KpiResult {\n /* 작업 짝맞춤은 **커널 규칙**으로(중복 구현 금지 — 두 곳에 적으면 같은 저널로 다른 답이 나온다). */\n const { records: tasks } = foldTaskRecords(events.filter(e => e.eventType === 'task.status'))\n let completedOrders = 0\n let seen = 0\n let inWindow = 0\n\n for (const e of events) {\n const at = ms(e.eventTime)\n if (at === null) continue\n seen++\n if (at >= window.fromMs && at <= window.toMs) inWindow++\n const d = e.payload?.data ?? e.payload ?? {}\n\n if (e.eventType === 'order.status') {\n /* 오더 완료 어휘는 도메인 소유다 — 코어가 강제하지 않는다. 그래서 이름을 하나로 못 박지 않고\n * 완료로 읽히는 표현을 넓게 받는다(그 밖은 세지 않는다). */\n const status = String(d.status ?? '').toLowerCase()\n if (at >= window.fromMs && at <= window.toMs && (status === 'completed' || status === 'fulfilled' || status === 'done')) {\n completedOrders++\n }\n }\n }\n\n /*\n * 에너지 — 창에 **온전히 든** 구간 마감 사건만 센다(걸친 구간은 커널 규칙이 뺀다).\n * 사건이 하나도 없으면 이 트윈에는 에너지가 없는 것이므로 블록 자체를 내지 않는다.\n */\n const demandWindows = events\n .filter(e => e.eventType === ENERGY_EVENT.demandWindow)\n .map(e => {\n const d = e.payload?.data ?? e.payload ?? {}\n /* 최대(`maxKW`)도 함께 넘긴다 — 요금이 걸리는 수는 평균이 아니라 최대다.\n 총부하(`grossMaxKW`)는 「무엇이 깎였나」를 되짚는 축이다(발전·방전 차감 전). */\n return {\n startMs: Number(d.startMs),\n endMs: Number(d.endMs),\n meanKW: Number(d.meanKW),\n maxKW: Number(d.maxKW),\n grossMaxKW: Number(d.grossMaxKW),\n /* 지점별 몫 — 계량이 분기에 있으면 그 값은 **측정**이다(배분이 아니다). 커널이 계층 판정\n (`root`)까지 실어 주므로 여기서 같은 규칙을 다시 쓰지 않는다. */\n points: (Array.isArray(d.points) ? d.points : [])\n .map((p: any) => ({ id: String(p?.id ?? ''), kWh: Number(p?.kWh), root: p?.root === true }))\n .filter((p: any) => p.id && Number.isFinite(p.kWh)),\n /* 피크가 찍힌 순간의 지점별 kW — 요금이 걸린 수는 kW 다(전력량이 큰 분기와 피크를 만든 분기는 다르다). */\n pointsAtPeak: (Array.isArray(d.pointsAtPeak) ? d.pointsAtPeak : [])\n .map((p: any) => ({ id: String(p?.id ?? ''), kW: Number(p?.kW) }))\n .filter((p: any) => p.id && Number.isFinite(p.kW))\n }\n })\n .filter(w => Number.isFinite(w.startMs) && Number.isFinite(w.endMs))\n\n const leads: number[] = []\n const works: number[] = []\n const waits: number[] = []\n const busyByResource = new Map<string, number>()\n /* 창 안에 완료된 건들의 **기록** — 통계를 낸 뒤에도 버리지 않는다. 관점 축은 같은 기록을 다시\n * 묶는 것일 뿐이므로, 기록을 남기면 축을 하나 더 붙이는 값이 거의 0 이 된다. */\n const records: CompletionRecord[] = []\n let tasksInWindow = 0\n let unpairedLead = 0\n let unpairedWork = 0\n\n for (const t of tasks) {\n const completed = t.completedMs\n if (completed === undefined) continue // 아직 완료되지 않은 작업 — 성과로 세지 않는다\n if (completed < window.fromMs || completed > window.toMs) continue // 성과는 완료 시점으로 센다\n tasksInWindow++\n const c = t.createdMs\n const s = t.startedMs\n const rec: CompletionRecord = { taskId: t.taskId, at: completed, facets: t.facets }\n const resource = rec.facets.resource\n if (c !== undefined && completed >= c) leads.push((rec.leadMs = completed - c))\n else unpairedLead++\n if (s !== undefined && completed >= s) {\n const busy = completed - s\n works.push((rec.workMs = busy))\n if (resource) busyByResource.set(resource, (busyByResource.get(resource) ?? 0) + busy)\n } else {\n unpairedWork++\n }\n if (c !== undefined && s !== undefined && s >= c) waits.push((rec.waitMs = s - c))\n records.push(rec)\n }\n\n const span = Math.max(1, window.toMs - window.fromMs)\n const utilization = [...busyByResource.entries()]\n .map(([resourceId, busyMs]) => ({ resourceId, busyMs, ratio: Math.min(1, busyMs / span) }))\n .sort((a, b) => b.busyMs - a.busyMs)\n\n return {\n window: { fromMs: window.fromMs, toMs: window.toMs },\n throughput: { tasks: tasksInWindow, orders: completedOrders },\n ...(demandWindows.length\n ? (() => {\n const e = energyOfWindows(demandWindows, { startMs: window.fromMs, endMs: window.toMs })\n /* 분모는 **완료 오더**다(대당 전력). 창에 완료가 없으면 커널 규칙이 「정의되지 않음」으로 답한다. */\n const i = energyIntensity({\n kWh: e.kWh,\n energyWindow: e.window,\n denominator: { kind: 'output', value: completedOrders, window: e.window }\n })\n return {\n energy: {\n ...(e.kWh !== undefined ? { kWh: e.kWh } : {}),\n /* 이 창의 최대수요 — 계약 대비 판정이 보는 수다(전력량과 같은 구간 집합에서 왔다). */\n ...(e.peakKW !== undefined ? { peakKW: e.peakKW, peakStartMs: e.peakStartMs } : {}),\n /* 깎인 양은 **같은 구간의 두 수**의 차이다 — 여기서 한 번만 계산해 두 곳이 갈라지지 않게. */\n ...(e.peakGrossKW !== undefined && e.peakKW !== undefined\n ? { peakGrossKW: e.peakGrossKW, ...(e.peakGrossKW > e.peakKW ? { shavedKW: e.peakGrossKW - e.peakKW } : {}) }\n : {}),\n basis: e.basis,\n counted: e.counted,\n skipped: e.skipped,\n intensity:\n i.value === null\n ? { value: null, reason: (i as any).reason }\n : { value: i.value, unit: (i as any).unit },\n ...(measuredFeeders(demandWindows, { startMs: window.fromMs, endMs: window.toMs }, e.kWh, options.meterFeeder) ??\n feederShares(e.kWh, utilization, options.resourceFeeder)),\n /* 금액은 같은 창의 전력량·최대수요에서 나온다 — 창이 하나뿐이라 어긋날 수 없다. */\n ...(options.tariff ? { cost: electricityCost({ kWh: e.kWh, peakKW: e.peakKW, tariff: options.tariff }) } : {})\n }\n }\n })()\n : {}),\n leadTime: stats(leads),\n workTime: stats(works),\n waitTime: stats(waits),\n utilization,\n unpaired: { leadTime: unpairedLead, workTime: unpairedWork },\n eventsSeen: seen,\n eventsInWindow: inWindow,\n ...(options.groupBy ? { groups: groupRecords(records, options) } : {})\n }\n}\n\n/**\n * 관점 × 비교 교차 — **\"구역별로 어제 대비 어떤가\"**.\n *\n * 축(무엇을)과 비교(언제 대비)를 따로 보면 사용자가 머릿속에서 두 표를 대조해야 한다. 운영에서 가장\n * 값 있는 질문은 그 교차다: 전체는 비슷한데 **한 구역만 나빠진** 경우를 전체 숫자로는 절대 못 본다.\n *\n * 정직성 규율:\n * · 두 구간에 **모두 있는** 축만 delta 를 낸다.\n * · 지금만 있는 축은 `isNew`(이전에 없던 일) — delta 를 건수와 같게 적으면 \"늘었다\" 로 오독된다.\n * · **사라진 축**(전에 있었고 지금 없는 것)은 따로 돌려준다. 표에서 그냥 빠지면 **멈춘 구역이\n * 눈에 보이지 않는다** — 운영에서는 이것이 가장 중요한 신호일 수 있다.\n */\nexport interface CrossedGroup extends KpiGroup {\n /** 비교 구간의 건수(두 구간에 모두 있을 때만). */\n prevTasks?: number\n /** 현재 − 비교구간 건수(두 구간에 모두 있을 때만). */\n deltaTasks?: number\n /** 비교 구간 대비 작업시간 중앙값 차이(둘 다 측정된 때만). */\n deltaWorkP50Ms?: number\n /** 비교 구간에는 없던 축 — delta 대신 이 표식을 준다. */\n isNew?: boolean\n}\n\nexport function crossGroups(\n now: KpiGroup[],\n prev: KpiGroup[]\n): { items: CrossedGroup[]; disappeared: { key: string; tasks: number }[] } {\n const before = new Map(prev.map(g => [g.key, g]))\n const items: CrossedGroup[] = now.map(g => {\n const p = before.get(g.key)\n if (!p) return { ...g, isNew: true }\n return {\n ...g,\n prevTasks: p.tasks,\n deltaTasks: g.tasks - p.tasks,\n ...(deltaP50(g.workTime, p.workTime) !== undefined ? { deltaWorkP50Ms: deltaP50(g.workTime, p.workTime) } : {})\n }\n })\n const present = new Set(now.map(g => g.key))\n const disappeared = prev.filter(g => !present.has(g.key)).map(g => ({ key: g.key, tasks: g.tasks }))\n return { items, disappeared }\n}\n\n/* ── 시간 비교 ──────────────────────────────────────────────────────────────\n * 숫자 하나로는 좋아졌는지 나빠졌는지 알 수 없다(\"29초\" 가 개선인지 악화인지). 같은 길이의 창을 옮겨\n * 한 번 더 접으면 되므로(폴드는 순수) 값싸다. 창 옮기기와 차이 계산은 계산 규칙이라 여기 있다 —\n * 조회 층에 두면 테스트가 DB 를 끌고 와야 한다.\n */\n\n/**\n * 비교 구간의 창 — 같은 **길이**로 옮긴다.\n *\n * previous: 바로 앞 구간(경계가 맞물리게 — 겹치거나 벌어지면 구간의 합이 어긋난다).\n * yesterday: 정확히 24시간 전 같은 자리. 시뮬 트윈의 클록이 실제 시각과 달라도 **차분은 정확**하다.\n */\nexport function shiftWindow(fromMs: number, toMs: number, basis: 'previous' | 'yesterday'): KpiWindow {\n const span = toMs - fromMs\n const back = basis === 'yesterday' ? 24 * 60 * 60_000 : span\n return { fromMs: fromMs - back, toMs: toMs - back }\n}\n\n/**\n * 중앙값 차이(현재 − 비교구간) — **둘 다 측정된 것만** 낸다.\n *\n * 한쪽이 결측이면 차이는 **없는 것**이다(0 이 아니다). 0 으로 내면 \"변화 없음\" 이라는 거짓이 된다.\n * 좋고 나쁨은 판단하지 않는다 — 처리량은 클수록, 소요는 작을수록 좋다는 해석은 소비처의 몫이다.\n */\nexport function deltaP50(now: DurationStats, prev: DurationStats): number | undefined {\n return now.count > 0 && prev.count > 0 ? now.p50Ms - prev.p50Ms : undefined\n}\n\n/** 구간 하나의 최소 요약 — 추세를 그릴 정보만(전체 KPI 를 구간마다 실어 보내지 않는다). */\nexport interface KpiBucket {\n fromMs: number\n toMs: number\n tasks: number\n orders: number\n /** 그 구간의 작업시간 중앙값 — 추세에서 \"느려지고 있는가\" 를 보는 값. */\n workP50Ms: number\n}\n\n/**\n * 창을 N 등분해 구간별로 접는다 — **같은 이벤트를 창만 바꿔** 다시 접는다(조회 추가 없음).\n *\n * 마지막 구간은 나머지를 흡수한다(부동소수 나눗셈으로 끝이 밀리면 마지막 완료가 빠진다).\n * `count <= 1` 이면 쪼갤 것이 없으므로 빈 배열 — 호출부가 \"추세 없음\" 으로 다룬다.\n */\nexport function foldKpiBuckets(events: KpiEvent[], window: KpiWindow, count: number): KpiBucket[] {\n const n = Math.max(0, Math.min(48, Math.floor(count)))\n if (n <= 1) return []\n const step = (window.toMs - window.fromMs) / n\n const out: KpiBucket[] = []\n for (let i = 0; i < n; i++) {\n const fromMs = window.fromMs + step * i\n const toMs = i === n - 1 ? window.toMs : window.fromMs + step * (i + 1)\n const b = foldKpi(events, { fromMs, toMs })\n out.push({ fromMs, toMs, tasks: b.throughput.tasks, orders: b.throughput.orders, workP50Ms: b.workTime.p50Ms })\n }\n return out\n}\n"]}
|
|
1
|
+
{"version":3,"file":"kpi-fold.js","sourceRoot":"","sources":["../../server/engine/kpi-fold.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 업무 KPI 폴드 — **이벤트 저널을 접어** 시간창 지표를 만든다(순수).\n *\n * ── 왜 필요한가 ─────────────────────────────────────────────────────────────\n * 지금 트윈이 답할 수 있는 것은 \"이 순간의 값\"(점유 92%)과 \"인프라 부하\"(초당 이벤트 수)뿐이다.\n * 운영이 묻는 것은 다르다 — **지난 한 시간에 몇 건 처리했나, 한 건에 얼마나 걸리나, 자원은 얼마나\n * 일했나.** 그 값이 없으면 AI 는 \"점유 92%\" 는 말해도 \"목표보다 나쁜가\" 는 답할 수 없다.\n *\n * ── 원료는 이미 있다 ────────────────────────────────────────────────────────\n * 저널(`TwinEvent`)에 `task.status`·`order.status`·`equipment.status` 전이가 `eventTime`(ISO)과 함께\n * append-only 로 쌓인다. 그래서 새 계측을 심을 필요가 없다 — **접기만 하면 된다.**\n * 같은 모양의 선례가 이 폴더에 있다(`oee-accumulator.ts` — equipment 전이를 접어 OEE 카운터 생산).\n *\n * ── 이 파일이 하지 않는 것 ──────────────────────────────────────────────────\n * · 목표와 비교하지 않는다. \"95% 목표 대비\" 는 목표를 저장하는 별개 층(경영 KPI)의 몫이고,\n * 이 층은 **사실만** 생산한다. 두 층을 섞으면 목표를 정의하지 않은 현장은 사실조차 못 본다.\n * · 체류시간(dwell)은 아직 내지 않는다. 물건이 노드에 머문 시간은 item 단위 이벤트(epcis.*)를\n * 짝지어야 하고, 그 짝맞춤 규칙이 도메인마다 다르다. **없는 것을 0 으로 꾸미지 않는다.**\n * · DB·커널을 모른다. 입력은 평범한 배열이고 출력은 평범한 객체다(테스트가 값싸다).\n */\n\n/*\n * 짝맞춤은 **커널이 소유한다**(`foldTaskRecords`). 완료는 작업당 하나·나중 것이 사실, 착수는 처음 것,\n * 종류·오더는 어느 전이에서든 — 그 규칙은 커널 상태 기계에서 나오는 지식이다. 여기서 다시 적었더니\n * 공정 타임라인과 **다른 답**을 냈다(재전송된 완료를 두 건으로 세어 처리량·점유가 부풀려졌다).\n * 이 파일은 이제 \"접힌 기록 → 창 지표\" 만 한다.\n */\nimport {\n ENERGY_EVENT,\n attributeEnergy,\n electricityCost,\n energyIntensity,\n energyOfWindows,\n foldTaskRecords,\n isOrderTerminal,\n activeShiftAt,\n type ElectricityCost,\n type TariffDeclaration,\n type TaskFacets,\n type WorkCalendarEntry\n} from '@operato/twin-kernel'\n\n/** 저널 한 줄 — 필요한 것만(엔티티·typeorm 비의존). */\nexport interface KpiEvent {\n eventType?: string\n /** ISO 시각. 파싱 불가하면 그 줄은 버린다(조용히 0 으로 세지 않는다). */\n eventTime?: string\n payload?: any\n}\n\nexport interface KpiWindow {\n /** 창의 시작·끝(epoch ms). 완료 시각이 이 안에 든 것만 성과로 센다. */\n fromMs: number\n toMs: number\n}\n\nexport interface DurationStats {\n /** 짝이 맞아 계산된 건수. */\n count: number\n /**\n * 하위 꼬리 — **분포를 양쪽에서 접지하기 위해** 낸다.\n * p50·p90 만 있으면 아래쪽 퍼짐을 위쪽에서 거울처럼 베껴 쓸 수밖에 없다(발명). 관측이 말하게 한다.\n */\n p10Ms: number\n p50Ms: number\n p90Ms: number\n avgMs: number\n}\n\n/**\n * 관점 축 — **같은 사실을 다른 각으로** 본다.\n *\n * 고정된 한 렌즈(자원별)만 낼 수 있으면 사용자가 \"구역별로는?\" 이라고 물어도 답이 없다. 사실 계산이\n * 축을 못 내면 AI 도 못 낸다 — 그래서 축은 계산 층의 능력이어야 한다.\n *\n * resource 자원별(누가 했나) taskKind 작업 종류별(무엇을 했나)\n * location 도착 지점별(어디서) order 오더별(무엇을 위해)\n * area 구역별 — 노드→구역 지도가 필요하다(이벤트에 없다. 호출부가 준다)\n * shift 교대별(언제) — 완료 시각 + 현장의 시각 기준에서 **계산한다**(이벤트에 없다)\n */\nexport type KpiGroupBy = 'resource' | 'taskKind' | 'location' | 'order' | 'area' | 'shift'\n\nexport interface KpiGroup {\n /** 축의 값(자원 id·작업 종류·노드 id·오더 id·구역 id). 값이 없던 기록은 'unknown'. */\n key: string\n /** 이 축에서 완료된 건수. */\n tasks: number\n leadTime: DurationStats\n workTime: DurationStats\n waitTime: DurationStats\n /** 이 축에 붙어 있던 시간의 합(자원 축에서 특히 의미 있다). */\n busyMs: number\n /**\n * **품질 판정이 있었던 건수만** 센다 (2026-08-19).\n *\n * 판정이 없는 작업(이동·체류·중간 스테이션)은 어느 쪽에도 세지 않는다 — 모름을 양품으로 세면 양품률이\n * 조용히 100% 로 올라간다. 그래서 `good + scrap` 은 `tasks` 보다 작을 수 있고, 그것이 정상이다.\n *\n * 판정이 하나도 없으면 **필드를 만들지 않는다**(0/0 을 「양품률 0%」로 읽히게 두지 않는다).\n */\n quality?: { good: number; scrap: number }\n}\n\nexport interface KpiResult {\n window: { fromMs: number; toMs: number }\n /** 창 안에 **완료된** 건수 — 성과는 완료 시점으로 센다(시작 시점으로 세면 진행 중인 것이 섞인다). */\n throughput: { tasks: number; orders: number }\n /**\n * 에너지 — **이 창의 전력량과 원단위.** 에너지 트윈이 없으면 `undefined`(0 이 아니다).\n *\n * ── 왜 KPI 폴드 안에 있나 (2026-08-14, §10 7단계) ─────────────────────────\n * 원단위는 분자(전력량)와 분모(산출)를 **같은 창**에서 나눈 값이다. 질의를 따로 두면 두 창을 맞추는\n * 일이 소비처의 몫이 되고, 어긋난 두 사실을 나눈 숫자는 아무것도 뜻하지 않는다(커널의 규칙이 그것을\n * 거절한다). 같은 폴드 안에 두면 창이 하나뿐이라 어긋날 수 없다.\n *\n * 분자는 **저널의 구간 마감 사건**에서 온다(`energy.demand.window`) — 커널 상태가 아니라 저널이므로\n * 멈춘 트윈·과거 구간에도 답할 수 있다.\n */\n energy?: {\n /** 이 창의 전력량. 셀 수 있는 구간이 없으면 `undefined`. */\n kWh?: number\n /**\n * 이 창의 **최대수요**(kW)와 그것이 난 구간의 시작.\n *\n * 전력량과 다른 질문이다: 전력량은 요금의 사용량 부분이고, 최대수요는 **기본요금**과 계약 위반을\n * 정한다. 평균으로 갈음할 수 없어서 커널이 구간의 최대들 중 가장 큰 값을 따로 낸다.\n */\n peakKW?: number\n peakStartMs?: number\n /**\n * 그 피크 구간에서 **설비가 끌어당긴** 최대(발전·방전 차감 전)와 그 차이(깎인 양).\n *\n * 요금은 계통 수요(`peakKW`)로 매겨지지만, 「무엇이 그 수요를 만들었나」와 「무엇이 덜어냈나」는\n * 이 두 수의 간격이 답한다. 발전·축전을 선언하지 않은 현장에서는 없다(차감할 것이 없다).\n */\n peakGrossKW?: number\n shavedKW?: number\n /** 파생의 근거 — 구간 평균부하 × 길이(`mean-kw`). */\n basis: string\n /** 센 구간 수 · 못 센 구간 수(계측이 끊긴 구간) — 조용히 자르지 않는다. */\n counted: number\n skipped: number\n /** 완료 오더당 전력량 — 답할 수 없으면 `null` 과 이유. */\n intensity: { value: number | null; unit?: string; reason?: string }\n /**\n * 배전 계통의 **자리별 몫** — 「이 분기가 얼마 썼나」.\n *\n * ── 이것은 측정이 아니라 배분이다 ──────────────────────────────────────\n * 저널에 있는 에너지 사실은 **현장 총량 한 줄**이다(뿌리 계량기 합). 계량기별 구간 사실이 없으므로\n * 자리별 몫은 **나눈 것**이고, 무엇으로 나눴는지(`weightKind`)를 값과 함께 낸다. 근거를 감추면\n * 나눈 숫자가 잰 숫자와 같은 무게로 읽힌다 — CBAM·ESG 보고에서 첫 질문에 깨지는 지점이다.\n *\n * 가중치는 **작업 가동시간**이다. 정격(kW)으로 나누지 않는다: 시뮬 트윈의 총부하가 정격의 합으로\n * 파생되므로, 그 총량을 다시 정격으로 나누면 넣은 값이 그대로 나오는 동어반복이 된다.\n *\n * 그래서 이 값에는 **가정이 하나 실려 있다**: 전기가 가동시간에 비례한다. 공조·조명처럼 공정에\n * 붙지 않는 부하도 그 비율에 섞인다 — 화면이 그 문장을 함께 말해야 한다.\n */\n byFeeder?: {\n /** 자리(분기·수전) id. 자리를 모르는 자원은 `unknown` 으로 모인다(조용히 버리지 않는다). */\n key: string\n kWh: number\n /**\n * 이 몫의 근거 — **커널이 정한다.**\n *\n * 선언된 부하가 여럿이면 배분(`apportioned`)이다. 선언된 부하가 **하나뿐인** 현장에서는 커널이\n * 「그 계량기는 그것만 잰다」고 읽어 `measured` 를 낸다 — 모델이 그렇게 말했으므로 그것도 사실이다.\n * 우리가 어느 쪽으로도 낙인찍지 않는다.\n */\n basis: 'apportioned' | 'measured'\n /**\n * 무엇으로 나눴나 — **배분일 때만** 있다.\n *\n * 분기마다 계량기가 있는 현장에서는 나누지 않는다(그 값이 곧 계량이다). 그때 이 칸을\n * `runtimeMs` 로 채우면 「가동시간으로 나눴다」는 없는 사실을 말하게 된다.\n */\n weightKind?: 'runtimeMs'\n /** 이 자리의 몫이 전체의 얼마였나 — 배분일 때만. */\n weightShare?: number\n /**\n * **피크가 찍힌 순간** 이 분기가 얼마였나(kW) — 요금이 걸린 수다.\n *\n * 창의 평균이나 합이 아니다: 최대수요를 낸 구간의 그 순간 값이다(다른 순간을 섞으면 「어느 분기를\n * 깎아야 피크가 내려가나」의 답이 흐려진다). 그 구간에 이 분기의 표본이 없었으면 **없다**.\n */\n peakKW?: number\n }[]\n /**\n * 나누지 못한 양과 그 이유 — **조용히 0 으로 만들지 않는다.**\n * `no-weights` = 그 창에 돌아간 자원이 없다(가동시간이 전부 0). 그러면 총량은 그대로 남는다.\n */\n unattributedKWh?: number\n /**\n * 왜 나누지 못했나(커널 코드) — `no-weights`(가동시간이 전부 0) · `no-consumers`(덮는 대상 미선언) ·\n * `not-submetered`(뿌리에서 재었으나 하위 계량이 덮지 않는 부분 — 결손이 아니라 계량 범위의 사실).\n */\n unattributedReason?: string\n /**\n * 분기 계량의 합이 뿌리 계량보다 **큰** 양 — 두 계량기의 회계가 다르다는 사실이다.\n *\n * 값을 깎아 맞추지 않는다: 그것은 계량을 우리가 고쳐 쓰는 일이고, 어느 쪽이 맞는지는 현장이 안다.\n */\n submeterExcessKWh?: number\n /**\n * 배분에서 **빠진 자원 수** — 그 창에 가동 기록이 없는 설비다. 0kWh 를 주지 않는 대신 몇 대가\n * 빠졌는지 센다(대기전력이 있는 설비에서 0 은 곧 거짓이다).\n */\n excludedResources?: number\n /**\n * 이 창의 전기요금 — **청구서가 아니다.** 계산과 그 한계(TOU·래칫·최소청구액 미모델)는 커널\n * (`electricityCost`)이 갖고 여기서는 나른다. 단가가 없으면 `reason: 'no-tariff'` 로 온다.\n */\n cost?: ElectricityCost\n }\n /** 리드타임 — 생성(created)에서 완료까지. 사용자가 체감하는 소요 시간. */\n leadTime: DurationStats\n /** 작업시간 — 착수(in-progress)에서 완료까지. 대기를 뺀 실작업. */\n workTime: DurationStats\n /** 대기시간 — 생성에서 착수까지. 자원이 부족하면 여기가 늘어난다. */\n waitTime: DurationStats\n /** 자원별 점유 — 작업에 붙어 있던 시간의 합과 창 대비 비율(작업으로 설명되는 부분만). */\n utilization: { resourceId: string; busyMs: number; ratio: number }[]\n /**\n * **셈에서 빠진 것** — 완료는 창 안이지만 시작 이벤트를 못 찾은 건수.\n * 조회 구간이 짧아 시작이 잘렸거나 저널이 유실된 경우다. 이 값을 숨기면 평균이 조용히 거짓이 된다.\n */\n unpaired: { leadTime: number; workTime: number }\n /**\n * **완료로 세지 않은 오더 상태 낱말의 인구조사** — 낱말 → 창 안에서 그 낱말로 본 건수.\n *\n * 「못 읽었다」가 **아니다.** `picking` 처럼 완료가 아닌 낱말이 여기 오는 것은 정상이다. 이 값은\n * 판정이 아니라 **재료**다: 완료가 0 인데 여기 `{ finished: 2780 }` 이 있으면 원인이 곧 보인다.\n *\n * ── 왜 필요한가, 그리고 무엇의 대체가 아닌가 ────────────────────────────────\n * 이 폴드는 완료를 **동의어 목록**으로 판정한다(`completed`·`fulfilled`·`done`·`finished`). 그 방식은\n * 원본의 낱말에서 뜻을 **추론**하는 것이고, 이 저장소가 다른 자리에서 거절하는 그것이다\n * (「선언이 있으면 변환이지만 없으면 발명이다」).\n *\n * 실제로 그 대가를 치렀다: `finished` 가 목록에 없어 첫 실 시스템의 완료 오더가 전부 0 이었다.\n * 그런데 **빈 자리는 이 목록이 아니라 커넥터였다** — 그 커넥터에 상태 표(`WORK_ORDER_STATE`)가\n * 정의돼 있으면서 오더 경로에서 쓰이지 않았고(로트 경로는 썼다), 원본 낱말이 그대로 흘렀다.\n * 목록에 `finished` 를 더한 것은 그 사실을 가린 땜빵이었다.\n *\n * **정석은 커넥터가 자기 표로 옮기고, 옮길 수 없는 낱말은 세고 넣지 않는 것이다**(재고 처분이 이미\n * 그 규율로 선다: 「표현할 길이 없는 것은 넣지 않는다」). 그러면 이 폴드는 추론하지 않아도 된다.\n *\n * 그러므로 이 값은 **동의어 목록을 늘리는 근거가 아니다.** 목록은 커넥터들이 옮기기 시작하면\n * 닫힌 어휘로 좁혀야 하고, 그때까지 이 값이 어긋남을 말해 준다. 순서가 중요하다 — 목록을 먼저\n * 좁히면 아직 옮기지 않는 원본의 완료가 그 순간 0 이 된다.\n *\n * 상태를 말하지 않은 사건은 여기 오지 않는다. 그것은 「모르는 낱말」이 아니라 다른 결함이고\n * (유입 문이 상태를 필수로 받는다) 그 진단은 그 문의 몫이다.\n */\n orderStatusNotCounted: Record<string, number>\n /**\n * 소비한 이벤트 수 — **넘겨받은 배열 전체**(창 앞 lookback 포함). 진단용이다.\n *\n * \"측정된 기록이 있는가\" 의 판단에는 쓰지 말 것 — 호출부는 창보다 앞까지 읽어 넘기므로(완료된 작업의\n * 시작이 창 밖일 수 있다) 창 안이 비어 있어도 이 값은 0 이 아니다. 그 판단은 `eventsInWindow` 로 한다.\n */\n eventsSeen: number\n /**\n * **창 안에** 들어온 이벤트 수 — \"이 구간에 측정할 기록이 있었는가\" 의 정답.\n *\n * 이것이 0 이면 \"처리량 0\" 이 아니라 **\"기록이 없다\"** 다. 두 상황을 섞으면 사용자가 잘못된 결론을\n * 낸다(앞 구간의 기록만 읽어 놓고 \"0건 처리\" 라고 말하는 일이 실제로 가능했다).\n */\n eventsInWindow: number\n /** 요청한 관점으로 쪼갠 값(요청했을 때만). */\n groups?: { by: KpiGroupBy; items: KpiGroup[]; truncated?: number; truncatedTasks?: number }\n}\n\n/**\n * 창의 전력량을 **배전 계통의 자리별로 나눈다** — 근거를 함께.\n *\n * ── 왜 커널 함수를 쓰나 ─────────────────────────────────────────────────────\n * 「나눌 근거가 없으면 나누지 않는다」·「합은 보존된다」·「몫이 0 이면 0kWh 를 주지 않는다」는 규칙이\n * 커널의 `attributeEnergy` 에 이미 있다. 여기서 다시 쓰면 두 벌이 되고, 그때부터 두 화면이 다른 합을\n * 말한다. 이 함수가 하는 일은 **재료를 모아 넘기고 답을 자리별로 묶는 것**뿐이다.\n *\n * ── 가중치는 가동시간이다 ───────────────────────────────────────────────────\n * 정격(kW)으로 나누지 않는다: 시뮬 트윈의 총부하가 정격의 합에서 파생되므로 그 총량을 다시 정격으로\n * 나누면 넣은 값이 그대로 나온다(새 정보 0의 동어반복). 가동시간은 **저널이 관측한 사실**이다.\n *\n * 자리를 모르는 자원은 `unknown` 으로 모인다 — 빼면 합이 줄고, 아무 자리에 넣으면 거짓이 된다.\n */\nfunction feederShares(\n kWh: number | undefined,\n utilization: { resourceId: string; busyMs: number }[],\n resourceFeeder?: Record<string, string>\n): Partial<NonNullable<KpiResult['energy']>> {\n /* 선언이 없으면 분해하지 않는다 — 자리를 모르는 채 나눈 숫자는 아무 것의 몫도 아니다. */\n if (!resourceFeeder || !Object.keys(resourceFeeder).length) return {}\n if (kWh === undefined || !(kWh > 0)) return {}\n\n /*\n * ── 풀이 **무엇을 덮는지 사실대로** 말한다 (2026-08-17) ────────────────────\n *\n * 처음에는 「그 창에 돌아간 자원」만 덮는다고 넘겼다. 그러자 돌아간 자원이 하나인 창에서 커널이\n * 그것을 **`measured`(전용 계량)** 로 판정했다 — 커널의 규칙은 옳다(「덮는 소비처가 하나면 그\n * 계량기는 그것만 잰다는 선언이다」). 틀린 것은 내 선언이었다: 현장 계량기는 돌아간 것만 재지 않는다.\n *\n * 그래서 **선언된 부하 전부**를 덮는다고 말한다(`resourceFeeder` 의 키가 곧 그 선언이다).\n * 그러면 그 창에 돌지 않은 설비는 몫 0 으로 **배분에서 빠지고 그 수가 보고된다** — 0kWh 를 주는\n * 대신 「이 설비는 이 창에 가동 기록이 없다」를 말하는 쪽이다.\n */\n /*\n * 덮는 대상은 **선언된 부하 ∪ 그 창에 돌아간 자원**이다.\n *\n * 선언만 쓰면, 돌았는데 전기 계통에 선언되지 않은 설비의 몫이 나머지 설비에 얹힌다 — 그 설비가\n * 안 쓴 전기를 쓴 것으로 만든다. 돌아간 것만 쓰면, 선언됐지만 쉰 설비가 사라져 「이 창에 안 돌았다」를\n * 말할 수 없다. 둘을 합해야 두 사실이 다 남는다(자리를 모르는 몫은 `unknown` 으로 모인다).\n */\n const busyOf = new Map(utilization.map(u => [u.resourceId, u.busyMs]))\n const ids = new Set<string>([...Object.keys(resourceFeeder), ...busyOf.keys()])\n const consumers = [...ids].map(id => ({ id, weight: busyOf.get(id) ?? 0 }))\n if (!consumers.length) return {}\n\n const r = attributeEnergy({\n pools: [{ meterId: 'site', kWh, consumerIds: consumers.map(c => c.id) }],\n consumers,\n weightKind: 'runtimeMs'\n })\n\n const byKey = new Map<string, { kWh: number; weightShare: number; apportioned: boolean }>()\n for (const sh of r.shares) {\n /* 자리를 모르면 `unknown` — 그 몫이 있다는 사실은 남기고, 어디의 것인지는 지어내지 않는다. */\n const key = resourceFeeder[sh.consumerId] ?? 'unknown'\n const cur = byKey.get(key) ?? { kWh: 0, weightShare: 0, apportioned: false }\n cur.kWh += sh.kWh\n cur.weightShare += sh.weightShare ?? 0\n /* **근거는 커널이 정한다.** 여기서 낙인찍으면 커널이 「쟀다」고 한 것을 「나눴다」고 바꿔 말하거나\n 그 반대가 된다. 섞이면 약한 쪽(배분)으로 적는다 — 강한 주장을 올려 잡지 않는다. */\n if (sh.basis === 'apportioned') cur.apportioned = true\n byKey.set(key, cur)\n }\n\n const byFeeder = [...byKey.entries()]\n .map(([key, v]) => ({\n key,\n kWh: v.kWh,\n basis: (v.apportioned ? 'apportioned' : 'measured') as 'apportioned' | 'measured',\n weightKind: 'runtimeMs' as const,\n weightShare: v.weightShare\n }))\n .sort((a, b) => b.kWh - a.kWh)\n\n const un = r.unattributed.reduce((sum, u) => sum + u.kWh, 0)\n return {\n ...(byFeeder.length ? { byFeeder } : {}),\n ...(un > 0 ? { unattributedKWh: un, unattributedReason: r.unattributed[0]?.reason } : {}),\n ...(r.excluded.length ? { excludedResources: r.excluded.length } : {})\n }\n}\n\n/**\n * 분기별 전력량을 **계량으로** 읽는다 — 계량이 있으면 배분하지 않는다.\n *\n * ── 왜 필요했나 (2026-08-18) ────────────────────────────────────────────────\n * 실 게이트웨이는 분기마다 계량기를 보낸다. 그런데 화면은 「나누지 못함」으로 답하고 있었다 — 여기에\n * 가동시간 배분 경로만 있었기 때문이다. 계량이 있는데 배분으로 말하면 **근거를 실제보다 약하게** 보고하는\n * 것이고, 그 위에 세운 SEU 판단이 무거운 값을 놓친다. 미러 트윈에는 설비 가동 기록조차 없어서\n * (원천이 계량만 준다) 그 경로로는 분기별 값이 영원히 나오지 않았다.\n *\n * 값의 출처는 커널이 구간 마감 사실에 실어 주는 **지점별 몫**이다(적산의 뺄셈). 표본을 직접 읽지 않는\n * 이유는 조회 상한이다 — 계측은 초당 여러 건이라 표본으로는 창을 채우기 전에 상한에 걸린다.\n *\n * 나머지(뿌리 − 분기 합)는 **버리지도 얹지도 않는다**: 수전 직결 부하·손실·계량되지 않은 회로가 그\n * 차이이므로, 어느 분기에 얹으면 그 분기가 쓰지 않은 전기를 쓴 것으로 만든다. 커널의 `remainder` 풀로\n * 넘겨 「하위 계량이 덮지 않는다」로 남긴다.\n *\n * 분기 계량 합이 뿌리보다 크면(회계가 다른 두 계량기) **그 사실을 낸다** — 값을 깎아 맞추면 계량을\n * 우리가 고쳐 쓰는 것이다.\n */\nfunction measuredFeeders(\n windows: readonly {\n startMs: number\n endMs: number\n maxKW?: number\n points: { id: string; kWh: number; root: boolean }[]\n pointsAtPeak?: { id: string; kW: number }[]\n }[],\n range: { startMs: number; endMs: number },\n totalKWh: number | undefined,\n meterFeeder?: Record<string, string>\n): Partial<NonNullable<KpiResult['energy']>> | undefined {\n if (!meterFeeder || !Object.keys(meterFeeder).length) return undefined\n if (totalKWh === undefined || !(totalKWh > 0)) return undefined\n\n /* 창에 온전히 든 구간만 — 전체 전력량과 같은 규칙이어야 몫과 합이 같은 창을 말한다. */\n const byFeederKWh = new Map<string, number>()\n let sawPoints = false\n for (const w of windows) {\n if (!(w.startMs >= range.startMs && w.endMs <= range.endMs)) continue\n for (const p of w.points ?? []) {\n const feeder = meterFeeder[p.id]\n if (!feeder) continue // 뿌리 계량기·모르는 지점 — 분기의 몫이 아니다\n sawPoints = true\n byFeederKWh.set(feeder, (byFeederKWh.get(feeder) ?? 0) + p.kWh)\n }\n }\n if (!sawPoints) return undefined // 계량이 없으면 이 경로를 쓰지 않는다(배분으로 돌아간다)\n\n const feeders = [...byFeederKWh.entries()].filter(([, kWh]) => kWh > 0)\n if (!feeders.length) return undefined\n const submetered = feeders.reduce((sum, [, kWh]) => sum + kWh, 0)\n const remainder = totalKWh - submetered\n\n const r = attributeEnergy({\n pools: [\n ...feeders.map(([feeder, kWh]) => ({ meterId: `${feeder}:meter`, kWh, consumerIds: [feeder] })),\n ...(remainder > 0 ? [{ meterId: 'root:remainder', kWh: remainder, consumerIds: [], remainder: true }] : [])\n ],\n consumers: feeders.map(([feeder]) => ({ id: feeder }))\n })\n\n /*\n * ── 피크를 만든 분기 (2026-08-18) ─────────────────────────────────────────\n *\n * 요금은 kW 로 매겨지므로 「어느 분기를 깎아야 피크가 내려가나」가 실무의 질문이다. 그 답은 **피크가\n * 찍힌 구간의 그 순간** 값이고, 창 안의 여러 구간을 더하거나 평균하면 뜻이 사라진다(다른 순간의 값을\n * 섞는 것이다). 그래서 **최대수요를 낸 구간 하나**만 골라 그 순간의 지점 값을 쓴다.\n */\n const peakWindow = windows\n .filter(w => w.startMs >= range.startMs && w.endMs <= range.endMs && Number.isFinite(Number(w.maxKW)))\n .reduce<(typeof windows)[number] | undefined>((best, w) => (!best || Number(w.maxKW) > Number(best.maxKW) ? w : best), undefined)\n const peakOf = new Map<string, number>()\n for (const p of peakWindow?.pointsAtPeak ?? []) {\n const feeder = meterFeeder[p.id]\n if (!feeder) continue // 뿌리 계량기·모르는 지점 — 분기의 몫이 아니다\n peakOf.set(feeder, (peakOf.get(feeder) ?? 0) + p.kW)\n }\n\n const byFeeder = r.shares\n .map(sh => ({\n key: sh.consumerId,\n kWh: sh.kWh,\n basis: sh.basis,\n weightKind: undefined,\n weightShare: undefined,\n /* 피크 순간의 값이 없으면 만들지 않는다 — 그 구간에 이 분기의 표본이 없었다는 뜻이다. */\n ...(peakOf.has(sh.consumerId) ? { peakKW: peakOf.get(sh.consumerId) } : {})\n }))\n .sort((a, b) => b.kWh - a.kWh) as NonNullable<KpiResult['energy']>['byFeeder']\n\n const un = r.unattributed.reduce((sum, u) => sum + u.kWh, 0)\n return {\n ...(byFeeder?.length ? { byFeeder } : {}),\n ...(un > 0 ? { unattributedKWh: un, unattributedReason: r.unattributed[0]?.reason } : {}),\n /* 분기 합이 뿌리를 넘었다 — 두 계량기의 회계가 다르다는 사실이다(깎아 맞추지 않는다). */\n ...(remainder < 0 ? { submeterExcessKWh: -remainder } : {})\n }\n}\n\n/** 폴드 옵션 — 관점 축과, 그 축을 만들기 위해 이벤트 밖에서 와야 하는 지도. */\nexport interface KpiFoldOptions {\n groupBy?: KpiGroupBy\n /**\n * 노드 → 구역 지도. **이벤트에는 구역이 없다**(노드까지만 있다) — 스냅샷·마스터가 아는 값이라\n * 호출부가 넘긴다. 없으면 구역 축은 'unknown' 으로 모인다(조용히 다른 축으로 바꾸지 않는다).\n */\n locationArea?: Record<string, string>\n /**\n * 자원 → **배전 계통의 자리**(분기·수전) 지도.\n *\n * ── 왜 밖에서 오나 ───────────────────────────────────────────────────────\n * 이벤트에는 전기 계통이 없다. 「이 설비가 어느 분기에서 먹는가」는 에너지 트윈의 모델이 아는\n * 선언이고(설비 하나에 사실 둘 — 공장에서는 자원, 전기에서는 부하), 그 선언을 읽는 것은 호출부의 일이다.\n *\n * 없으면 분기 분해를 **하지 않는다** — 자리를 모르는 채 나누면 그 숫자는 아무 것의 몫도 아니다.\n */\n resourceFeeder?: Record<string, string>\n /**\n * **계량 지점 → 그것이 재는 분기** 지도.\n *\n * 있으면 분기별 전력량을 계량으로 읽는다(배분보다 강한 근거). 이 지도는 모델의 선언에서 오고\n * (계량기의 자리), 그것을 읽는 것은 호출부의 일이다 — 폴드는 저널만 본다.\n */\n meterFeeder?: Record<string, string>\n /**\n * 선언된 **전기요금 단가** — 수를 금액으로 바꾸는 유일한 입력.\n *\n * 이벤트에는 요금이 없다. 단가는 현장의 계약이고(에너지 트윈 모델의 수전 자리에 선언한다), 그것을\n * 읽는 것은 호출부의 일이다. 없으면 금액을 만들지 않는다 — 짐작한 단가로 낸 금액은 숫자가 있다는\n * 것만으로 사람을 결정으로 밀어붙인다.\n */\n tariff?: TariffDeclaration\n /**\n * 교대 선언 — **이벤트에는 교대가 없다**(시각만 있다). 현장 마스터가 아는 값이라 호출부가 넘긴다.\n * 없으면 교대 축은 'unknown' 으로 모인다(다른 축으로 조용히 바꾸지 않는다).\n */\n shifts?: WorkCalendarEntry[]\n /**\n * 현장의 시각 기준(분) — 교대 축이 `HH:MM` 을 어느 기준으로 읽을지. **공간이 갖는 값**이다\n * (테넌트가 아니다: 한 테넌트가 다른 시간대의 공장을 함께 가질 수 있다). 없으면 UTC.\n */\n utcOffsetMinutes?: number\n /**\n * 축의 상한(기본 20). 넘치면 잘라내고 **잘린 축의 수와 그 안의 건수를 함께 알린다**.\n *\n * 축의 수만 알리면 부족하다 — \"그 외 89개\" 로는 그것이 3건인지 300건인지 모른다. 보여준 축이 전부인\n * 것처럼 읽히면 사용자가 꼬리를 놓친다.\n */\n groupLimit?: number\n}\n\n/* ── 관점 축의 재료 ─────────────────────────────────────────────────────────\n * 축의 값은 이벤트 payload 에 이미 들어 있다(`TaskStatusDelta`: kind·orderId·fromNode·toNode·\n * resourceRef). 새 계측을 심을 필요가 없다 — **이미 쌓인 저널을 다르게 묶기만** 한다.\n */\n/** 창 안에 완료된 한 건의 기록 — 통계의 원료이자 관점 축의 원료. */\ninterface CompletionRecord {\n taskId: string\n at: number\n facets: TaskFacets\n leadMs?: number\n workMs?: number\n waitMs?: number\n}\n\n/** 이 기록이 요청한 축에서 어느 값에 속하는가. 값이 없으면 'unknown'(조용히 버리지 않는다). */\nfunction facetKey(rec: CompletionRecord, options: KpiFoldOptions): string {\n const f = rec.facets\n switch (options.groupBy) {\n case 'resource':\n return f.resource ?? 'unknown'\n case 'taskKind':\n return f.kind ?? 'unknown'\n case 'location':\n return f.location ?? 'unknown'\n case 'order':\n return f.order ?? 'unknown'\n case 'area':\n /* 구역은 이벤트 밖의 지식이다 — 지도가 없으면 없다고 말한다(노드 id 로 대체하면 사용자가\n * 그것을 구역으로 오해한다). */\n return (f.location && options.locationArea?.[f.location]) ?? 'unknown'\n case 'shift':\n /*\n * 교대는 **저널에 찍혀 있지 않다 — 계산한다.**\n *\n * 이벤트에 교대 이름을 스탬프하면 두 가지가 무너진다: ① 같은 사실이 두 곳에 생기고,\n * ② 나중에 교대 시간을 고치면 **과거가 옛 구분으로 굳어** 다시 셀 수 없다. 교대는\n * `완료 시각 + 현장의 시각 기준 + 교대 선언`에서 나오는 순수 파생이므로 여기서 낸다 —\n * 라이브·히스토리·예측이 **커널과 같은 함수**(`activeShiftOf`)를 쓴다.\n *\n * 선언이 없으면 'unknown' 이다. 교대를 나눠 놓지 않은 현장에 이름을 지어내지 않는다.\n */\n if (!options.shifts?.length) return 'unknown'\n /* 시각까지 넘긴다 — **휴일에 일어난 일은 어느 교대에도 속하지 않는다.** 되풀이만 보면 그날 서지도\n 않은 교대에 집계돼, 교대별 성과가 조용히 거짓이 된다. */\n return activeShiftAt(options.shifts, rec.at, options.utcOffsetMinutes) ?? 'unknown'\n default:\n return 'unknown'\n }\n}\n\n/**\n * 이 축의 품질 — **판정이 있었던 것만** 센다(없으면 필드를 만들지 않는다).\n *\n * 양품률의 이력이 여기서 나온다: 커널이 작업 완료에 결과를 적고(`TaskStatusDelta.outcome`), 폴드가 축으로\n * 들고(`TaskFacets.outcome`), 여기서 종류별로 센다. 그 사슬이 없으면 「이 공정의 양품률」은 저널에\n * 남지 않는다(설비 누적 카운터는 종류를 모른다).\n */\nfunction qualityOf(rows: CompletionRecord[]): { quality?: { good: number; scrap: number } } {\n let good = 0\n let scrap = 0\n for (const r of rows) {\n const o = r.facets.outcome\n if (o === 'good') good++\n else if (o === 'scrap') scrap++\n }\n return good + scrap > 0 ? { quality: { good, scrap } } : {}\n}\n\n/**\n * 기록을 축으로 묶어 축별 통계를 낸다 — **전체와 같은 규칙**으로(같은 stats 함수, 같은 창).\n *\n * 축의 값이 아주 많을 수 있다(오더 축은 수천 개). 그래서 건수 큰 것부터 상한까지만 돌려주고\n * **잘라낸 수를 함께 알린다** — 조용한 절단은 \"이게 전부\" 라는 거짓을 만든다.\n */\nfunction groupRecords(\n records: CompletionRecord[],\n options: KpiFoldOptions\n): { by: KpiGroupBy; items: KpiGroup[]; truncated?: number; truncatedTasks?: number } {\n const by = options.groupBy as KpiGroupBy\n const bins = new Map<string, CompletionRecord[]>()\n for (const rec of records) {\n const key = facetKey(rec, options)\n const bin = bins.get(key)\n if (bin) bin.push(rec)\n else bins.set(key, [rec])\n }\n\n const all = [...bins.entries()]\n .map(([key, rows]) => ({\n key,\n tasks: rows.length,\n leadTime: stats(rows.map(r => r.leadMs).filter((v): v is number => v !== undefined)),\n workTime: stats(rows.map(r => r.workMs).filter((v): v is number => v !== undefined)),\n waitTime: stats(rows.map(r => r.waitMs).filter((v): v is number => v !== undefined)),\n busyMs: rows.reduce((a, r) => a + (r.workMs ?? 0), 0),\n ...qualityOf(rows)\n }))\n .sort((a, b) => b.tasks - a.tasks || (a.key < b.key ? -1 : 1))\n\n const limit = Math.max(1, Math.min(100, options.groupLimit ?? 20))\n const items = all.slice(0, limit)\n const dropped = all.slice(limit)\n /* 축별로 \"창 대비 비율\" 은 내지 않는다 — 자원 아닌 축(오더·작업종류)에서 그 비율은 의미가 없고,\n * 의미 없는 숫자를 내면 사용자가 그것으로 판단한다. 자원 점유는 `utilization` 이 이미 낸다. */\n return {\n by,\n items,\n ...(dropped.length\n ? { truncated: dropped.length, truncatedTasks: dropped.reduce((a, g) => a + g.tasks, 0) }\n : {})\n }\n}\n\nfunction ms(value: string | undefined): number | null {\n if (!value) return null\n const t = Date.parse(value)\n return Number.isFinite(t) ? t : null\n}\n\n/** 분위수 — 정렬된 값에서 선형 보간 없이 가장 가까운 아래 값(작은 표본에서 과장하지 않는다). */\nfunction quantile(sorted: number[], q: number): number {\n if (sorted.length === 0) return 0\n const idx = Math.min(sorted.length - 1, Math.max(0, Math.floor(q * sorted.length)))\n return sorted[idx]\n}\n\nfunction stats(values: number[]): DurationStats {\n if (values.length === 0) return { count: 0, p10Ms: 0, p50Ms: 0, p90Ms: 0, avgMs: 0 }\n const sorted = [...values].sort((a, b) => a - b)\n const sum = sorted.reduce((a, b) => a + b, 0)\n return {\n count: sorted.length,\n p10Ms: quantile(sorted, 0.1),\n p50Ms: quantile(sorted, 0.5),\n p90Ms: quantile(sorted, 0.9),\n avgMs: Math.round(sum / sorted.length)\n }\n}\n\n/**\n * 이벤트를 접어 시간창 KPI 를 만든다.\n *\n * **호출부 규약**: `events` 는 창보다 **넉넉히 앞까지** 담아 넘긴다(예: 창 시작 − 리드타임 상한).\n * 완료된 작업의 시작 이벤트가 창 밖에 있을 수 있기 때문이다. 못 찾은 것은 `unpaired` 로 알린다 —\n * 평균에서 조용히 빠지면 지표가 거짓이 된다.\n *\n * 시간축은 `eventTime`(ISO) 차분이다. sim 클록이 실제 시각과 오프셋이 있어도 **차분은 정확**하다\n * (고정 오프셋은 상쇄된다). 파싱 불가한 줄은 버린다.\n */\nexport function foldKpi(events: KpiEvent[], window: KpiWindow, options: KpiFoldOptions = {}): KpiResult {\n /* 작업 짝맞춤은 **커널 규칙**으로(중복 구현 금지 — 두 곳에 적으면 같은 저널로 다른 답이 나온다). */\n const { records: tasks } = foldTaskRecords(events.filter(e => e.eventType === 'task.status'))\n let completedOrders = 0\n /* 완료로 읽지 못한 오더 상태 낱말 → 건수(§`orderStatusNotCounted`). */\n const orderStatusNotCounted = new Map<string, number>()\n /* 이미 완료로 센 오더 — 폴링 원본이 같은 종결을 다시 보내도 한 번만 센다. */\n const completedOrderIds = new Set<string>()\n let seen = 0\n let inWindow = 0\n\n for (const e of events) {\n const at = ms(e.eventTime)\n if (at === null) continue\n seen++\n if (at >= window.fromMs && at <= window.toMs) inWindow++\n const d = e.payload?.data ?? e.payload ?? {}\n\n if (e.eventType === 'order.status') {\n /*\n * **끝났나는 커널이 답한다** (2026-08-23) — `isOrderTerminal`.\n *\n * 예전에는 여기서 **동의어 목록**으로 추론했다(`completed`·`fulfilled`·`done`·`finished`).\n * 그 방식이 낸 대가가 둘이었고 둘 다 조용했다.\n *\n * ① 원본의 낱말이 목록에 없으면 완료가 0 이 된다. 실제로 그랬다 — 첫 실 시스템의 `FINISHED`\n * 가 빠져 있었고, 그때 목록에 낱말을 더한 것은 진짜 빈 자리(그 커넥터가 자기 상태 표를\n * 안 쓰고 있었다)를 가린 땜빵이었다.\n * ② **시뮬 트윈에서는 언제나 0 이었다.** 시뮬은 오더를 이행하며 `fulfilled` 만 올리고\n * `status` 는 바꾸지 않는다 — 낱말로 묻는 판정은 시뮬의 완료를 한 건도 세지 못했다.\n *\n * 표준이 그 축을 갈라 두었다: `orders.status` ↔ `RequestState`(표준도 열거하지 않는다)이고,\n * 「됐나」는 **실적**에서 읽는다(`JobResponse`, 요구는 `SegmentRequirement.Quantity`). 그래서\n * 판정의 1차 근거는 **양**이고 낱말은 양으로 말할 수 없는 종결(취소·중도 종료)에만 쓴다.\n */\n const status = String(d.status ?? '').toLowerCase()\n const done = isOrderTerminal({ status: d.status, requested: d.requested, fulfilled: d.fulfilled })\n if (at >= window.fromMs && at <= window.toMs) {\n /*\n * **오더당 한 번만 센다.** 폴링하는 원본은 같은 오더의 종결을 여러 주기에 걸쳐 다시 보낼 수\n * 있고(그것은 결함이 아니라 폴링의 성질이다), 사건마다 세면 처리량이 조용히 부풀려진다.\n * 작업 쪽은 커널의 짝맞춤이 이미 그 규율을 갖고 있다(「완료는 작업당 하나」) — 오더만 없었다.\n */\n if (done) {\n const id = String(d.orderId ?? '')\n if (id && !completedOrderIds.has(id)) {\n completedOrderIds.add(id)\n completedOrders++\n } else if (!id) completedOrders++ // 정체를 모르면 셈에서 빼지 않는다(조용히 줄이지 않는다)\n }\n /*\n * **완료로 세지 않은 낱말의 인구조사.** 「못 읽었다」가 아니다 — `picking` 처럼 완료가 아닌\n * 낱말이 여기 오는 것은 정상이다. 완료가 0 인데 여기 낱말이 쌓여 있으면 원인이 곧 보인다.\n *\n * 빈 낱말은 세지 않는다: 상태를 말하지 않은 사건은 다른 결함이고(유입 문이 상태를 필수로\n * 받는다) 여기서 섞으면 이 값의 뜻이 흐려진다.\n */\n else if (status) orderStatusNotCounted.set(status, (orderStatusNotCounted.get(status) ?? 0) + 1)\n }\n }\n }\n\n /*\n * 에너지 — 창에 **온전히 든** 구간 마감 사건만 센다(걸친 구간은 커널 규칙이 뺀다).\n * 사건이 하나도 없으면 이 트윈에는 에너지가 없는 것이므로 블록 자체를 내지 않는다.\n */\n const demandWindows = events\n .filter(e => e.eventType === ENERGY_EVENT.demandWindow)\n .map(e => {\n const d = e.payload?.data ?? e.payload ?? {}\n /* 최대(`maxKW`)도 함께 넘긴다 — 요금이 걸리는 수는 평균이 아니라 최대다.\n 총부하(`grossMaxKW`)는 「무엇이 깎였나」를 되짚는 축이다(발전·방전 차감 전). */\n return {\n startMs: Number(d.startMs),\n endMs: Number(d.endMs),\n meanKW: Number(d.meanKW),\n maxKW: Number(d.maxKW),\n grossMaxKW: Number(d.grossMaxKW),\n /* 지점별 몫 — 계량이 분기에 있으면 그 값은 **측정**이다(배분이 아니다). 커널이 계층 판정\n (`root`)까지 실어 주므로 여기서 같은 규칙을 다시 쓰지 않는다. */\n points: (Array.isArray(d.points) ? d.points : [])\n .map((p: any) => ({ id: String(p?.id ?? ''), kWh: Number(p?.kWh), root: p?.root === true }))\n .filter((p: any) => p.id && Number.isFinite(p.kWh)),\n /* 피크가 찍힌 순간의 지점별 kW — 요금이 걸린 수는 kW 다(전력량이 큰 분기와 피크를 만든 분기는 다르다). */\n pointsAtPeak: (Array.isArray(d.pointsAtPeak) ? d.pointsAtPeak : [])\n .map((p: any) => ({ id: String(p?.id ?? ''), kW: Number(p?.kW) }))\n .filter((p: any) => p.id && Number.isFinite(p.kW))\n }\n })\n .filter(w => Number.isFinite(w.startMs) && Number.isFinite(w.endMs))\n\n const leads: number[] = []\n const works: number[] = []\n const waits: number[] = []\n const busyByResource = new Map<string, number>()\n /* 창 안에 완료된 건들의 **기록** — 통계를 낸 뒤에도 버리지 않는다. 관점 축은 같은 기록을 다시\n * 묶는 것일 뿐이므로, 기록을 남기면 축을 하나 더 붙이는 값이 거의 0 이 된다. */\n const records: CompletionRecord[] = []\n let tasksInWindow = 0\n let unpairedLead = 0\n let unpairedWork = 0\n\n for (const t of tasks) {\n const completed = t.completedMs\n if (completed === undefined) continue // 아직 완료되지 않은 작업 — 성과로 세지 않는다\n if (completed < window.fromMs || completed > window.toMs) continue // 성과는 완료 시점으로 센다\n tasksInWindow++\n const c = t.createdMs\n const s = t.startedMs\n const rec: CompletionRecord = { taskId: t.taskId, at: completed, facets: t.facets }\n const resource = rec.facets.resource\n if (c !== undefined && completed >= c) leads.push((rec.leadMs = completed - c))\n else unpairedLead++\n if (s !== undefined && completed >= s) {\n const busy = completed - s\n works.push((rec.workMs = busy))\n if (resource) busyByResource.set(resource, (busyByResource.get(resource) ?? 0) + busy)\n } else {\n unpairedWork++\n }\n if (c !== undefined && s !== undefined && s >= c) waits.push((rec.waitMs = s - c))\n records.push(rec)\n }\n\n const span = Math.max(1, window.toMs - window.fromMs)\n const utilization = [...busyByResource.entries()]\n .map(([resourceId, busyMs]) => ({ resourceId, busyMs, ratio: Math.min(1, busyMs / span) }))\n .sort((a, b) => b.busyMs - a.busyMs)\n\n return {\n window: { fromMs: window.fromMs, toMs: window.toMs },\n throughput: { tasks: tasksInWindow, orders: completedOrders },\n ...(demandWindows.length\n ? (() => {\n const e = energyOfWindows(demandWindows, { startMs: window.fromMs, endMs: window.toMs })\n /* 분모는 **완료 오더**다(대당 전력). 창에 완료가 없으면 커널 규칙이 「정의되지 않음」으로 답한다. */\n const i = energyIntensity({\n kWh: e.kWh,\n energyWindow: e.window,\n denominator: { kind: 'output', value: completedOrders, window: e.window }\n })\n return {\n energy: {\n ...(e.kWh !== undefined ? { kWh: e.kWh } : {}),\n /* 이 창의 최대수요 — 계약 대비 판정이 보는 수다(전력량과 같은 구간 집합에서 왔다). */\n ...(e.peakKW !== undefined ? { peakKW: e.peakKW, peakStartMs: e.peakStartMs } : {}),\n /* 깎인 양은 **같은 구간의 두 수**의 차이다 — 여기서 한 번만 계산해 두 곳이 갈라지지 않게. */\n ...(e.peakGrossKW !== undefined && e.peakKW !== undefined\n ? { peakGrossKW: e.peakGrossKW, ...(e.peakGrossKW > e.peakKW ? { shavedKW: e.peakGrossKW - e.peakKW } : {}) }\n : {}),\n basis: e.basis,\n counted: e.counted,\n skipped: e.skipped,\n intensity:\n i.value === null\n ? { value: null, reason: (i as any).reason }\n : { value: i.value, unit: (i as any).unit },\n ...(measuredFeeders(demandWindows, { startMs: window.fromMs, endMs: window.toMs }, e.kWh, options.meterFeeder) ??\n feederShares(e.kWh, utilization, options.resourceFeeder)),\n /* 금액은 같은 창의 전력량·최대수요에서 나온다 — 창이 하나뿐이라 어긋날 수 없다. */\n ...(options.tariff ? { cost: electricityCost({ kWh: e.kWh, peakKW: e.peakKW, tariff: options.tariff }) } : {})\n }\n }\n })()\n : {}),\n leadTime: stats(leads),\n workTime: stats(works),\n waitTime: stats(waits),\n utilization,\n unpaired: { leadTime: unpairedLead, workTime: unpairedWork },\n /* 못 읽은 오더 상태 낱말 — 비면 빈 객체다(§`orderStatusNotCounted`). */\n orderStatusNotCounted: Object.fromEntries(orderStatusNotCounted),\n eventsSeen: seen,\n eventsInWindow: inWindow,\n ...(options.groupBy ? { groups: groupRecords(records, options) } : {})\n }\n}\n\n/**\n * 관점 × 비교 교차 — **\"구역별로 어제 대비 어떤가\"**.\n *\n * 축(무엇을)과 비교(언제 대비)를 따로 보면 사용자가 머릿속에서 두 표를 대조해야 한다. 운영에서 가장\n * 값 있는 질문은 그 교차다: 전체는 비슷한데 **한 구역만 나빠진** 경우를 전체 숫자로는 절대 못 본다.\n *\n * 정직성 규율:\n * · 두 구간에 **모두 있는** 축만 delta 를 낸다.\n * · 지금만 있는 축은 `isNew`(이전에 없던 일) — delta 를 건수와 같게 적으면 \"늘었다\" 로 오독된다.\n * · **사라진 축**(전에 있었고 지금 없는 것)은 따로 돌려준다. 표에서 그냥 빠지면 **멈춘 구역이\n * 눈에 보이지 않는다** — 운영에서는 이것이 가장 중요한 신호일 수 있다.\n */\nexport interface CrossedGroup extends KpiGroup {\n /** 비교 구간의 건수(두 구간에 모두 있을 때만). */\n prevTasks?: number\n /** 현재 − 비교구간 건수(두 구간에 모두 있을 때만). */\n deltaTasks?: number\n /** 비교 구간 대비 작업시간 중앙값 차이(둘 다 측정된 때만). */\n deltaWorkP50Ms?: number\n /** 비교 구간에는 없던 축 — delta 대신 이 표식을 준다. */\n isNew?: boolean\n}\n\nexport function crossGroups(\n now: KpiGroup[],\n prev: KpiGroup[]\n): { items: CrossedGroup[]; disappeared: { key: string; tasks: number }[] } {\n const before = new Map(prev.map(g => [g.key, g]))\n const items: CrossedGroup[] = now.map(g => {\n const p = before.get(g.key)\n if (!p) return { ...g, isNew: true }\n return {\n ...g,\n prevTasks: p.tasks,\n deltaTasks: g.tasks - p.tasks,\n ...(deltaP50(g.workTime, p.workTime) !== undefined ? { deltaWorkP50Ms: deltaP50(g.workTime, p.workTime) } : {})\n }\n })\n const present = new Set(now.map(g => g.key))\n const disappeared = prev.filter(g => !present.has(g.key)).map(g => ({ key: g.key, tasks: g.tasks }))\n return { items, disappeared }\n}\n\n/* ── 시간 비교 ──────────────────────────────────────────────────────────────\n * 숫자 하나로는 좋아졌는지 나빠졌는지 알 수 없다(\"29초\" 가 개선인지 악화인지). 같은 길이의 창을 옮겨\n * 한 번 더 접으면 되므로(폴드는 순수) 값싸다. 창 옮기기와 차이 계산은 계산 규칙이라 여기 있다 —\n * 조회 층에 두면 테스트가 DB 를 끌고 와야 한다.\n */\n\n/**\n * 비교 구간의 창 — 같은 **길이**로 옮긴다.\n *\n * previous: 바로 앞 구간(경계가 맞물리게 — 겹치거나 벌어지면 구간의 합이 어긋난다).\n * yesterday: 정확히 24시간 전 같은 자리. 시뮬 트윈의 클록이 실제 시각과 달라도 **차분은 정확**하다.\n */\nexport function shiftWindow(fromMs: number, toMs: number, basis: 'previous' | 'yesterday'): KpiWindow {\n const span = toMs - fromMs\n const back = basis === 'yesterday' ? 24 * 60 * 60_000 : span\n return { fromMs: fromMs - back, toMs: toMs - back }\n}\n\n/**\n * 중앙값 차이(현재 − 비교구간) — **둘 다 측정된 것만** 낸다.\n *\n * 한쪽이 결측이면 차이는 **없는 것**이다(0 이 아니다). 0 으로 내면 \"변화 없음\" 이라는 거짓이 된다.\n * 좋고 나쁨은 판단하지 않는다 — 처리량은 클수록, 소요는 작을수록 좋다는 해석은 소비처의 몫이다.\n */\nexport function deltaP50(now: DurationStats, prev: DurationStats): number | undefined {\n return now.count > 0 && prev.count > 0 ? now.p50Ms - prev.p50Ms : undefined\n}\n\n/** 구간 하나의 최소 요약 — 추세를 그릴 정보만(전체 KPI 를 구간마다 실어 보내지 않는다). */\nexport interface KpiBucket {\n fromMs: number\n toMs: number\n tasks: number\n orders: number\n /** 그 구간의 작업시간 중앙값 — 추세에서 \"느려지고 있는가\" 를 보는 값. */\n workP50Ms: number\n}\n\n/**\n * 창을 N 등분해 구간별로 접는다 — **같은 이벤트를 창만 바꿔** 다시 접는다(조회 추가 없음).\n *\n * 마지막 구간은 나머지를 흡수한다(부동소수 나눗셈으로 끝이 밀리면 마지막 완료가 빠진다).\n * `count <= 1` 이면 쪼갤 것이 없으므로 빈 배열 — 호출부가 \"추세 없음\" 으로 다룬다.\n */\nexport function foldKpiBuckets(events: KpiEvent[], window: KpiWindow, count: number): KpiBucket[] {\n const n = Math.max(0, Math.min(48, Math.floor(count)))\n if (n <= 1) return []\n const step = (window.toMs - window.fromMs) / n\n const out: KpiBucket[] = []\n for (let i = 0; i < n; i++) {\n const fromMs = window.fromMs + step * i\n const toMs = i === n - 1 ? window.toMs : window.fromMs + step * (i + 1)\n const b = foldKpi(events, { fromMs, toMs })\n out.push({ fromMs, toMs, tasks: b.throughput.tasks, orders: b.throughput.orders, workP50Ms: b.workTime.p50Ms })\n }\n return out\n}\n"]}
|
|
@@ -18,6 +18,7 @@ exports.computeTwinKpi = computeTwinKpi;
|
|
|
18
18
|
* 상수를 앱에서 재현하지 않고(무방언), 라이브 트윈에서도 자연스럽다(마지막 기록 ≈ 지금).
|
|
19
19
|
* 어떤 기준을 썼는지는 결과의 `window.basis` 로 밝힌다 — 숨기면 사용자가 숫자를 오해한다.
|
|
20
20
|
*/
|
|
21
|
+
const log_js_1 = require("./log.js");
|
|
21
22
|
const typeorm_1 = require("typeorm");
|
|
22
23
|
const twin_kernel_1 = require("@operato/twin-kernel");
|
|
23
24
|
const shell_1 = require("@things-factory/shell");
|
|
@@ -113,7 +114,7 @@ async function locationAreaMap(domainId, instanceIds, spaceId) {
|
|
|
113
114
|
const locations = (row.model?.locations ?? []).filter(n => n?.id);
|
|
114
115
|
if (!locations.length)
|
|
115
116
|
continue;
|
|
116
|
-
/* 순환은 hierarchyOf 가
|
|
117
|
+
/* 순환은 hierarchyOf 가 오류를 낸다 — 한 트윈의 잘못된 계층이 다른 트윈의 집계까지 죽이지 않게 여기서 가둔다. */
|
|
117
118
|
let h;
|
|
118
119
|
try {
|
|
119
120
|
/*
|
|
@@ -124,7 +125,7 @@ async function locationAreaMap(domainId, instanceIds, spaceId) {
|
|
|
124
125
|
h = (0, twin_kernel_1.hierarchyOf)({ locations }, t => (0, twin_kernel_1.levelOfLocationType)(t, row.kind));
|
|
125
126
|
}
|
|
126
127
|
catch (e) {
|
|
127
|
-
|
|
128
|
+
(0, log_js_1.twinWarn)(`[twin-kpi] "${row.instanceId}": location hierarchy is broken — area rollup skipped for this twin: ${e?.message}`);
|
|
128
129
|
continue;
|
|
129
130
|
}
|
|
130
131
|
for (const n of locations) {
|
|
@@ -313,24 +314,114 @@ async function latestEventTime(domainId, instanceIds) {
|
|
|
313
314
|
async function fetchEvents(domainId, instanceIds, fromMs, toMs, lookbackMs) {
|
|
314
315
|
if (instanceIds.length === 0)
|
|
315
316
|
return { events: [], capped: false };
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
317
|
+
/*
|
|
318
|
+
* ── 필요한 세 컬럼만, 그리고 **읽기 연결로** (2026-08-22 실측) ─────────────
|
|
319
|
+
* 이 읽기가 서버를 붙잡고 있었다. 실측(개발 DB 16.3GB · 저널 1,134만 행): 2만 행을 실어 오는 동안
|
|
320
|
+
* 다른 가벼운 읽기가 **47.6초** 기다렸다. 로그에서 이 자리 뒤로 사용자·도메인 조회까지 30.9초로
|
|
321
|
+
* 찍혔다 — TypeORM sqlite 드라이버는 연결을 하나만 들기 때문이다.
|
|
322
|
+
*
|
|
323
|
+
* **행 단위로 읽는 것 자체는 정당하다**: 이 폴드는 `p10·p50·p90` 을 낸다(§`kpi-fold`). 분위수는
|
|
324
|
+
* 카운터로 만들 수 없고 트윈별 사전집계로도 만들 수 없다(p90 의 평균은 p90 이 아니다). 개별 값이
|
|
325
|
+
* 있어야 한다.
|
|
326
|
+
*
|
|
327
|
+
* 정당하지 않은 것은 **읽는 방식**이었다. 세 가지를 고친다.
|
|
328
|
+
*
|
|
329
|
+
* ① **엔티티를 만들지 않는다.** 폴드가 쓰는 것은 `eventType`·`eventTime`·`payload` 뿐인데
|
|
330
|
+
* `find()` 는 열여섯 컬럼을 실어 엔티티로 만들고 관계 로더를 지난다. CPU 프로파일에서
|
|
331
|
+
* `RawSqlResultsToEntityTransformer` 와 `stringToSimpleJson` 이 상위였다.
|
|
332
|
+
* ② **읽기 연결로 보낸다**(`getReadRepository`). 순수 읽기이므로 쓰기와 순서를 다툴 것이 없다.
|
|
333
|
+
* 실측: 같은 2만 행 읽기에서 다른 읽기의 대기가 47.6초 → 2.8초.
|
|
334
|
+
* ③ **정렬 축을 거르는 축과 맞춘다.** 여기가 이 자리의 진짜 비용이었다.
|
|
335
|
+
*
|
|
336
|
+
* ── `ORDER BY revision` 이 `LIMIT` 을 무력화했다 (실측) ────────────────────
|
|
337
|
+
* 거르는 축은 `event_time`(창)이고 정렬 축은 `revision` 이었다. 그러면 sqlite 는 창에 맞는 행을
|
|
338
|
+
* **전부** 찾아 `revision` 으로 **전부 정렬한 뒤** 상한을 적용한다 — 상한이 정렬 뒤에 걸리므로
|
|
339
|
+
* 아무것도 줄여 주지 않는다. 실측이 그것을 그대로 보였다(인스턴스 하나가 375만 행):
|
|
340
|
+
*
|
|
341
|
+
* ORDER BY revision LIMIT 20000 → 32.2초 · LIMIT 5000 → 28.3초 · LIMIT 2000 → 28.2초
|
|
342
|
+
* ORDER BY eventTime, revision LIMIT 20000 → 0.41초 (78배)
|
|
343
|
+
*
|
|
344
|
+
* 계획도 그렇게 말한다: 앞은 `USE TEMP B-TREE FOR ORDER BY`(전체 정렬), 뒤는
|
|
345
|
+
* `USE TEMP B-TREE FOR LAST TERM OF ORDER BY` — 시각은 인덱스(`ix_twin_event_1`)에서 순서대로
|
|
346
|
+
* 나오고 **동시각만** 임시 정렬한다. 즉 범위를 훑다가 상한에서 멈춘다.
|
|
347
|
+
*
|
|
348
|
+
* ── 순서의 뜻은 **완전히 같지는 않다** (측정해서 고친 문장) ────────────────
|
|
349
|
+
* 처음에 「순서의 뜻은 같다」고 적었다. 재 보니 아니었다 — `revision` 순서와 `eventTime` 순서가
|
|
350
|
+
* 어긋나는 자리가 실재한다(hatio-mx2: 426만 행 중 **7건** · rosarito-mes: 11만 중 8건). 미러는
|
|
351
|
+
* 원본의 시각을 싣고 도착 순서로 리비전을 받으므로, 늦게 도착한 사건이 그 어긋남을 만든다.
|
|
352
|
+
*
|
|
353
|
+
* **그러나 폴드의 수치는 같다.** `foldTaskRecords`(커널 `task-fold.ts`)가 스트림 위치가 아니라
|
|
354
|
+
* **시각으로** 판정한다:
|
|
355
|
+
*
|
|
356
|
+
* startedMs = Math.min(startedMs, at)
|
|
357
|
+
* completedMs = Math.max(completedMs, at)
|
|
358
|
+
*
|
|
359
|
+
* `min`·`max` 이므로 행이 어떤 순서로 와도 답이 같다. 소요시간·분위수·처리량은 순서와 무관하다.
|
|
360
|
+
*
|
|
361
|
+
* 순서에 민감한 자리는 **하나**다: 그 폴드의 `location` 이 「마지막에 본 것」이다. 같은 밀리초에 같은
|
|
362
|
+
* 작업의 자리가 두 번 바뀌면 어느 것이 남는지가 순서로 갈린다(라이브 미러가 같은 밀리초에 여러 행을
|
|
363
|
+
* 보내는 경우다). 수치가 아니라 **표시되는 자리 하나**이고, 그래서 이 바꿈의 값이 그 위험보다 크다.
|
|
364
|
+
*
|
|
365
|
+
* `revision` 을 둘째 축으로 남기는 이유도 그것이다 — 동시각의 순서가 실행마다 달라지지 않게 한다.
|
|
366
|
+
*
|
|
367
|
+
* **상한을 낮추는 것으로는 고쳐지지 않는다**(위 숫자가 그것을 말한다). 정렬 축이 원인이다.
|
|
368
|
+
*
|
|
369
|
+
* 남는 비용을 감추지 않는다: `payload` 파싱은 여전히 메인 스레드 일이고 연결을 나눠도 나뉘지 않는다
|
|
370
|
+
* (실측 평균 1.5초). 그것을 더 줄이려면 폴드가 payload 에서 무엇을 쓰는지 좁혀야 한다 — 별 건이다.
|
|
371
|
+
*/
|
|
372
|
+
const raw = await (0, shell_1.getReadRepository)(twin_event_js_1.TwinEvent)
|
|
373
|
+
.createQueryBuilder('e')
|
|
374
|
+
.select(['e.eventType AS "eventType"', 'e.eventTime AS "eventTime"', 'e.payload AS "payload"'])
|
|
375
|
+
.where('e.domain = :domainId', { domainId })
|
|
376
|
+
.andWhere('e.instanceId IN (:...instanceIds)', { instanceIds })
|
|
377
|
+
.andWhere('e.eventType IN (:...types)', { types: BUSINESS_EVENTS })
|
|
378
|
+
.andWhere('e.eventTime BETWEEN :from AND :to', { from: new Date(fromMs - lookbackMs), to: new Date(toMs) })
|
|
379
|
+
.orderBy('e.eventTime', 'ASC')
|
|
380
|
+
.addOrderBy('e.revision', 'ASC')
|
|
381
|
+
.limit(MAX_EVENTS)
|
|
382
|
+
.getRawMany();
|
|
326
383
|
/* 상한에 정확히 닿으면 **더 있었을 수 있다** — 그 사실을 돌려준다. 조용히 잘라내면 숫자가 조용히
|
|
327
384
|
* 작아지고, 사용자는 그것을 성과 하락으로 읽는다(가장 나쁜 종류의 거짓이다). */
|
|
328
385
|
return {
|
|
329
386
|
/* 폴드는 ISO 문자열 축으로 수행된다(kpi-fold) — 경계에서 한 번만 옮긴다. */
|
|
330
|
-
events:
|
|
331
|
-
capped:
|
|
387
|
+
events: raw.map(r => ({ eventType: r.eventType, eventTime: isoOf(r.eventTime), payload: jsonOf(r.payload) })),
|
|
388
|
+
capped: raw.length >= MAX_EVENTS
|
|
332
389
|
};
|
|
333
390
|
}
|
|
391
|
+
/**
|
|
392
|
+
* 원시 결과의 시각을 ISO 로 — **드라이버마다 모양이 다르다.**
|
|
393
|
+
*
|
|
394
|
+
* 엔티티 경로에서는 TypeORM 이 `Date` 로 바꿔 주지만, 원시 결과는 드라이버가 준 그대로다(sqlite 는
|
|
395
|
+
* `'2026-08-07 11:00:00.000'`, pg 는 `Date`). 경계에서 한 번 정규화한다 — 폴드는 ISO 문자열만 안다.
|
|
396
|
+
*/
|
|
397
|
+
function isoOf(v) {
|
|
398
|
+
if (v == null)
|
|
399
|
+
return undefined;
|
|
400
|
+
if (v instanceof Date)
|
|
401
|
+
return v.toISOString();
|
|
402
|
+
const s = String(v);
|
|
403
|
+
/* sqlite 의 공백 구분 형식을 ISO 로 — 그대로 `Date` 에 주면 시간대 해석이 갈린다. */
|
|
404
|
+
const d = new Date(s.includes('T') ? s : s.replace(' ', 'T') + (s.endsWith('Z') ? '' : 'Z'));
|
|
405
|
+
return Number.isNaN(d.getTime()) ? undefined : d.toISOString();
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* 원시 결과의 `payload` 를 객체로 — **엔티티 경로의 `simple-json` 변환을 여기서 한다.**
|
|
409
|
+
*
|
|
410
|
+
* 깨진 JSON 은 **버리지 않고 비운다**: 그 사건이 있었다는 사실은 남고, 안을 읽을 수 없다는 것만
|
|
411
|
+
* 폴드에 전해진다(폴드는 없는 필드를 건너뛴다). 던지면 창 하나가 통째로 사라진다.
|
|
412
|
+
*/
|
|
413
|
+
function jsonOf(v) {
|
|
414
|
+
if (v == null)
|
|
415
|
+
return undefined;
|
|
416
|
+
if (typeof v !== 'string')
|
|
417
|
+
return v;
|
|
418
|
+
try {
|
|
419
|
+
return JSON.parse(v);
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
return undefined;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
334
425
|
/**
|
|
335
426
|
* 집계 대상 트윈 목록.
|
|
336
427
|
*
|
|
@@ -418,6 +509,29 @@ async function computeTwinKpi(input) {
|
|
|
418
509
|
...shiftCtx,
|
|
419
510
|
groupLimit: input.groupLimit
|
|
420
511
|
});
|
|
512
|
+
/*
|
|
513
|
+
* **완료로 세지 않은 오더 상태 낱말을 말한다** (2026-08-23).
|
|
514
|
+
*
|
|
515
|
+
* 폴드는 순수하다(로그를 모른다) — 그래서 부르는 쪽이 말한다. 왜 말해야 하나: 이 폴드는 오더의
|
|
516
|
+
* 완료를 **동의어 목록**으로 판정하고, 그것은 원본의 낱말에서 뜻을 추론하는 것이다. 커넥터가
|
|
517
|
+
* 자기 표로 옮기지 않으면 그 낱말이 목록에 없어 **완료가 조용히 0 으로 세어진다.**
|
|
518
|
+
*
|
|
519
|
+
* 실제로 그랬다: 첫 실 원본의 `FINISHED` 가 목록에 없어 완료 오더가 전부 0 이었고, 사람이 화면을
|
|
520
|
+
* 보고 의심해서야 드러났다(그 커넥터에는 상태 표가 정의돼 **있으면서** 오더 경로에서 쓰이지 않았다).
|
|
521
|
+
* 그때 목록에 낱말을 더한 것은 그 사실을 가린 땜빵이었다.
|
|
522
|
+
*
|
|
523
|
+
* 그러니 이 로그는 「목록을 늘려라」가 아니라 **「그 커넥터가 옮기지 않았다」**를 뜻한다. 화면을 보지
|
|
524
|
+
* 않는 사람도 알아야 하므로 로그에 남긴다 — 조용한 0 이 이 시스템에서 가장 비싼 침묵이다.
|
|
525
|
+
*
|
|
526
|
+
* 완료가 하나도 없을 때만 말한다: `picking` 처럼 완료가 아닌 낱말이 섞여 있는 것은 정상이고,
|
|
527
|
+
* 그것까지 매번 경고하면 진짜 신호가 묻힌다.
|
|
528
|
+
*/
|
|
529
|
+
const notCounted = Object.entries(kpi.orderStatusNotCounted ?? {});
|
|
530
|
+
if (notCounted.length && kpi.throughput.orders === 0) {
|
|
531
|
+
(0, log_js_1.twinWarn)(`[twin-kpi] no order counted as completed in this window, but these order status words were seen: ` +
|
|
532
|
+
`${notCounted.map(([w, n]) => `${w}×${n}`).join(' · ')} — the connector may not be translating them ` +
|
|
533
|
+
`to the kernel's vocabulary (see its own status table).`);
|
|
534
|
+
}
|
|
421
535
|
/*
|
|
422
536
|
* 목표를 **폴드 뒤, 그룹 조립 앞**에 읽는다 — 전체 판정과 축별 판정이 같은 목록에서 나와야
|
|
423
537
|
* 두 곳의 임계가 갈라지지 않는다. 폴드 자체는 목표를 모른다(사실 층과 판정 층의 분리).
|