@things-factory/headless-twin 10.0.9 → 10.0.10
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 +2 -2
- package/dist-server/engine/canonical-ingest.js +24 -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/energy-topology.d.ts +86 -0
- package/dist-server/engine/energy-topology.js +144 -0
- package/dist-server/engine/energy-topology.js.map +1 -0
- package/dist-server/engine/index.d.ts +8 -3
- package/dist-server/engine/index.js +10 -3
- package/dist-server/engine/index.js.map +1 -1
- package/dist-server/engine/kpi-baseline.d.ts +1 -1
- package/dist-server/engine/kpi-baseline.js.map +1 -1
- package/dist-server/engine/kpi-fold.d.ts +137 -1
- package/dist-server/engine/kpi-fold.js +225 -0
- package/dist-server/engine/kpi-fold.js.map +1 -1
- package/dist-server/engine/kpi-query.d.ts +1 -1
- package/dist-server/engine/kpi-query.js +212 -18
- package/dist-server/engine/kpi-query.js.map +1 -1
- package/dist-server/engine/kpi-target.d.ts +17 -0
- package/dist-server/engine/kpi-target.js +24 -2
- package/dist-server/engine/kpi-target.js.map +1 -1
- package/dist-server/engine/live-feed-registry.d.ts +25 -0
- package/dist-server/engine/live-feed-registry.js +51 -0
- package/dist-server/engine/live-feed-registry.js.map +1 -0
- package/dist-server/engine/load-meter.d.ts +181 -0
- package/dist-server/engine/load-meter.js +267 -0
- package/dist-server/engine/load-meter.js.map +1 -0
- package/dist-server/engine/local-declarations.d.ts +262 -0
- package/dist-server/engine/local-declarations.js +528 -0
- package/dist-server/engine/local-declarations.js.map +1 -0
- package/dist-server/engine/model-basis.d.ts +39 -6
- package/dist-server/engine/model-basis.js +69 -9
- package/dist-server/engine/model-basis.js.map +1 -1
- package/dist-server/engine/{board-vocabulary.d.ts → model-vocabulary.d.ts} +4 -4
- package/dist-server/engine/{board-vocabulary.js → model-vocabulary.js} +37 -19
- package/dist-server/engine/model-vocabulary.js.map +1 -0
- package/dist-server/engine/oee-accumulator.d.ts +28 -0
- package/dist-server/engine/oee-accumulator.js +26 -1
- package/dist-server/engine/oee-accumulator.js.map +1 -1
- package/dist-server/engine/operation-basis.d.ts +32 -0
- package/dist-server/engine/operation-basis.js +87 -0
- package/dist-server/engine/operation-basis.js.map +1 -0
- package/dist-server/engine/property-effects.d.ts +30 -0
- package/dist-server/engine/property-effects.js +192 -0
- package/dist-server/engine/property-effects.js.map +1 -0
- package/dist-server/engine/runtime-key.d.ts +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/spec-coverage.js +3 -3
- package/dist-server/engine/spec-coverage.js.map +1 -1
- package/dist-server/engine/state-axes.d.ts +7 -2
- package/dist-server/engine/state-axes.js +8 -3
- package/dist-server/engine/state-axes.js.map +1 -1
- package/dist-server/engine/structure-diff.js +1 -1
- package/dist-server/engine/structure-diff.js.map +1 -1
- package/dist-server/engine/travel-estimator.d.ts +9 -2
- package/dist-server/engine/travel-estimator.js +11 -6
- package/dist-server/engine/travel-estimator.js.map +1 -1
- package/dist-server/engine/twin-engine.d.ts +332 -47
- package/dist-server/engine/twin-engine.js +1314 -279
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/engine/warm-start.d.ts +38 -4
- package/dist-server/engine/warm-start.js +38 -9
- package/dist-server/engine/warm-start.js.map +1 -1
- package/dist-server/index.d.ts +1 -0
- package/dist-server/index.js +17 -11
- package/dist-server/index.js.map +1 -1
- package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.d.ts +5 -0
- package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js +48 -0
- package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js.map +1 -0
- package/dist-server/migrations/index.d.ts +2 -0
- package/dist-server/migrations/index.js +10 -0
- package/dist-server/migrations/index.js.map +1 -0
- package/dist-server/service/index.d.ts +3 -2
- package/dist-server/service/index.js +25 -17
- 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 +2 -2
- package/dist-server/service/reference/discovery-result.js.map +1 -1
- package/dist-server/service/reference/ingest-space.d.ts +24 -1
- package/dist-server/service/reference/ingest-space.js +17 -1
- package/dist-server/service/reference/ingest-space.js.map +1 -1
- package/dist-server/service/reference/reference-live.d.ts +23 -0
- package/dist-server/service/reference/reference-live.js +121 -4
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-master.d.ts +194 -13
- package/dist-server/service/reference/reference-master.js +159 -28
- package/dist-server/service/reference/reference-master.js.map +1 -1
- package/dist-server/service/reference/reference-resolver.d.ts +31 -1
- package/dist-server/service/reference/reference-resolver.js +223 -12
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/reference/template-registry.d.ts +19 -3
- package/dist-server/service/reference/template-registry.js.map +1 -1
- package/dist-server/service/twin-audit/command-audit.d.ts +37 -0
- package/dist-server/service/twin-audit/command-audit.js +53 -0
- package/dist-server/service/twin-audit/command-audit.js.map +1 -0
- package/dist-server/service/twin-audit/index.d.ts +4 -0
- package/dist-server/service/twin-audit/index.js +8 -0
- package/dist-server/service/twin-audit/index.js.map +1 -0
- package/dist-server/service/twin-audit/twin-audit-event.d.ts +24 -0
- package/dist-server/service/twin-audit/twin-audit-event.js +125 -0
- package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -0
- package/dist-server/service/twin-audit/twin-audit-query.d.ts +4 -0
- package/dist-server/service/twin-audit/twin-audit-query.js +76 -0
- package/dist-server/service/twin-audit/twin-audit-query.js.map +1 -0
- package/dist-server/service/twin-control/twin-control-mutation.js +21 -1
- package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
- package/dist-server/service/twin-event/twin-event-keys.d.ts +1 -1
- package/dist-server/service/twin-event/twin-event-keys.js +1 -1
- package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
- package/dist-server/service/twin-event/twin-event.d.ts +14 -5
- package/dist-server/service/twin-event/twin-event.js +46 -14
- package/dist-server/service/twin-event/twin-event.js.map +1 -1
- package/dist-server/service/twin-forecast/forecast-metrics.d.ts +13 -0
- package/dist-server/service/twin-forecast/forecast-metrics.js +57 -0
- package/dist-server/service/twin-forecast/forecast-metrics.js.map +1 -0
- package/dist-server/service/twin-forecast/forecast-tuning.d.ts +12 -0
- package/dist-server/service/twin-forecast/forecast-tuning.js +44 -0
- package/dist-server/service/twin-forecast/forecast-tuning.js.map +1 -0
- package/dist-server/service/twin-forecast/gap-analytics.d.ts +28 -2
- package/dist-server/service/twin-forecast/gap-analytics.js +43 -17
- package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
- package/dist-server/service/twin-forecast/twin-forecast-query.js +217 -38
- package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
- package/dist-server/service/twin-instance/twin-instance.d.ts +38 -5
- package/dist-server/service/twin-instance/twin-instance.js +66 -20
- package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
- package/dist-server/service/twin-journal/twin-journal-query.d.ts +5 -2
- package/dist-server/service/twin-journal/twin-journal-query.js +71 -9
- package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +3 -5
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +24 -27
- 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 +62 -0
- package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
- package/dist-server/service/twin-model/epcis-coverage.d.ts +14 -0
- package/dist-server/service/twin-model/epcis-coverage.js +174 -0
- package/dist-server/service/twin-model/epcis-coverage.js.map +1 -0
- package/dist-server/service/twin-model/iec61850-coverage.d.ts +14 -0
- package/dist-server/service/twin-model/iec61850-coverage.js +98 -0
- package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -0
- package/dist-server/service/twin-model/index.d.ts +13 -0
- package/dist-server/service/twin-model/index.js +29 -0
- package/dist-server/service/twin-model/index.js.map +1 -0
- package/dist-server/service/twin-model/isa95-coverage.d.ts +48 -0
- package/dist-server/service/twin-model/isa95-coverage.js +155 -0
- package/dist-server/service/twin-model/isa95-coverage.js.map +1 -0
- package/dist-server/service/twin-model/project-structure.d.ts +16 -0
- package/dist-server/service/twin-model/project-structure.js +173 -0
- package/dist-server/service/twin-model/project-structure.js.map +1 -0
- package/dist-server/service/twin-model/standard-coverage.d.ts +9 -0
- package/dist-server/service/twin-model/standard-coverage.js +37 -0
- package/dist-server/service/twin-model/standard-coverage.js.map +1 -0
- package/dist-server/service/twin-model/twin-equipment.d.ts +64 -0
- package/dist-server/service/twin-model/twin-equipment.js +134 -0
- package/dist-server/service/twin-model/twin-equipment.js.map +1 -0
- package/dist-server/service/twin-model/twin-lineage-query.d.ts +3 -0
- package/dist-server/service/twin-model/twin-lineage-query.js +206 -0
- package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -0
- package/dist-server/service/twin-model/twin-location.d.ts +58 -0
- package/dist-server/service/twin-model/twin-location.js +130 -0
- package/dist-server/service/twin-model/twin-location.js.map +1 -0
- package/dist-server/service/twin-model/twin-model-item-query.d.ts +141 -0
- package/dist-server/service/twin-model/twin-model-item-query.js +852 -0
- package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -0
- package/dist-server/service/twin-model/twin-model-mutation.d.ts +57 -0
- package/dist-server/service/twin-model/twin-model-mutation.js +490 -0
- package/dist-server/service/twin-model/twin-model-mutation.js.map +1 -0
- package/dist-server/service/twin-model/twin-model-query.d.ts +43 -0
- package/dist-server/service/twin-model/twin-model-query.js +551 -0
- package/dist-server/service/twin-model/twin-model-query.js.map +1 -0
- package/dist-server/service/twin-model/twin-model-tree-query.d.ts +3 -0
- package/dist-server/service/twin-model/twin-model-tree-query.js +158 -0
- package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -0
- package/dist-server/service/twin-model/twin-operation.d.ts +61 -0
- package/dist-server/service/twin-model/twin-operation.js +137 -0
- package/dist-server/service/twin-model/twin-operation.js.map +1 -0
- package/dist-server/service/twin-space/move-space.d.ts +65 -0
- package/dist-server/service/twin-space/move-space.js +166 -0
- package/dist-server/service/twin-space/move-space.js.map +1 -0
- package/dist-server/service/twin-space/space-integrity.d.ts +97 -0
- package/dist-server/service/twin-space/space-integrity.js +182 -0
- package/dist-server/service/twin-space/space-integrity.js.map +1 -0
- package/dist-server/service/twin-space/twin-space-area.js +4 -4
- package/dist-server/service/twin-space/twin-space-area.js.map +1 -1
- package/dist-server/service/twin-space/twin-space-representation.js +6 -6
- package/dist-server/service/twin-space/twin-space-representation.js.map +1 -1
- package/dist-server/service/twin-space/twin-space-resolver.d.ts +63 -1
- package/dist-server/service/twin-space/twin-space-resolver.js +356 -18
- package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
- package/dist-server/service/twin-space/twin-space.d.ts +17 -1
- package/dist-server/service/twin-space/twin-space.js +15 -5
- package/dist-server/service/twin-space/twin-space.js.map +1 -1
- package/dist-server/service/twin-structure/twin-structure.d.ts +1 -1
- package/dist-server/service/twin-structure/twin-structure.js +8 -8
- package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
- package/dist-shared/axis-read.d.ts +12 -0
- package/dist-shared/axis-read.js +32 -0
- package/dist-shared/axis-read.js.map +1 -0
- package/{dist-server/engine → dist-shared}/entity-delta.d.ts +3 -0
- package/{dist-server/engine → dist-shared}/entity-delta.js +102 -2
- package/dist-shared/entity-delta.js.map +1 -0
- package/dist-shared/kpi-broadcast.js.map +1 -0
- package/dist-shared/twin-level.js.map +1 -0
- package/package.json +14 -12
- package/server/engine/canonical-ingest.ts +36 -4
- package/server/engine/command-routing.ts +1 -1
- package/server/engine/energy-topology.ts +189 -0
- package/server/engine/index.ts +10 -3
- package/server/engine/kpi-baseline.ts +1 -1
- package/server/engine/kpi-fold.ts +389 -1
- package/server/engine/kpi-query.ts +214 -19
- package/server/engine/kpi-target.ts +24 -2
- package/server/engine/live-feed-registry.ts +58 -0
- package/server/engine/load-meter.ts +384 -0
- package/server/engine/local-declarations.ts +700 -0
- package/server/engine/model-basis.ts +78 -10
- package/server/engine/{board-vocabulary.ts → model-vocabulary.ts} +36 -15
- package/server/engine/oee-accumulator.ts +34 -1
- package/server/engine/operation-basis.ts +100 -0
- package/server/engine/property-effects.ts +199 -0
- package/server/engine/runtime-key.ts +1 -1
- package/server/engine/spec-coverage.ts +3 -3
- package/server/engine/state-axes.ts +8 -3
- package/server/engine/structure-diff.ts +1 -1
- package/server/engine/travel-estimator.ts +11 -6
- package/server/engine/twin-engine.ts +1414 -274
- package/server/engine/warm-start.ts +83 -12
- package/server/index.ts +13 -10
- package/server/migrations/1786000000000-RenameTwinInstanceBoardToModel.ts +43 -0
- package/server/migrations/index.ts +7 -0
- package/server/service/index.ts +9 -1
- package/server/service/reference/discovery-result.ts +2 -2
- package/server/service/reference/ingest-space.ts +35 -1
- package/server/service/reference/reference-live.ts +119 -4
- package/server/service/reference/reference-master.ts +276 -35
- package/server/service/reference/reference-resolver.ts +232 -12
- package/server/service/reference/template-registry.ts +20 -5
- package/server/service/twin-audit/command-audit.ts +81 -0
- package/server/service/twin-audit/index.ts +5 -0
- package/server/service/twin-audit/twin-audit-event.ts +112 -0
- package/server/service/twin-audit/twin-audit-query.ts +72 -0
- package/server/service/twin-control/twin-control-mutation.ts +22 -1
- package/server/service/twin-event/twin-event-keys.ts +1 -1
- package/server/service/twin-event/twin-event.ts +52 -15
- package/server/service/twin-forecast/forecast-metrics.ts +60 -0
- package/server/service/twin-forecast/forecast-tuning.ts +40 -0
- package/server/service/twin-forecast/gap-analytics.ts +48 -9
- package/server/service/twin-forecast/twin-forecast-query.ts +208 -36
- package/server/service/twin-instance/twin-instance.ts +126 -25
- package/server/service/twin-journal/twin-journal-query.ts +74 -10
- package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +23 -22
- package/server/service/twin-metrics/twin-metrics-query.ts +59 -2
- package/server/service/twin-model/epcis-coverage.ts +195 -0
- package/server/service/twin-model/iec61850-coverage.ts +117 -0
- package/server/service/twin-model/index.ts +25 -0
- package/server/service/twin-model/isa95-coverage.ts +190 -0
- package/server/service/twin-model/project-structure.ts +199 -0
- package/server/service/twin-model/standard-coverage.ts +35 -0
- package/server/service/twin-model/twin-equipment.ts +150 -0
- package/server/service/twin-model/twin-lineage-query.ts +193 -0
- package/server/service/twin-model/twin-location.ts +140 -0
- package/server/service/twin-model/twin-model-item-query.ts +848 -0
- package/server/service/twin-model/twin-model-mutation.ts +503 -0
- package/server/service/twin-model/twin-model-query.ts +537 -0
- package/server/service/twin-model/twin-model-tree-query.ts +180 -0
- package/server/service/twin-model/twin-operation.ts +150 -0
- package/server/service/twin-space/move-space.ts +262 -0
- package/server/service/twin-space/space-integrity.ts +293 -0
- package/server/service/twin-space/twin-space-area.ts +4 -4
- package/server/service/twin-space/twin-space-representation.ts +6 -6
- package/server/service/twin-space/twin-space-resolver.ts +355 -21
- package/server/service/twin-space/twin-space.ts +40 -6
- package/server/service/twin-structure/twin-structure.ts +10 -10
- package/shared/axis-read.ts +28 -0
- package/{server/engine → shared}/entity-delta.ts +96 -1
- package/test/adopt-structure-live.test.ts +133 -0
- package/test/attention-digest.test.ts +3 -3
- package/test/axis-read.test.ts +87 -0
- package/test/boot-resume.test.ts +205 -0
- package/test/canonical-ingest-vocabularies.test.ts +118 -0
- package/test/capability-mapping.test.ts +1 -1
- package/test/command-routing.test.ts +1 -1
- package/test/declaration-reaches-model.test.ts +178 -0
- package/test/discovery-result.test.ts +1 -1
- package/test/energy-topology.test.ts +113 -0
- package/test/entity-delta.test.ts +171 -1
- package/test/event-time-column.test.ts +67 -0
- package/test/forecast-metrics.test.ts +71 -0
- package/test/forecast-tuning.test.ts +42 -0
- package/test/gap-analytics.test.ts +34 -7
- package/test/ingest-bench.test.ts +6 -6
- package/test/ingest-running-guard.test.ts +136 -0
- package/test/ingest-space.test.ts +29 -1
- package/test/instance-cache-lifecycle.test.ts +110 -0
- package/test/kernel-kind-guard.test.ts +96 -0
- package/test/kpi-baseline-db.test.ts +3 -1
- package/test/kpi-baseline.test.ts +3 -3
- package/test/kpi-fold.test.ts +393 -2
- package/test/kpi-query-bench.test.ts +5 -3
- package/test/lineage-survives-restart.test.ts +203 -0
- package/test/live-feed-registry.test.ts +67 -0
- package/test/live-kernel-facts.test.ts +161 -0
- package/test/live-mirror-parity.test.ts +22 -15
- package/test/load-meter.test.ts +314 -0
- package/test/local-declarations.test.ts +554 -0
- package/test/master-to-twin.test.ts +121 -33
- package/test/model-basis.test.ts +56 -1
- package/test/{board-vocabulary.test.ts → model-vocabulary.test.ts} +25 -25
- package/test/move-space.test.ts +149 -0
- package/test/mutation-gate.test.ts +19 -5
- package/test/oee-accumulator.test.ts +26 -7
- package/test/operation-basis.test.ts +100 -0
- package/test/operations-capability-db.test.ts +165 -0
- package/test/project-structure-db.test.ts +226 -0
- package/test/projection-reaches-screen.test.ts +272 -0
- package/test/property-effects.test.ts +96 -0
- package/test/resync-origin-site.test.ts +77 -0
- package/test/scale-twin-bench.test.ts +5 -5
- package/test/snapshot-freshness.test.ts +60 -0
- package/test/space-integrity.test.ts +223 -0
- package/test/standard-coverage.test.ts +94 -0
- package/test/state-axes.test.ts +1 -1
- package/test/streamline-e2e.test.ts +14 -14
- package/test/structure-revision-db.test.ts +11 -11
- package/test/tenant-registry-db.test.ts +5 -5
- package/test/twin-audit.test.ts +83 -0
- package/test/twin-model-item-db.test.ts +275 -0
- package/test/twin-model-tree-db.test.ts +176 -0
- package/test/twin-origin-resync.test.ts +114 -0
- package/test/warm-start-seam.test.ts +6 -6
- package/test/warm-start.test.ts +88 -3
- package/tsconfig.json +6 -1
- package/tsconfig.shared.json +23 -0
- package/tsconfig.shared.tsbuildinfo +1 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/dist-server/engine/board-vocabulary.js.map +0 -1
- package/dist-server/engine/entity-delta.js.map +0 -1
- package/dist-server/engine/kpi-broadcast.js.map +0 -1
- package/dist-server/engine/twin-level.js.map +0 -1
- package/dist-server/service/twin-event/backfill-keys.d.ts +0 -11
- package/dist-server/service/twin-event/backfill-keys.js +0 -63
- package/dist-server/service/twin-event/backfill-keys.js.map +0 -1
- package/dist-server/tsconfig.tsbuildinfo +0 -1
- package/server/service/twin-event/backfill-keys.ts +0 -72
- /package/{dist-server/engine → dist-shared}/kpi-broadcast.d.ts +0 -0
- /package/{dist-server/engine → dist-shared}/kpi-broadcast.js +0 -0
- /package/{dist-server/engine → dist-shared}/twin-level.d.ts +0 -0
- /package/{dist-server/engine → dist-shared}/twin-level.js +0 -0
- /package/{server/engine → shared}/kpi-broadcast.ts +0 -0
- /package/{server/engine → shared}/twin-level.ts +0 -0
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { Between, In } from 'typeorm'
|
|
18
18
|
|
|
19
|
-
import { hierarchyOf } from '@operato/twin-kernel'
|
|
19
|
+
import { hierarchyOf, levelOfLocationType, electricalUpstreamOf, EMS_LOCATION_TYPES, EMS_PROPERTY, type TariffDeclaration } from '@operato/twin-kernel'
|
|
20
20
|
|
|
21
21
|
import { getRepository } from '@things-factory/shell'
|
|
22
22
|
|
|
@@ -27,6 +27,7 @@ import { TwinSpace } from '../service/twin-space/twin-space.js'
|
|
|
27
27
|
import { TwinTarget } from '../service/twin-target/twin-target.js'
|
|
28
28
|
import { TWIN_METRIC, judgeTargets, judgeGroupTargets, scopedTargets, type ScopedTargets, type TargetVerdict, type TwinMetricId } from './kpi-target.js'
|
|
29
29
|
import { TwinEngine } from './twin-engine.js'
|
|
30
|
+
import { buildEnergyTopology, type EnergyTopology } from './energy-topology.js'
|
|
30
31
|
import {
|
|
31
32
|
crossGroups,
|
|
32
33
|
deltaP50,
|
|
@@ -40,8 +41,14 @@ import {
|
|
|
40
41
|
} from './kpi-fold.js'
|
|
41
42
|
import { MAX_BASELINE_PERIODS, baselineWindows, summarizeBaseline, type BaselineStride, type MetricBaseline } from './kpi-baseline.js'
|
|
42
43
|
|
|
43
|
-
/**
|
|
44
|
-
|
|
44
|
+
/**
|
|
45
|
+
* 업무 전이만 읽는다 — 설비 상태(equipment.status)는 OEE 누적기의 몫이고 여기선 쓰지 않는다.
|
|
46
|
+
*
|
|
47
|
+
* **에너지 구간 마감**은 함께 읽는다(2026-08-14): 원단위는 분자(전력량)와 분모(산출)를 **같은 창**에서
|
|
48
|
+
* 나눈 값이라, 두 사실이 한 폴드에 들어와야 창이 어긋날 수 없다. 표본(`energy.measured`)은 읽지 않는다 —
|
|
49
|
+
* 저널에 없고(§4.1 인제스트 계약) 여기서 필요한 것은 마감된 구간뿐이다.
|
|
50
|
+
*/
|
|
51
|
+
const BUSINESS_EVENTS = ['task.status', 'order.status', 'energy.demand.window']
|
|
45
52
|
|
|
46
53
|
/** 한 번에 접을 이벤트 상한 — 큰 구간이 서버를 붙잡지 않게. */
|
|
47
54
|
const MAX_EVENTS = 20000
|
|
@@ -183,7 +190,7 @@ export interface TwinKpiOutput extends Omit<Partial<KpiResult>, 'window'> {
|
|
|
183
190
|
by: KpiGroupBy
|
|
184
191
|
items: (KpiGroupOut & { label?: string })[]
|
|
185
192
|
/**
|
|
186
|
-
* 비교 구간에는 일이 있었는데 **지금은 없는** 축. 표에서 그냥 빠지면 멈춘
|
|
193
|
+
* 비교 구간에는 일이 있었는데 **지금은 없는** 축. 표에서 그냥 빠지면 멈춘 구역·대기 중인 자원이
|
|
187
194
|
* 눈에 보이지 않는다 — 운영에서는 이것이 가장 중요한 신호일 수 있다.
|
|
188
195
|
*/
|
|
189
196
|
disappeared?: { key: string; label?: string; tasks: number }[]
|
|
@@ -199,8 +206,8 @@ type KpiGroupOut = NonNullable<KpiResult['groups']>['items'][number] &
|
|
|
199
206
|
Partial<Pick<CrossedGroup, 'prevTasks' | 'deltaTasks' | 'deltaWorkP50Ms' | 'isNew'>>
|
|
200
207
|
|
|
201
208
|
/**
|
|
202
|
-
* 노드 → 구역 지도. **저널에는 구역이 없다**(노드까지만 있다) —
|
|
203
|
-
* 마스터 계층에서 온 소속이다. 기동하지 않은 트윈도
|
|
209
|
+
* 노드 → 구역 지도. **저널에는 구역이 없다**(노드까지만 있다) — 모델 정의의 `locations[].parentId` 가
|
|
210
|
+
* 마스터 계층에서 온 소속이다. 기동하지 않은 트윈도 모델은 남아 있어 이 경로가 가장 튼튼하다.
|
|
204
211
|
*
|
|
205
212
|
* **한 홉만 보면 안 된다.** `parentId` 는 구역일 수도 있고 중간 단(라인·셀)일 수도 있다. 라인이 끼면
|
|
206
213
|
* 한 홉짜리 코드는 "구역이 LINE-1" 이라고 답하고, 그 id 는 구역 목록에 없으므로 그 아래 스테이션이
|
|
@@ -213,11 +220,11 @@ type KpiGroupOut = NonNullable<KpiResult['groups']>['items'][number] &
|
|
|
213
220
|
* 저널에는 시각만 있고 교대가 없다(일부러 그렇게 뒀다 — 교대는 파생이라 찍어 두면 나중에 교대 시간을
|
|
214
221
|
* 고쳤을 때 과거가 옛 구분으로 굳는다). 그래서 폴드가 계산할 재료를 여기서 모은다.
|
|
215
222
|
*
|
|
216
|
-
* 선언의 출처는
|
|
223
|
+
* 선언의 출처는 **모델의 설비**다. 여러 설비가 서로 다른 교대를 가질 수 있지만, 축은 하나여야 하므로
|
|
217
224
|
* **가장 많이 쓰이는 선언 한 벌**을 쓴다 — 3교대 공장은 전 설비가 같은 교대이므로 이것이 곧 정답이고,
|
|
218
225
|
* 자원마다 교대가 다른 현장에서는 이 축이 거칠다는 뜻이다(그 사실을 여기 적어 둔다).
|
|
219
226
|
*
|
|
220
|
-
* 시각 기준은
|
|
227
|
+
* 시각 기준은 모델에 실려 있다(`utcOffsetMinutes` — 공간이 갖는 값을 기동 시 호스트가 풀어 넣는다).
|
|
221
228
|
* 없으면 UTC 로 읽고, 그 기본값은 커널 계약에 밝혀져 있다.
|
|
222
229
|
*/
|
|
223
230
|
/**
|
|
@@ -247,9 +254,9 @@ async function shiftContext(
|
|
|
247
254
|
const tally = new Map<string, { decl: any[]; n: number }>()
|
|
248
255
|
let utcOffsetMinutes: number | undefined
|
|
249
256
|
for (const row of rows) {
|
|
250
|
-
const
|
|
251
|
-
if (utcOffsetMinutes === undefined && typeof
|
|
252
|
-
for (const e of (
|
|
257
|
+
const model = row.model as any
|
|
258
|
+
if (utcOffsetMinutes === undefined && typeof model?.utcOffsetMinutes === 'number') utcOffsetMinutes = model.utcOffsetMinutes
|
|
259
|
+
for (const e of (model?.equipment ?? []) as any[]) {
|
|
253
260
|
const decl = e?.workCalendar
|
|
254
261
|
if (!Array.isArray(decl) || !decl.length) continue
|
|
255
262
|
const key = JSON.stringify(decl)
|
|
@@ -274,12 +281,17 @@ async function locationAreaMap(
|
|
|
274
281
|
where: { domain: { id: domainId }, instanceId: In(instanceIds) }
|
|
275
282
|
})
|
|
276
283
|
for (const row of rows) {
|
|
277
|
-
const locations = (((row.
|
|
284
|
+
const locations = (((row.model as any)?.locations ?? []) as any[]).filter(n => n?.id)
|
|
278
285
|
if (!locations.length) continue
|
|
279
286
|
/* 순환은 hierarchyOf 가 던진다 — 한 트윈의 잘못된 계층이 다른 트윈의 집계까지 죽이지 않게 여기서 가둔다. */
|
|
280
287
|
let h
|
|
281
288
|
try {
|
|
282
|
-
|
|
289
|
+
/*
|
|
290
|
+
* 단 해석기를 함께 넘긴다 — 자리가 `level` 을 적지 않으면 **타입이 답한다**(카탈로그 선언).
|
|
291
|
+
* 이 집계는 지금 구역 롤업(`rollupOf`)만 쓰므로 결과가 달라지지 않는다. 그래도 여기서
|
|
292
|
+
* 잇는다: 단으로 묻는 질의(`ancestorOfLevel`)가 생길 때 배선을 다시 찾지 않게 한다.
|
|
293
|
+
*/
|
|
294
|
+
h = hierarchyOf({ locations }, t => levelOfLocationType(t, row.kind))
|
|
283
295
|
} catch (e: any) {
|
|
284
296
|
console.warn(`[twin-kpi] "${row.instanceId}": location hierarchy is broken — area rollup skipped for this twin: ${e?.message}`)
|
|
285
297
|
continue
|
|
@@ -305,6 +317,153 @@ async function locationAreaMap(
|
|
|
305
317
|
return { locationArea, labels }
|
|
306
318
|
}
|
|
307
319
|
|
|
320
|
+
/**
|
|
321
|
+
* 자원 → **배전 계통의 자리**(분기·수전) 지도와 그 자리의 이름.
|
|
322
|
+
*
|
|
323
|
+
* ── 무엇을 읽나 ─────────────────────────────────────────────────────────────
|
|
324
|
+
* 에너지 트윈의 모델이 「이 설비가 어느 자리에 매달렸나」를 선언한다(`homeLocation`). 그 자리가 분기가
|
|
325
|
+
* 아니라 하위 자리라면 부모를 타고 올라가 **전기 계통의 자리**를 찾는다 — 전기의 계통은 물리 구역과
|
|
326
|
+
* 다른 나무이므로 구역 롤업(`locationAreaMap`)을 쓸 수 없다.
|
|
327
|
+
*
|
|
328
|
+
* ── 왜 에너지 트윈만 읽나 ───────────────────────────────────────────────────
|
|
329
|
+
* 같은 설비가 공장 트윈에서는 자원(작업을 한다)이고 에너지 트윈에서는 부하(전기를 먹는다)다 — 사실 둘,
|
|
330
|
+
* id 하나. 가동시간은 공장 트윈의 저널에서 오고, 전기 계통의 소속은 이 모델에서 온다. 그 둘을 id 로
|
|
331
|
+
* 잇는 것이 이 지도의 일이다.
|
|
332
|
+
*
|
|
333
|
+
* 자리를 말하지 않는 설비는 **넣지 않는다** — 없는 소속을 만들면 그 몫이 엉뚱한 분기의 것이 된다.
|
|
334
|
+
*/
|
|
335
|
+
/**
|
|
336
|
+
* 선언된 **전기요금 단가** — 에너지 트윈의 자리 속성에서 읽는다(계약전력과 같은 자리).
|
|
337
|
+
*
|
|
338
|
+
* 여러 에너지 트윈이 한 현장에 있으면 **처음 선언된 것 하나**를 쓴다. 평균을 내거나 합치지 않는다:
|
|
339
|
+
* 요금은 계약이고, 두 계약을 섞은 단가는 어느 계약도 아니다. 서로 다른 단가가 선언돼 있으면 그것을
|
|
340
|
+
* 밝힌다(조용히 하나를 고르면 왜 그 금액인지 아무도 설명할 수 없다).
|
|
341
|
+
*/
|
|
342
|
+
async function tariffOf(
|
|
343
|
+
domainId: string,
|
|
344
|
+
instanceIds: string[]
|
|
345
|
+
): Promise<{ tariff?: TariffDeclaration; conflict?: boolean }> {
|
|
346
|
+
if (!instanceIds.length) return {}
|
|
347
|
+
const rows = await getRepository(TwinInstance).find({ where: { domain: { id: domainId }, instanceId: In(instanceIds) } })
|
|
348
|
+
const found: TariffDeclaration[] = []
|
|
349
|
+
for (const row of rows) {
|
|
350
|
+
if (row.kind !== 'ems') continue
|
|
351
|
+
for (const loc of ((row.model as any)?.locations ?? []) as any[]) {
|
|
352
|
+
const props = new Map<string, any>(((loc?.properties ?? []) as any[]).map(p => [String(p?.id), p?.value]))
|
|
353
|
+
const num = (id: string): number | undefined => {
|
|
354
|
+
const v = Number(props.get(id))
|
|
355
|
+
return Number.isFinite(v) && v > 0 ? v : undefined
|
|
356
|
+
}
|
|
357
|
+
const d = num(EMS_PROPERTY.demandChargePerKW)
|
|
358
|
+
const e = num(EMS_PROPERTY.energyChargePerKWh)
|
|
359
|
+
const cur = props.get(EMS_PROPERTY.currency)
|
|
360
|
+
if (d === undefined && e === undefined) continue
|
|
361
|
+
found.push({
|
|
362
|
+
...(d !== undefined ? { demandChargePerKW: d } : {}),
|
|
363
|
+
...(e !== undefined ? { energyChargePerKWh: e } : {}),
|
|
364
|
+
...(cur ? { currency: String(cur) } : {})
|
|
365
|
+
})
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (!found.length) return {}
|
|
369
|
+
const first = found[0]
|
|
370
|
+
const same = found.every(
|
|
371
|
+
t => t.demandChargePerKW === first.demandChargePerKW && t.energyChargePerKWh === first.energyChargePerKWh && t.currency === first.currency
|
|
372
|
+
)
|
|
373
|
+
return { tariff: first, ...(same ? {} : { conflict: true }) }
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function resourceFeederMap(
|
|
377
|
+
domainId: string,
|
|
378
|
+
instanceIds: string[]
|
|
379
|
+
): Promise<{ resourceFeeder: Record<string, string>; meterFeeder: Record<string, string>; labels: Record<string, string> }> {
|
|
380
|
+
const resourceFeeder: Record<string, string> = {}
|
|
381
|
+
/** 계량 지점 → 그것이 재는 분기. 있으면 그 분기 값은 **측정**이다(배분보다 강한 근거). */
|
|
382
|
+
const meterFeeder: Record<string, string> = {}
|
|
383
|
+
const labels: Record<string, string> = {}
|
|
384
|
+
if (!instanceIds.length) return { resourceFeeder, meterFeeder, labels }
|
|
385
|
+
|
|
386
|
+
const rows = await getRepository(TwinInstance).find({ where: { domain: { id: domainId }, instanceId: In(instanceIds) } })
|
|
387
|
+
for (const row of rows) {
|
|
388
|
+
if (row.kind !== 'ems') continue // 전기 계통을 선언하는 것은 에너지 트윈이다
|
|
389
|
+
const model: any = row.model ?? {}
|
|
390
|
+
const locations: any[] = (model.locations ?? []).filter((n: any) => n?.id)
|
|
391
|
+
const byId = new Map(locations.map((n: any) => [String(n.id), n]))
|
|
392
|
+
const electrical = new Set(EMS_LOCATION_TYPES as readonly string[])
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* 이 자리에서 **전기 상류**로 올라가 처음 만나는 계통의 자리. 없으면 undefined.
|
|
396
|
+
*
|
|
397
|
+
* 상류의 뜻은 커널이 읽는다(`electricalUpstreamOf`) — 공간 부모(공장·구역)를 따라 올라가면 전기적으로
|
|
398
|
+
* 뜻이 없는 길을 걷게 되고, 옛 세대(분기의 `parentId` 에 수전)와 새 세대(`upstreamId`)가 섞여 있으므로
|
|
399
|
+
* 그 판정을 여기서 다시 쓰면 두 벌이 된다.
|
|
400
|
+
*/
|
|
401
|
+
const feederOf = (startId: string): string | undefined => {
|
|
402
|
+
const seen = new Set<string>()
|
|
403
|
+
let cur = byId.get(startId)
|
|
404
|
+
while (cur && !seen.has(String(cur.id))) {
|
|
405
|
+
seen.add(String(cur.id)) // 잘못된 모델의 순환이 여기서 멈춘다
|
|
406
|
+
if (electrical.has(String(cur.type))) return String(cur.id)
|
|
407
|
+
const up = electricalUpstreamOf(locations as any, String(cur.id))
|
|
408
|
+
cur = up ? byId.get(up) : cur.parentId ? byId.get(String(cur.parentId)) : undefined
|
|
409
|
+
}
|
|
410
|
+
return undefined
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
for (const e of (model.equipment ?? []) as any[]) {
|
|
414
|
+
const home = e?.homeLocation ?? e?.homeLocationId
|
|
415
|
+
if (!e?.id || !home) continue
|
|
416
|
+
/*
|
|
417
|
+
* 계량기는 부하가 아니다 — 그것의 「몫」을 세면 총량이 이중으로 나뉜다. 대신 **어디를 재는지**를
|
|
418
|
+
* 따로 모은다: 계량기가 분기에 있으면 그 분기 값은 배분이 아니라 **측정**이다(가장 강한 근거).
|
|
419
|
+
*/
|
|
420
|
+
if (e.kind === 'meter') {
|
|
421
|
+
const at = feederOf(String(home))
|
|
422
|
+
/* 뿌리(수전)를 재는 계량기는 분기의 것이 아니다 — 그 값은 총량이고, 나머지는 뺄셈으로 나온다. */
|
|
423
|
+
if (at && String(byId.get(at)?.type) !== 'incoming') meterFeeder[String(e.id)] = at
|
|
424
|
+
continue
|
|
425
|
+
}
|
|
426
|
+
/* 차단기·태양광·축전지도 부하가 아니다. */
|
|
427
|
+
if (e.kind === 'breaker' || e.kind === 'pv-array' || e.kind === 'battery') continue
|
|
428
|
+
const feeder = feederOf(String(home))
|
|
429
|
+
if (feeder) resourceFeeder[String(e.id)] = feeder
|
|
430
|
+
}
|
|
431
|
+
for (const n of locations) if (electrical.has(String(n.type)) && n.name) labels[String(n.id)] = String(n.name)
|
|
432
|
+
}
|
|
433
|
+
return { resourceFeeder, meterFeeder, labels }
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* 이 공간의 에너지 트윈들의 계통도 — 모델을 읽어 트리로 접는다.
|
|
438
|
+
*
|
|
439
|
+
* 여러 트윈이 한 공간에 있으면 **각 트윈의 뿌리를 나란히** 둔다(하나로 합치면 없는 결선을 그리게 된다).
|
|
440
|
+
* 에너지 트윈이 없으면 만들지 않는다 — 빈 그림은 고장으로 읽힌다.
|
|
441
|
+
*/
|
|
442
|
+
async function energyTopologyOf(
|
|
443
|
+
domainId: string,
|
|
444
|
+
instanceIds: string[],
|
|
445
|
+
energy: { byFeeder?: { key: string; kWh: number; basis?: 'measured' | 'apportioned'; peakKW?: number }[] } | undefined
|
|
446
|
+
): Promise<EnergyTopology | undefined> {
|
|
447
|
+
if (!instanceIds.length) return undefined
|
|
448
|
+
const rows = await getRepository(TwinInstance).find({ where: { domain: { id: domainId }, instanceId: In(instanceIds) } })
|
|
449
|
+
const ems = rows.filter(r => r.kind === 'ems' && r.model)
|
|
450
|
+
if (!ems.length) return undefined
|
|
451
|
+
const merged: EnergyTopology = { roots: [], unplaced: [], unmetered: [], loadsOnIncoming: 0 }
|
|
452
|
+
for (const row of ems) {
|
|
453
|
+
/*
|
|
454
|
+
* 지금의 설비 상태를 함께 넘긴다 — 발전·저장의 **지금 값**은 구간 폴드가 아니라 스냅샷이 안다.
|
|
455
|
+
* 없으면(정지된 트윈) 구조만 그린다.
|
|
456
|
+
*/
|
|
457
|
+
const snap = TwinEngine.snapshot(domainId, row.instanceId)
|
|
458
|
+
const t = buildEnergyTopology(row.model, energy, snap?.equipment)
|
|
459
|
+
merged.roots.push(...t.roots)
|
|
460
|
+
merged.unplaced.push(...t.unplaced)
|
|
461
|
+
merged.unmetered.push(...t.unmetered)
|
|
462
|
+
merged.loadsOnIncoming += t.loadsOnIncoming
|
|
463
|
+
}
|
|
464
|
+
return merged.roots.length ? merged : undefined
|
|
465
|
+
}
|
|
466
|
+
|
|
308
467
|
/** 이 대상들의 마지막 업무 기록 시각(ISO). 기록이 없으면 null. */
|
|
309
468
|
async function latestEventTime(domainId: string, instanceIds: string[]): Promise<string | null> {
|
|
310
469
|
if (instanceIds.length === 0) return null
|
|
@@ -312,14 +471,15 @@ async function latestEventTime(domainId: string, instanceIds: string[]): Promise
|
|
|
312
471
|
where: { domain: { id: domainId }, instanceId: In(instanceIds), eventType: In(BUSINESS_EVENTS) },
|
|
313
472
|
order: { revision: 'DESC' }
|
|
314
473
|
})
|
|
315
|
-
|
|
474
|
+
/* 소비처는 ISO 문자열을 기대한다(시각 축의 공용 표기) — 컬럼이 날짜가 됐으므로 여기서 옮긴다. */
|
|
475
|
+
return row?.eventTime ? row.eventTime.toISOString() : null
|
|
316
476
|
}
|
|
317
477
|
|
|
318
478
|
/**
|
|
319
479
|
* 창 구간의 업무 이벤트 — 창보다 `lookbackMs` 만큼 앞까지 읽는다(완료된 작업의 시작이 창 밖일 수 있다).
|
|
320
480
|
*
|
|
321
|
-
* eventTime 은
|
|
322
|
-
*
|
|
481
|
+
* eventTime 은 날짜 컬럼이라 DB 가 시각으로 비교한다 — 예전 문자열 컬럼 시절의 "UTC ISO 만 들어오면
|
|
482
|
+
* 사전식 비교가 맞는다" 는 전제가 필요 없다.
|
|
323
483
|
*/
|
|
324
484
|
async function fetchEvents(domainId: string, instanceIds: string[], fromMs: number, toMs: number, lookbackMs: number) {
|
|
325
485
|
if (instanceIds.length === 0) return { events: [] as KpiEvent[], capped: false }
|
|
@@ -328,7 +488,7 @@ async function fetchEvents(domainId: string, instanceIds: string[], fromMs: numb
|
|
|
328
488
|
domain: { id: domainId },
|
|
329
489
|
instanceId: In(instanceIds),
|
|
330
490
|
eventType: In(BUSINESS_EVENTS),
|
|
331
|
-
eventTime: Between(new Date(fromMs - lookbackMs)
|
|
491
|
+
eventTime: Between(new Date(fromMs - lookbackMs), new Date(toMs))
|
|
332
492
|
},
|
|
333
493
|
order: { revision: 'ASC' },
|
|
334
494
|
take: MAX_EVENTS
|
|
@@ -336,7 +496,8 @@ async function fetchEvents(domainId: string, instanceIds: string[], fromMs: numb
|
|
|
336
496
|
/* 상한에 정확히 닿으면 **더 있었을 수 있다** — 그 사실을 돌려준다. 조용히 잘라내면 숫자가 조용히
|
|
337
497
|
* 작아지고, 사용자는 그것을 성과 하락으로 읽는다(가장 나쁜 종류의 거짓이다). */
|
|
338
498
|
return {
|
|
339
|
-
|
|
499
|
+
/* 폴드는 ISO 문자열 축으로 수행된다(kpi-fold) — 경계에서 한 번만 옮긴다. */
|
|
500
|
+
events: rows.map(r => ({ eventType: r.eventType, eventTime: r.eventTime?.toISOString(), payload: r.payload })),
|
|
340
501
|
capped: rows.length >= MAX_EVENTS
|
|
341
502
|
}
|
|
342
503
|
}
|
|
@@ -355,7 +516,8 @@ async function resolveTargets(input: TwinKpiInput): Promise<{ instanceIds: strin
|
|
|
355
516
|
: list.filter((x: any) => x?.running && x.spaceId === input.spaceId && x.purpose !== 'bench' && x.instanceId)
|
|
356
517
|
/* 목록에서 못 찾아도(정지·비등록) 지정된 트윈은 그대로 조회한다 — 저널은 남아 있을 수 있다. */
|
|
357
518
|
const instanceIds = rows.length ? rows.map((x: any) => x.instanceId as string) : input.instanceId ? [input.instanceId] : []
|
|
358
|
-
|
|
519
|
+
/* 컬럼이 NOT NULL 이라 'unknown' 으로 메울 일이 없다 — 비면 계약이 깨진 것이지 모르는 상태가 아니다. */
|
|
520
|
+
const realityModes = [...new Set(rows.map((x: any) => String(x.realityMode)))]
|
|
359
521
|
return { instanceIds, realityModes }
|
|
360
522
|
}
|
|
361
523
|
|
|
@@ -419,9 +581,19 @@ export async function computeTwinKpi(input: TwinKpiInput): Promise<TwinKpiOutput
|
|
|
419
581
|
const areaMap =
|
|
420
582
|
input.groupBy === 'area' ? await locationAreaMap(input.domainId, instanceIds, input.spaceId) : { locationArea: {}, labels: {} }
|
|
421
583
|
const shiftCtx = input.groupBy === 'shift' ? await shiftContext(input.domainId, instanceIds) : {}
|
|
584
|
+
/*
|
|
585
|
+
* 전기 계통 소속은 **축과 무관하게** 읽는다 — 분기별 몫은 관점 축이 아니라 에너지 사실의 일부라
|
|
586
|
+
* (같은 창의 총량을 나눈 것) 어느 축을 보고 있든 함께 나가야 한다.
|
|
587
|
+
*/
|
|
588
|
+
const feederMap = await resourceFeederMap(input.domainId, instanceIds)
|
|
589
|
+
/* 요금 단가도 축과 무관하게 읽는다 — 금액은 같은 창의 전력량·최대수요에서 나오는 사실이다. */
|
|
590
|
+
const tariffFound = await tariffOf(input.domainId, instanceIds)
|
|
422
591
|
const kpi = foldKpi(events, { fromMs, toMs }, {
|
|
423
592
|
groupBy: input.groupBy,
|
|
424
593
|
locationArea: areaMap.locationArea,
|
|
594
|
+
resourceFeeder: feederMap.resourceFeeder,
|
|
595
|
+
meterFeeder: feederMap.meterFeeder,
|
|
596
|
+
...(tariffFound.tariff ? { tariff: tariffFound.tariff } : {}),
|
|
425
597
|
...shiftCtx,
|
|
426
598
|
groupLimit: input.groupLimit
|
|
427
599
|
})
|
|
@@ -608,6 +780,29 @@ export async function computeTwinKpi(input: TwinKpiInput): Promise<TwinKpiOutput
|
|
|
608
780
|
|
|
609
781
|
/* 폴드가 돌려준 raw 창(ms)은 버린다 — 위의 ISO 창이 사람이 읽을 정본이다. */
|
|
610
782
|
const { window: _rawWindow, groups: _rawGroups, ...rest } = kpi
|
|
783
|
+
/*
|
|
784
|
+
* 자리에 **부르는 이름**을 붙인다 — `feeder-2` 는 id 이고 사람은 「가공 라인」으로 부른다.
|
|
785
|
+
* 이름을 모르는 자리는 id 그대로 둔다(지어내지 않는다).
|
|
786
|
+
*/
|
|
787
|
+
/* 단가가 갈려 있으면 밝힌다 — 우리가 고른 하나로 낸 금액이라는 사실을 화면이 말할 수 있게. */
|
|
788
|
+
if (rest.energy?.cost && tariffFound.conflict) rest.energy = { ...rest.energy, tariffConflict: true } as any
|
|
789
|
+
if (rest.energy?.byFeeder?.length) {
|
|
790
|
+
rest.energy = {
|
|
791
|
+
...rest.energy,
|
|
792
|
+
byFeeder: rest.energy.byFeeder.map(row => ({ ...row, ...(feederMap.labels[row.key] ? { label: feederMap.labels[row.key] } : {}) }))
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
/*
|
|
796
|
+
* 계통도 — **선언된 관계와 계량이 덮는 범위**를 함께 낸다.
|
|
797
|
+
*
|
|
798
|
+
* 값은 여기서 다시 계산하지 않는다(폴드가 낸 분기별 몫을 그대로 노드에 얹는다). 계통은 모델이 아는
|
|
799
|
+
* 사실이므로 저널만 보는 폴드가 아니라 여기서 조립한다 — 화면이 모델을 따로 또 조회하지 않아도 되고,
|
|
800
|
+
* AI 도 같은 값을 읽는다(규칙 한 벌).
|
|
801
|
+
*/
|
|
802
|
+
if (rest.energy) {
|
|
803
|
+
const topo = await energyTopologyOf(input.domainId, instanceIds, rest.energy)
|
|
804
|
+
if (topo) rest.energy = { ...rest.energy, topology: topo } as any
|
|
805
|
+
}
|
|
611
806
|
return {
|
|
612
807
|
window,
|
|
613
808
|
measured: true,
|
|
@@ -50,7 +50,7 @@ const statOf = (s: DurationStats, pick: (d: DurationStats) => number): number |
|
|
|
50
50
|
* 좋다 — 이것을 입력으로 받으면 반대로 적은 목표가 만들어지고, 그 뒤 판정이 전부 뒤집힌다.
|
|
51
51
|
*/
|
|
52
52
|
export const TWIN_METRIC = {
|
|
53
|
-
/* 기록이 없는 구간은 처리량 0 이 아니라 **모름**이다 — `eventsInWindow` 가 그것을
|
|
53
|
+
/* 기록이 없는 구간은 처리량 0 이 아니라 **모름**이다 — `eventsInWindow` 가 그것을 구분한다.
|
|
54
54
|
0 으로 판정하면 "측정 못 한 구간" 이 "일을 안 한 구간" 으로 보고된다. */
|
|
55
55
|
'throughput.tasks': { direction: 'higher', unit: 'count', of: (k: KpiResult) => measured(k, k.throughput.tasks) },
|
|
56
56
|
'throughput.orders': { direction: 'higher', unit: 'count', of: (k: KpiResult) => measured(k, k.throughput.orders) },
|
|
@@ -61,6 +61,28 @@ export const TWIN_METRIC = {
|
|
|
61
61
|
'leadTime.p90Ms': { direction: 'lower', unit: 'ms', of: (k: KpiResult) => statOf(k.leadTime, d => d.p90Ms) },
|
|
62
62
|
'workTime.p50Ms': { direction: 'lower', unit: 'ms', of: (k: KpiResult) => statOf(k.workTime, d => d.p50Ms) },
|
|
63
63
|
'waitTime.p50Ms': { direction: 'lower', unit: 'ms', of: (k: KpiResult) => statOf(k.waitTime, d => d.p50Ms) },
|
|
64
|
+
/*
|
|
65
|
+
* ── 에너지 (2026-08-17) ────────────────────────────────────────────────────
|
|
66
|
+
*
|
|
67
|
+
* 값은 폴드가 이미 내고 있었는데 **목표를 걸 수 있는 지표 목록에 없었다.** 그래서 화면은 「이번 창에
|
|
68
|
+
* 1,240kWh」를 보여 주면서도 그것이 좋은지 나쁜지는 말하지 못했다 — 에너지야말로 계약이라는 딱딱한
|
|
69
|
+
* 기준선이 있는 영역인데도 그랬다.
|
|
70
|
+
*
|
|
71
|
+
* 셋 다 방향은 **낮을수록 좋다**로 못 박는다. 「많이 쓸수록 좋다」는 에너지 목표는 없다. 산출이 늘어
|
|
72
|
+
* 전력량이 느는 경우는 원단위(`energy.intensity`)가 답할 자리이지, 방향을 뒤집을 이유가 아니다.
|
|
73
|
+
*
|
|
74
|
+
* 값이 없는 창(에너지 트윈이 아니거나 계측이 끊긴 창)은 `undefined` 로 남긴다 — 0 으로 읽으면
|
|
75
|
+
* 시간 지표에서 이미 겪은 그 결함이 그대로 재현된다(안 쓴 것이 아니라 못 잰 것인데 만점이 된다).
|
|
76
|
+
*/
|
|
77
|
+
'energy.kWh': { direction: 'lower', unit: 'kWh', of: (k: KpiResult) => k.energy?.kWh },
|
|
78
|
+
/** 최대수요 — 계약 상한을 목표로 적는 자리다(요금의 기본요금 부분이 여기서 정해진다). */
|
|
79
|
+
'energy.peakKW': { direction: 'lower', unit: 'kW', of: (k: KpiResult) => k.energy?.peakKW },
|
|
80
|
+
/** 원단위 — 완료 오더당 kWh. 답할 수 없으면(`null`) 판정하지 않는다. */
|
|
81
|
+
'energy.intensity': {
|
|
82
|
+
direction: 'lower',
|
|
83
|
+
unit: 'kWh/order',
|
|
84
|
+
of: (k: KpiResult) => (typeof k.energy?.intensity?.value === 'number' ? k.energy.intensity.value : undefined)
|
|
85
|
+
},
|
|
64
86
|
/** 자원 점유 — 개별 자원이 아니라 **창 전체의 평균 비율**(자원별은 관점 축의 일이다). */
|
|
65
87
|
'utilization.ratio': {
|
|
66
88
|
direction: 'higher',
|
|
@@ -218,7 +240,7 @@ export function judgeTargets(kpi: KpiResult, targets: readonly TwinTargetSpec[]
|
|
|
218
240
|
return out
|
|
219
241
|
}
|
|
220
242
|
|
|
221
|
-
/** 판정 산수 한 곳 — 전체 판정과 축 판정이 **같은 규칙**을 쓴다(두 벌이면
|
|
243
|
+
/** 판정 산수 한 곳 — 전체 판정과 축 판정이 **같은 규칙**을 쓴다(두 벌이면 어긋난다). */
|
|
222
244
|
function verdictOf(metric: TwinMetricId, actual: number, target: number, direction: 'higher' | 'lower'): TargetVerdict {
|
|
223
245
|
const met = direction === 'higher' ? actual >= target : actual <= target
|
|
224
246
|
const ratio = direction === 'higher' ? (target > 0 ? actual / target : Number.NaN) : actual > 0 ? target / actual : Number.NaN
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 「이 트윈에 계측이 들어오고 있나」 — **그 사실을 한 자리에서 묻는다.**
|
|
3
|
+
*
|
|
4
|
+
* ── 무엇이 보이지 않았나 (2026-08-14) ───────────────────────────────────────
|
|
5
|
+
* 미러 트윈은 커널이 돌기만 하면 「도는 중」이라 보고했다. 그런데 계측을 나르는 피드가 떨어져 있으면
|
|
6
|
+
* 그 트윈은 **아무것도 받지 않으면서 도는 중**이다 — 화면에서 두 상태가 똑같이 보였고, 사람은
|
|
7
|
+
* 「값이 안 변하네」로 뒤늦게 알게 됐다(재기동 뒤 실제로 그랬다).
|
|
8
|
+
*
|
|
9
|
+
* ── 왜 장부가 하나여야 하나 ────────────────────────────────────────────────
|
|
10
|
+
* 피드를 여는 곳이 둘이다: 커넥터 기반(`reference-live`)과 프리셋 기반(앱의 `provision-from-preset`).
|
|
11
|
+
* 한쪽만 보고 「끊김」이라 말하면 **멀쩡히 흐르는 트윈을 끊겼다고 부르게 된다** — 거짓 경보는 참 경보를
|
|
12
|
+
* 죽인다. 그래서 각 계층이 자기 장부를 여기 **등록**하고, 묻는 쪽은 이 한 자리에만 묻는다.
|
|
13
|
+
*
|
|
14
|
+
* 호스트는 어떤 어댑터·프리셋이 있는지 몰라도 된다(계층을 거꾸로 잇지 않는다).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** 지금 피드가 붙어 있는 인스턴스 id 들 — 그 계층의 장부가 답한다. */
|
|
18
|
+
export type LiveFeedProbe = () => string[]
|
|
19
|
+
|
|
20
|
+
const probes: LiveFeedProbe[] = []
|
|
21
|
+
|
|
22
|
+
/** 피드 장부를 등록한다(모듈 로드 시 한 번). */
|
|
23
|
+
export function registerLiveFeedProbe(probe: LiveFeedProbe): void {
|
|
24
|
+
probes.push(probe)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 피드가 붙어 있나 — 어느 장부든 그렇다고 하면 그렇다.
|
|
29
|
+
*
|
|
30
|
+
* 장부가 터지는 것을 이 판정 전체의 실패로 만들지 않는다: 한 계층이 답하지 못해도 다른 계층의 사실은
|
|
31
|
+
* 그대로 참이다. 다만 **조용히 넘기지 않는다** — 답하지 못한 장부는 로그에 남긴다.
|
|
32
|
+
*/
|
|
33
|
+
export function liveFeedAttached(instanceId: string): boolean {
|
|
34
|
+
for (const probe of probes) {
|
|
35
|
+
try {
|
|
36
|
+
if (probe().includes(instanceId)) return true
|
|
37
|
+
} catch (e: any) {
|
|
38
|
+
console.warn('[twin-live] a feed registry could not answer —', e?.message ?? e)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 이 트윈의 **피드 사실** — 화면이 그대로 말할 수 있는 세 값.
|
|
46
|
+
*
|
|
47
|
+
* `not-applicable` 을 `detached` 와 섞지 않는다(이 시스템이 내내 지켜 온 「없음 ≠ 해당 없음」):
|
|
48
|
+
* · 시뮬 트윈에는 나를 피드가 **없어야 한다** — 그건 결함이 아니라 그 트윈의 성질이다.
|
|
49
|
+
* · 멈춘 트윈에게 피드를 묻는 것은 뜻이 없다 — 사람이 멈춰 둔 것이다.
|
|
50
|
+
* · 남는 것 하나가 우리가 보이려던 사실이다: **도는 미러인데 아무것도 오지 않는다.**
|
|
51
|
+
*/
|
|
52
|
+
export type LiveFeedState = 'attached' | 'detached' | 'not-applicable'
|
|
53
|
+
|
|
54
|
+
export function liveFeedStateOf(args: { realityMode?: string; running?: boolean; instanceId: string }): LiveFeedState {
|
|
55
|
+
if (args.realityMode !== 'mirror') return 'not-applicable'
|
|
56
|
+
if (!args.running) return 'not-applicable'
|
|
57
|
+
return liveFeedAttached(args.instanceId) ? 'attached' : 'detached'
|
|
58
|
+
}
|