@things-factory/headless-twin 10.0.15 → 10.0.18
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/attention-digest.d.ts +4 -4
- package/dist-server/engine/attention-digest.js +7 -7
- package/dist-server/engine/attention-digest.js.map +1 -1
- package/dist-server/engine/canonical-ingest.d.ts +18 -82
- package/dist-server/engine/canonical-ingest.js +183 -18
- 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/declared-stimulus.js +1 -1
- package/dist-server/engine/declared-stimulus.js.map +1 -1
- package/dist-server/engine/energy-topology.js +4 -4
- package/dist-server/engine/energy-topology.js.map +1 -1
- package/dist-server/engine/ingest-dedupe.d.ts +38 -0
- package/dist-server/engine/ingest-dedupe.js +145 -0
- package/dist-server/engine/ingest-dedupe.js.map +1 -0
- package/dist-server/engine/ingest-health.d.ts +90 -10
- package/dist-server/engine/ingest-health.js +125 -16
- package/dist-server/engine/ingest-health.js.map +1 -1
- package/dist-server/engine/integration-probes.d.ts +3 -3
- package/dist-server/engine/integration-probes.js +6 -6
- package/dist-server/engine/integration-probes.js.map +1 -1
- package/dist-server/engine/kpi-baseline.js +4 -4
- package/dist-server/engine/kpi-baseline.js.map +1 -1
- package/dist-server/engine/kpi-fold.d.ts +212 -14
- package/dist-server/engine/kpi-fold.js +374 -21
- package/dist-server/engine/kpi-fold.js.map +1 -1
- package/dist-server/engine/kpi-query.d.ts +11 -11
- package/dist-server/engine/kpi-query.js +281 -36
- package/dist-server/engine/kpi-query.js.map +1 -1
- package/dist-server/engine/live-feed-registry.d.ts +1 -1
- package/dist-server/engine/live-feed-registry.js +1 -1
- 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.map +1 -1
- package/dist-server/engine/local-declarations.d.ts +7 -7
- package/dist-server/engine/local-declarations.js +15 -14
- package/dist-server/engine/local-declarations.js.map +1 -1
- package/dist-server/engine/log.d.ts +1 -1
- package/dist-server/engine/log.js +1 -1
- package/dist-server/engine/log.js.map +1 -1
- package/dist-server/engine/measured-estimator.js +1 -1
- package/dist-server/engine/measured-estimator.js.map +1 -1
- package/dist-server/engine/measured-yield.js +1 -1
- package/dist-server/engine/measured-yield.js.map +1 -1
- package/dist-server/engine/model-basis.d.ts +1 -1
- package/dist-server/engine/model-basis.js +1 -1
- package/dist-server/engine/model-basis.js.map +1 -1
- package/dist-server/engine/model-gap.d.ts +3 -3
- package/dist-server/engine/model-gap.js +1 -1
- package/dist-server/engine/model-gap.js.map +1 -1
- package/dist-server/engine/model-vocabulary.js +3 -3
- package/dist-server/engine/model-vocabulary.js.map +1 -1
- package/dist-server/engine/oee-accumulator.d.ts +6 -5
- package/dist-server/engine/oee-accumulator.js +6 -5
- package/dist-server/engine/oee-accumulator.js.map +1 -1
- package/dist-server/engine/property-effects.js +2 -2
- package/dist-server/engine/property-effects.js.map +1 -1
- package/dist-server/engine/read-time.d.ts +22 -0
- package/dist-server/engine/read-time.js +81 -0
- package/dist-server/engine/read-time.js.map +1 -0
- package/dist-server/engine/restart-policy.js +1 -1
- 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/spec-coverage.d.ts +2 -2
- package/dist-server/engine/spec-coverage.js +1 -1
- package/dist-server/engine/spec-coverage.js.map +1 -1
- package/dist-server/engine/stage-path.d.ts +4 -4
- package/dist-server/engine/stage-path.js +1 -1
- package/dist-server/engine/stage-path.js.map +1 -1
- package/dist-server/engine/state-axes.d.ts +1 -1
- package/dist-server/engine/state-axes.js +2 -2
- package/dist-server/engine/state-axes.js.map +1 -1
- package/dist-server/engine/travel-estimator.d.ts +2 -2
- package/dist-server/engine/travel-estimator.js +1 -1
- package/dist-server/engine/travel-estimator.js.map +1 -1
- package/dist-server/engine/twin-engine.d.ts +307 -54
- package/dist-server/engine/twin-engine.js +822 -196
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/engine/warm-start.d.ts +2 -2
- package/dist-server/engine/warm-start.js +5 -5
- package/dist-server/engine/warm-start.js.map +1 -1
- package/dist-server/index.js +1 -1
- package/dist-server/index.js.map +1 -1
- package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js +1 -1
- package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js.map +1 -1
- package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js +3 -3
- package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js.map +1 -1
- package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js +2 -2
- package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js.map +1 -1
- package/dist-server/migrations/1786300000000-IndexEventTypeByTime.d.ts +5 -0
- package/dist-server/migrations/1786300000000-IndexEventTypeByTime.js +54 -0
- package/dist-server/migrations/1786300000000-IndexEventTypeByTime.js.map +1 -0
- package/dist-server/migrations/1786400000000-IndexEventCreatedAt.d.ts +5 -0
- package/dist-server/migrations/1786400000000-IndexEventCreatedAt.js +44 -0
- package/dist-server/migrations/1786400000000-IndexEventCreatedAt.js.map +1 -0
- package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.d.ts +6 -0
- package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.js +84 -0
- package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.js.map +1 -0
- package/dist-server/migrations/index.js +7 -1
- package/dist-server/migrations/index.js.map +1 -1
- package/dist-server/routes.d.ts +1 -0
- package/dist-server/routes.js +69 -5
- package/dist-server/routes.js.map +1 -1
- package/dist-server/service/index.d.ts +2 -2
- package/dist-server/service/index.js +30 -24
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/reference/control-routing.d.ts +1 -1
- package/dist-server/service/reference/control-routing.js +1 -1
- package/dist-server/service/reference/control-routing.js.map +1 -1
- package/dist-server/service/reference/discovery-result.d.ts +2 -2
- package/dist-server/service/reference/discovery-result.js +3 -3
- package/dist-server/service/reference/discovery-result.js.map +1 -1
- package/dist-server/service/reference/hook-contract.d.ts +43 -0
- package/dist-server/service/reference/hook-contract.js +59 -0
- package/dist-server/service/reference/hook-contract.js.map +1 -0
- package/dist-server/service/reference/ingest-space.d.ts +1 -1
- package/dist-server/service/reference/ingest-space.js +4 -4
- package/dist-server/service/reference/ingest-space.js.map +1 -1
- package/dist-server/service/reference/knob-defaults.d.ts +1 -1
- package/dist-server/service/reference/knob-defaults.js +4 -4
- package/dist-server/service/reference/knob-defaults.js.map +1 -1
- package/dist-server/service/reference/reference-adapter.d.ts +116 -9
- package/dist-server/service/reference/reference-adapter.js +27 -0
- package/dist-server/service/reference/reference-adapter.js.map +1 -1
- package/dist-server/service/reference/reference-hook.d.ts +20 -0
- package/dist-server/service/reference/reference-hook.js +133 -0
- package/dist-server/service/reference/reference-hook.js.map +1 -0
- package/dist-server/service/reference/reference-live.d.ts +2 -2
- package/dist-server/service/reference/reference-live.js +76 -13
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-master.d.ts +25 -16
- package/dist-server/service/reference/reference-master.js +35 -13
- package/dist-server/service/reference/reference-master.js.map +1 -1
- package/dist-server/service/reference/reference-resolver.d.ts +1 -1
- package/dist-server/service/reference/reference-resolver.js +12 -12
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/reference/template-registry.d.ts +1 -1
- package/dist-server/service/reference/template-registry.js.map +1 -1
- package/dist-server/service/reference/twin-reference.d.ts +3 -3
- package/dist-server/service/reference/twin-reference.js.map +1 -1
- package/dist-server/service/twin-attention/twin-attention-query.d.ts +1 -1
- package/dist-server/service/twin-attention/twin-attention-query.js +1 -1
- package/dist-server/service/twin-attention/twin-attention-query.js.map +1 -1
- package/dist-server/service/twin-audit/command-audit.d.ts +1 -1
- package/dist-server/service/twin-audit/command-audit.js.map +1 -1
- package/dist-server/service/twin-audit/twin-audit-event.d.ts +1 -1
- package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -1
- package/dist-server/service/twin-audit/twin-audit-query.js +1 -1
- package/dist-server/service/twin-audit/twin-audit-query.js.map +1 -1
- package/dist-server/service/twin-backfill/backfill-period-facts.d.ts +24 -0
- package/dist-server/service/twin-backfill/backfill-period-facts.js +96 -0
- package/dist-server/service/twin-backfill/backfill-period-facts.js.map +1 -0
- package/dist-server/service/twin-backfill/backfill-shape.d.ts +34 -0
- package/dist-server/service/twin-backfill/backfill-shape.js +75 -0
- package/dist-server/service/twin-backfill/backfill-shape.js.map +1 -0
- package/dist-server/service/twin-backfill/index.d.ts +2 -0
- package/dist-server/service/twin-backfill/index.js +6 -0
- package/dist-server/service/twin-backfill/index.js.map +1 -0
- package/dist-server/service/twin-backfill/twin-backfill-resolver.d.ts +10 -0
- package/dist-server/service/twin-backfill/twin-backfill-resolver.js +44 -0
- package/dist-server/service/twin-backfill/twin-backfill-resolver.js.map +1 -0
- package/dist-server/service/twin-control/twin-control-mutation.js +3 -3
- 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 +2 -2
- package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
- package/dist-server/service/twin-event/twin-event-type.d.ts +1 -0
- package/dist-server/service/twin-event/twin-event-type.js +8 -1
- package/dist-server/service/twin-event/twin-event-type.js.map +1 -1
- package/dist-server/service/twin-event/twin-event.d.ts +2 -2
- package/dist-server/service/twin-event/twin-event.js +32 -4
- package/dist-server/service/twin-event/twin-event.js.map +1 -1
- package/dist-server/service/twin-forecast/forecast-metrics.js +1 -1
- package/dist-server/service/twin-forecast/forecast-metrics.js.map +1 -1
- package/dist-server/service/twin-forecast/gap-analytics.d.ts +1 -1
- package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
- package/dist-server/service/twin-forecast/twin-forecast-query.js +29 -11
- package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
- package/dist-server/service/twin-ingest-window/twin-ingest-window-query.d.ts +1 -1
- package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js +1 -1
- package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js.map +1 -1
- package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js +1 -1
- package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js.map +1 -1
- package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
- package/dist-server/service/twin-journal/event-type-split-shape.d.ts +50 -0
- package/dist-server/service/twin-journal/event-type-split-shape.js +111 -0
- package/dist-server/service/twin-journal/event-type-split-shape.js.map +1 -0
- package/dist-server/service/twin-journal/event-type-split.d.ts +15 -0
- package/dist-server/service/twin-journal/event-type-split.js +102 -0
- package/dist-server/service/twin-journal/event-type-split.js.map +1 -0
- package/dist-server/service/twin-journal/journal-order.d.ts +15 -0
- package/dist-server/service/twin-journal/journal-order.js +17 -0
- package/dist-server/service/twin-journal/journal-order.js.map +1 -0
- package/dist-server/service/twin-journal/twin-journal-query.d.ts +5 -5
- package/dist-server/service/twin-journal/twin-journal-query.js +232 -21
- package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
- package/dist-server/service/twin-lifecycle/domain-catalog.js +2 -1
- package/dist-server/service/twin-lifecycle/domain-catalog.js.map +1 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +27 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +59 -3
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
- package/dist-server/service/twin-metrics/twin-metrics-query.js +14 -1
- package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
- package/dist-server/service/twin-model/axis-journal-evidence.js +1 -1
- package/dist-server/service/twin-model/axis-journal-evidence.js.map +1 -1
- package/dist-server/service/twin-model/epcis-coverage.js +34 -3
- package/dist-server/service/twin-model/epcis-coverage.js.map +1 -1
- package/dist-server/service/twin-model/iec61850-coverage.js +1 -1
- package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -1
- package/dist-server/service/twin-model/isa95-coverage.d.ts +12 -9
- package/dist-server/service/twin-model/isa95-coverage.js +33 -9
- package/dist-server/service/twin-model/isa95-coverage.js.map +1 -1
- package/dist-server/service/twin-model/item-ref.js +2 -2
- package/dist-server/service/twin-model/item-ref.js.map +1 -1
- package/dist-server/service/twin-model/name-index.d.ts +1 -1
- package/dist-server/service/twin-model/name-index.js +2 -2
- package/dist-server/service/twin-model/name-index.js.map +1 -1
- package/dist-server/service/twin-model/project-structure.d.ts +1 -1
- package/dist-server/service/twin-model/project-structure.js +7 -6
- package/dist-server/service/twin-model/project-structure.js.map +1 -1
- package/dist-server/service/twin-model/twin-equipment.d.ts +2 -2
- package/dist-server/service/twin-model/twin-equipment.js +3 -3
- package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
- package/dist-server/service/twin-model/twin-lineage-query.js +16 -5
- package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-location.d.ts +1 -1
- package/dist-server/service/twin-model/twin-location.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-item-query.d.ts +3 -3
- package/dist-server/service/twin-model/twin-model-item-query.js +15 -15
- package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-mutation.d.ts +2 -2
- package/dist-server/service/twin-model/twin-model-mutation.js +4 -4
- package/dist-server/service/twin-model/twin-model-mutation.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-query.js +51 -30
- package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-tree-query.js +23 -1
- package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-operation.d.ts +1 -1
- package/dist-server/service/twin-model/twin-operation.js +1 -1
- package/dist-server/service/twin-model/twin-operation.js.map +1 -1
- package/dist-server/service/twin-space/move-space.js +2 -2
- package/dist-server/service/twin-space/move-space.js.map +1 -1
- package/dist-server/service/twin-space/representation-role.d.ts +2 -0
- package/dist-server/service/twin-space/representation-role.js +18 -0
- package/dist-server/service/twin-space/representation-role.js.map +1 -0
- package/dist-server/service/twin-space/space-integrity.d.ts +1 -1
- package/dist-server/service/twin-space/space-integrity.js.map +1 -1
- package/dist-server/service/twin-space/twin-space-representation.d.ts +2 -0
- package/dist-server/service/twin-space/twin-space-representation.js +16 -0
- package/dist-server/service/twin-space/twin-space-representation.js.map +1 -1
- package/dist-server/service/twin-space/twin-space-resolver.d.ts +2 -2
- package/dist-server/service/twin-space/twin-space-resolver.js +37 -8
- package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
- package/dist-server/service/twin-structure/twin-structure.js +3 -3
- package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
- package/dist-server/service/twin-subject/event-subjects.d.ts +43 -0
- package/dist-server/service/twin-subject/event-subjects.js +137 -0
- package/dist-server/service/twin-subject/event-subjects.js.map +1 -0
- package/dist-server/service/twin-subject/index.d.ts +9 -0
- package/dist-server/service/twin-subject/index.js +16 -0
- package/dist-server/service/twin-subject/index.js.map +1 -0
- package/dist-server/service/twin-subject/item-resolver.d.ts +10 -0
- package/dist-server/service/twin-subject/item-resolver.js +93 -0
- package/dist-server/service/twin-subject/item-resolver.js.map +1 -0
- package/dist-server/service/twin-subject/subject-query-shape.d.ts +22 -0
- package/dist-server/service/twin-subject/subject-query-shape.js +88 -0
- package/dist-server/service/twin-subject/subject-query-shape.js.map +1 -0
- package/dist-server/service/twin-subject/twin-subject-backfill.d.ts +23 -0
- package/dist-server/service/twin-subject/twin-subject-backfill.js +159 -0
- package/dist-server/service/twin-subject/twin-subject-backfill.js.map +1 -0
- package/dist-server/service/twin-subject/twin-subject-event.d.ts +14 -0
- package/dist-server/service/twin-subject/twin-subject-event.js +111 -0
- package/dist-server/service/twin-subject/twin-subject-event.js.map +1 -0
- package/dist-server/service/twin-subject/twin-subject-fast-path.d.ts +39 -0
- package/dist-server/service/twin-subject/twin-subject-fast-path.js +295 -0
- package/dist-server/service/twin-subject/twin-subject-fast-path.js.map +1 -0
- package/dist-server/service/twin-subject/twin-subject-query.d.ts +27 -0
- package/dist-server/service/twin-subject/twin-subject-query.js +232 -0
- package/dist-server/service/twin-subject/twin-subject-query.js.map +1 -0
- package/dist-server/service/twin-subject/twin-subject-writer.d.ts +27 -0
- package/dist-server/service/twin-subject/twin-subject-writer.js +166 -0
- package/dist-server/service/twin-subject/twin-subject-writer.js.map +1 -0
- package/dist-server/service/twin-target/twin-target-resolver.js +1 -1
- package/dist-server/service/twin-target/twin-target-resolver.js.map +1 -1
- package/dist-shared/entity-delta.d.ts +1 -1
- package/dist-shared/entity-delta.js +4 -4
- package/dist-shared/entity-delta.js.map +1 -1
- package/dist-shared/touched-items.js +2 -2
- package/dist-shared/touched-items.js.map +1 -1
- package/dist-shared/twin-level.js +1 -1
- package/dist-shared/twin-level.js.map +1 -1
- package/package.json +7 -6
- package/server/engine/attention-digest.ts +9 -9
- package/server/engine/canonical-ingest.ts +202 -87
- package/server/engine/command-routing.ts +1 -1
- package/server/engine/declared-stimulus.ts +1 -1
- package/server/engine/energy-topology.ts +2 -2
- package/server/engine/ingest-dedupe.ts +149 -0
- package/server/engine/ingest-health.ts +171 -22
- package/server/engine/integration-probes.ts +7 -7
- package/server/engine/kpi-baseline.ts +4 -4
- package/server/engine/kpi-fold.ts +592 -41
- package/server/engine/kpi-query.ts +307 -48
- package/server/engine/live-feed-registry.ts +1 -1
- package/server/engine/load-meter.ts +1 -1
- package/server/engine/local-declarations.ts +15 -14
- package/server/engine/log.ts +1 -1
- package/server/engine/measured-estimator.ts +1 -1
- package/server/engine/measured-yield.ts +1 -1
- package/server/engine/model-basis.ts +2 -2
- package/server/engine/model-gap.ts +3 -3
- package/server/engine/model-vocabulary.ts +2 -2
- package/server/engine/oee-accumulator.ts +8 -6
- package/server/engine/property-effects.ts +2 -2
- package/server/engine/read-time.ts +77 -0
- package/server/engine/restart-policy.ts +1 -1
- package/server/engine/runtime-key.ts +1 -1
- package/server/engine/spec-coverage.ts +3 -3
- package/server/engine/stage-path.ts +5 -5
- package/server/engine/state-axes.ts +2 -2
- package/server/engine/travel-estimator.ts +2 -2
- package/server/engine/twin-engine.ts +906 -205
- package/server/engine/warm-start.ts +6 -6
- package/server/index.ts +1 -1
- package/server/migrations/1786000000000-RenameTwinInstanceBoardToModel.ts +1 -1
- package/server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.ts +3 -3
- package/server/migrations/1786200000000-CarryLiveFeedCursor.ts +2 -2
- package/server/migrations/1786300000000-IndexEventTypeByTime.ts +48 -0
- package/server/migrations/1786400000000-IndexEventCreatedAt.ts +38 -0
- package/server/migrations/1786500000000-CreateTwinSubjectEvents.ts +93 -0
- package/server/migrations/index.ts +7 -1
- package/server/routes.ts +73 -5
- package/server/service/index.ts +7 -1
- package/server/service/reference/control-routing.ts +1 -1
- package/server/service/reference/discovery-result.ts +3 -3
- package/server/service/reference/hook-contract.ts +103 -0
- package/server/service/reference/ingest-space.ts +4 -4
- package/server/service/reference/knob-defaults.ts +4 -4
- package/server/service/reference/reference-adapter.ts +145 -10
- package/server/service/reference/reference-hook.ts +145 -0
- package/server/service/reference/reference-live.ts +87 -15
- package/server/service/reference/reference-master.ts +51 -20
- package/server/service/reference/reference-resolver.ts +11 -11
- package/server/service/reference/template-registry.ts +1 -1
- package/server/service/reference/twin-reference.ts +3 -3
- package/server/service/twin-attention/twin-attention-query.ts +1 -1
- package/server/service/twin-audit/command-audit.ts +1 -1
- package/server/service/twin-audit/twin-audit-event.ts +2 -2
- package/server/service/twin-audit/twin-audit-query.ts +1 -1
- package/server/service/twin-backfill/backfill-period-facts.ts +168 -0
- package/server/service/twin-backfill/backfill-shape.ts +86 -0
- package/server/service/twin-backfill/index.ts +3 -0
- package/server/service/twin-backfill/twin-backfill-resolver.ts +35 -0
- package/server/service/twin-control/twin-control-mutation.ts +2 -2
- package/server/service/twin-event/twin-event-keys.ts +2 -2
- package/server/service/twin-event/twin-event-type.ts +18 -1
- package/server/service/twin-event/twin-event.ts +32 -6
- package/server/service/twin-forecast/forecast-metrics.ts +1 -1
- package/server/service/twin-forecast/gap-analytics.ts +1 -1
- package/server/service/twin-forecast/twin-forecast-query.ts +30 -11
- package/server/service/twin-ingest-window/twin-ingest-window-query.ts +1 -1
- package/server/service/twin-ingest-window/twin-ingest-window-writer.ts +1 -1
- package/server/service/twin-instance/twin-instance.ts +4 -4
- package/server/service/twin-journal/event-type-split-shape.ts +112 -0
- package/server/service/twin-journal/event-type-split.ts +144 -0
- package/server/service/twin-journal/journal-order.ts +20 -0
- package/server/service/twin-journal/twin-journal-query.ts +224 -20
- package/server/service/twin-lifecycle/domain-catalog.ts +2 -1
- package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +58 -4
- package/server/service/twin-metrics/twin-metrics-query.ts +14 -1
- package/server/service/twin-model/axis-journal-evidence.ts +1 -1
- package/server/service/twin-model/epcis-coverage.ts +34 -3
- package/server/service/twin-model/iec61850-coverage.ts +1 -1
- package/server/service/twin-model/isa95-coverage.ts +43 -16
- package/server/service/twin-model/item-ref.ts +2 -2
- package/server/service/twin-model/name-index.ts +2 -2
- package/server/service/twin-model/project-structure.ts +7 -6
- package/server/service/twin-model/twin-equipment.ts +16 -4
- package/server/service/twin-model/twin-lineage-query.ts +16 -5
- package/server/service/twin-model/twin-location.ts +1 -1
- package/server/service/twin-model/twin-model-item-query.ts +15 -15
- package/server/service/twin-model/twin-model-mutation.ts +4 -4
- package/server/service/twin-model/twin-model-query.ts +49 -28
- package/server/service/twin-model/twin-model-tree-query.ts +26 -2
- package/server/service/twin-model/twin-operation.ts +2 -2
- package/server/service/twin-space/move-space.ts +3 -3
- package/server/service/twin-space/representation-role.ts +15 -0
- package/server/service/twin-space/space-integrity.ts +1 -1
- package/server/service/twin-space/twin-space-representation.ts +37 -0
- package/server/service/twin-space/twin-space-resolver.ts +40 -8
- package/server/service/twin-structure/twin-structure.ts +3 -3
- package/server/service/twin-subject/event-subjects.ts +155 -0
- package/server/service/twin-subject/index.ts +10 -0
- package/server/service/twin-subject/item-resolver.ts +92 -0
- package/server/service/twin-subject/subject-query-shape.ts +84 -0
- package/server/service/twin-subject/twin-subject-backfill.ts +196 -0
- package/server/service/twin-subject/twin-subject-event.ts +101 -0
- package/server/service/twin-subject/twin-subject-fast-path.ts +346 -0
- package/server/service/twin-subject/twin-subject-query.ts +210 -0
- package/server/service/twin-subject/twin-subject-writer.ts +189 -0
- package/server/service/twin-target/twin-target-resolver.ts +1 -1
- package/shared/entity-delta.ts +4 -4
- package/shared/touched-items.ts +2 -2
- package/shared/twin-level.ts +1 -1
- package/test/adopt-structure-live.test.ts +3 -3
- package/test/aggregate-not-findone.test.ts +112 -0
- package/test/attention-digest.test.ts +6 -6
- package/test/axis-read.test.ts +6 -5
- package/test/backfill-shape.test.ts +89 -0
- package/test/boot-resume.test.ts +20 -7
- package/test/broadcast-cost-baseline.test.ts +2 -1
- package/test/broadcast-period.test.ts +2 -2
- package/test/broadcast-queue-overflow.test.ts +244 -0
- package/test/canonical-ingest-vocabularies.test.ts +259 -8
- package/test/canonical-quantity-door.test.ts +2 -2
- package/test/capability-mapping.test.ts +1 -1
- package/test/checkpoint-refuses-empty.test.ts +2 -2
- package/test/connector-capability-declaration.test.ts +88 -0
- package/test/contract-layer-guard.test.ts +48 -0
- package/test/control-capability.test.ts +1 -1
- package/test/cursor-stall-not-read-failure.test.ts +2 -2
- package/test/declaration-reaches-model.test.ts +7 -5
- package/test/declared-stimulus.test.ts +4 -4
- package/test/discovery-result.test.ts +2 -2
- package/test/entity-delta.test.ts +4 -4
- package/test/equipment-identity-reaches.test.ts +57 -0
- package/test/event-subjects.test.ts +147 -0
- package/test/event-time-column.test.ts +3 -3
- package/test/event-type-split.test.ts +155 -0
- package/test/fact-scope-wiring.test.ts +96 -0
- package/test/fold-gap-speaks.test.ts +53 -0
- package/test/forecast-metrics.test.ts +1 -1
- package/test/forecast-tuning.test.ts +1 -1
- package/test/generated-not-counted.test.ts +99 -0
- package/test/ingest-bench.test.ts +2 -1
- package/test/ingest-health-engine.test.ts +2 -2
- package/test/ingest-health-wiring.test.ts +7 -5
- package/test/ingest-health.test.ts +6 -6
- package/test/ingest-history.test.ts +2 -2
- package/test/ingest-idempotent.test.ts +132 -0
- package/test/ingest-space.test.ts +3 -3
- package/test/ingest-wiring-guard.test.ts +197 -0
- package/test/integration-probes.test.ts +3 -3
- package/test/integration-runner.test.ts +1 -1
- package/test/item-ref.test.ts +1 -1
- package/test/journal-order-axis.test.ts +80 -0
- package/test/journal-read-discipline.test.ts +16 -6
- package/test/journal-retention.test.ts +171 -29
- package/test/journal-sort-axis.test.ts +1 -1
- package/test/journal-write-door.test.ts +3 -3
- package/test/journal-write-trend.test.ts +1 -1
- package/test/kernel-kind-guard.test.ts +3 -3
- package/test/knob-defaults.test.ts +1 -1
- package/test/kpi-baseline-db.test.ts +2 -2
- package/test/kpi-fold.test.ts +266 -14
- package/test/kpi-query-bench.test.ts +3 -3
- package/test/live-cursor-wiring.test.ts +6 -6
- package/test/live-kernel-facts.test.ts +11 -11
- package/test/live-mirror-parity.test.ts +9 -6
- package/test/load-meter.test.ts +1 -1
- package/test/local-declarations.test.ts +13 -12
- package/test/log-stamp.test.ts +1 -1
- package/test/master-to-twin.test.ts +2 -2
- package/test/measured-yield.test.ts +2 -2
- package/test/merge-streams.test.ts +70 -0
- package/test/mirror-resumes-from-checkpoint.test.ts +13 -13
- package/test/model-basis.test.ts +2 -2
- package/test/model-gap.test.ts +4 -4
- package/test/model-vocabulary.test.ts +2 -2
- package/test/move-space.test.ts +1 -1
- package/test/oee-accumulator.test.ts +20 -5
- package/test/operational-vocabulary.test.ts +2 -2
- package/test/project-structure-db.test.ts +2 -2
- package/test/projection-reaches-screen.test.ts +7 -2
- package/test/projection-reads-declared.test.ts +109 -0
- package/test/property-effects.test.ts +10 -6
- package/test/read-failure-ledger.test.ts +106 -0
- package/test/read-failure-visible.test.ts +7 -7
- package/test/read-time.test.ts +118 -0
- package/test/reference-grounding.test.ts +1 -1
- package/test/reference-hook.test.ts +86 -0
- package/test/registry-key-guard.test.ts +1 -1
- package/test/rename-twin.test.ts +81 -0
- package/test/restart-policy.test.ts +3 -3
- package/test/resync-origin-site.test.ts +3 -3
- package/test/revision-axis.test.ts +14 -8
- package/test/runtime-key.test.ts +3 -3
- package/test/scale-twin-bench.test.ts +2 -1
- package/test/snapshot-freshness.test.ts +4 -4
- package/test/source-outcome-audit.test.ts +1 -1
- package/test/space-integrity.test.ts +1 -1
- package/test/space-representation.test.ts +66 -0
- package/test/space-scope-includes-stopped.test.ts +55 -0
- package/test/spec-coverage.test.ts +1 -1
- package/test/standard-coverage.test.ts +19 -21
- package/test/status-tally.test.ts +1 -1
- package/test/structure-revision-db.test.ts +7 -7
- package/test/subject-fast-path.test.ts +148 -0
- package/test/subject-read-model.test.ts +175 -0
- package/test/touched-items.test.ts +2 -2
- package/test/twin-audit.test.ts +1 -1
- package/test/twin-event-keys.test.ts +1 -1
- package/test/twin-model-item-db.test.ts +5 -5
- package/test/twin-model-tree-db.test.ts +35 -2
- package/test/twin-origin-resync.test.ts +1 -1
- package/test/vocabulary-guard.test.ts +1 -1
- package/test/warm-start-seam.test.ts +1 -1
- package/test/warm-start.test.ts +6 -6
- package/test/withheld-door.test.ts +2 -2
- package/test/yield-loop.test.ts +4 -4
- package/tsconfig.shared.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* 업무 KPI 폴드 — **이벤트 저널을
|
|
2
|
+
* 업무 KPI 폴드 — **이벤트 저널을 계산해** 시간창 지표를 만든다(순수).
|
|
3
3
|
*
|
|
4
4
|
* ── 왜 필요한가 ─────────────────────────────────────────────────────────────
|
|
5
5
|
* 지금 트윈이 답할 수 있는 것은 "이 순간의 값"(점유 92%)과 "인프라 부하"(초당 이벤트 수)뿐이다.
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
*
|
|
9
9
|
* ── 원료는 이미 있다 ────────────────────────────────────────────────────────
|
|
10
10
|
* 저널(`TwinEvent`)에 `task.status`·`order.status`·`equipment.status` 전이가 `eventTime`(ISO)과 함께
|
|
11
|
-
* append-only 로 쌓인다. 그래서 새 계측을 심을 필요가 없다 —
|
|
12
|
-
* 같은 모양의 선례가 이 폴더에 있다(`oee-accumulator.ts` — equipment 전이를
|
|
11
|
+
* append-only 로 쌓인다. 그래서 새 계측을 심을 필요가 없다 — **계산만 하면 된다.**
|
|
12
|
+
* 같은 모양의 선례가 이 폴더에 있다(`oee-accumulator.ts` — equipment 전이를 계산해 OEE 카운터 생산).
|
|
13
13
|
*
|
|
14
14
|
* ── 이 파일이 하지 않는 것 ──────────────────────────────────────────────────
|
|
15
15
|
* · 목표와 비교하지 않는다. "95% 목표 대비" 는 목표를 저장하는 별개 층(경영 KPI)의 몫이고,
|
|
@@ -23,27 +23,16 @@
|
|
|
23
23
|
* 짝맞춤은 **커널이 소유한다**(`foldTaskRecords`). 완료는 작업당 하나·나중 것이 사실, 착수는 처음 것,
|
|
24
24
|
* 종류·오더는 어느 전이에서든 — 그 규칙은 커널 상태 기계에서 나오는 지식이다. 여기서 다시 적었더니
|
|
25
25
|
* 공정 타임라인과 **다른 답**을 냈다(재전송된 완료를 두 건으로 세어 처리량·점유가 부풀려졌다).
|
|
26
|
-
* 이 파일은 이제 "
|
|
26
|
+
* 이 파일은 이제 "계산한 기록 → 창 지표" 만 한다.
|
|
27
27
|
*/
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
electricityCost,
|
|
32
|
-
energyIntensity,
|
|
33
|
-
energyOfWindows,
|
|
34
|
-
foldTaskRecords,
|
|
35
|
-
isOrderTerminal,
|
|
36
|
-
activeShiftAt,
|
|
37
|
-
type ElectricityCost,
|
|
38
|
-
type TariffDeclaration,
|
|
39
|
-
type TaskFacets,
|
|
40
|
-
type WorkCalendarEntry
|
|
41
|
-
} from '@operato/twin-kernel'
|
|
28
|
+
import { attributeEnergy, electricityCost, energyIntensity, energyOfWindows, foldTaskRecords, type ElectricityCost, type TariffDeclaration, type TaskFacets } from '@operato/twin-kernel'
|
|
29
|
+
import { type WorkCalendarEntry } from '@operato/ops-contract'
|
|
30
|
+
import { ENERGY_EVENT, isOrderTerminal, activeShiftAt } from '@operato/ops-contract'
|
|
42
31
|
|
|
43
32
|
/** 저널 한 줄 — 필요한 것만(엔티티·typeorm 비의존). */
|
|
44
33
|
export interface KpiEvent {
|
|
45
34
|
eventType?: string
|
|
46
|
-
/** ISO 시각. 파싱 불가하면 그 줄은 버린다(
|
|
35
|
+
/** ISO 시각. 파싱 불가하면 그 줄은 버린다(알리지 않고 0 으로 세지 않는다). */
|
|
47
36
|
eventTime?: string
|
|
48
37
|
payload?: any
|
|
49
38
|
}
|
|
@@ -58,7 +47,7 @@ export interface DurationStats {
|
|
|
58
47
|
/** 짝이 맞아 계산된 건수. */
|
|
59
48
|
count: number
|
|
60
49
|
/**
|
|
61
|
-
* 하위 꼬리 — **분포를 양쪽에서
|
|
50
|
+
* 하위 꼬리 — **분포를 양쪽에서 계산하지하기 위해** 낸다.
|
|
62
51
|
* p50·p90 만 있으면 아래쪽 퍼짐을 위쪽에서 거울처럼 베껴 쓸 수밖에 없다(발명). 관측이 말하게 한다.
|
|
63
52
|
*/
|
|
64
53
|
p10Ms: number
|
|
@@ -94,7 +83,7 @@ export interface KpiGroup {
|
|
|
94
83
|
* **품질 판정이 있었던 건수만** 센다 (2026-08-19).
|
|
95
84
|
*
|
|
96
85
|
* 판정이 없는 작업(이동·체류·중간 스테이션)은 어느 쪽에도 세지 않는다 — 모름을 양품으로 세면 양품률이
|
|
97
|
-
*
|
|
86
|
+
* 알리지 않고 100% 로 올라간다. 그래서 `good + scrap` 은 `tasks` 보다 작을 수 있고, 그것이 정상이다.
|
|
98
87
|
*
|
|
99
88
|
* 판정이 하나도 없으면 **필드를 만들지 않는다**(0/0 을 「양품률 0%」로 읽히게 두지 않는다).
|
|
100
89
|
*/
|
|
@@ -135,9 +124,137 @@ export interface KpiResult {
|
|
|
135
124
|
*/
|
|
136
125
|
peakGrossKW?: number
|
|
137
126
|
shavedKW?: number
|
|
127
|
+
/**
|
|
128
|
+
* 이 창의 **발전량** — 커널이 마감한 기간 사실(`energy.generation.period`)에서 온다.
|
|
129
|
+
*
|
|
130
|
+
* 부하(`kWh`)와 더하지 않는다: 발전은 설비가 만든 양이고 부하는 계량 지점을 지난 양이다. 두 수를
|
|
131
|
+
* 더하면 어느 쪽도 아닌 숫자가 된다.
|
|
132
|
+
*
|
|
133
|
+
* 성과지표(ISO 50001 EnPI)와 태양광 성능비가 이 값을 분자로 쓴다. 발전 사실이 없으면 `undefined`
|
|
134
|
+
* 다(0 이 아니다 — 「발전이 없다」와 「발전 설비가 없다」는 다르다).
|
|
135
|
+
*/
|
|
136
|
+
generated?: {
|
|
137
|
+
/** 창에 온전히 든 기간들의 합(kWh). */
|
|
138
|
+
kWh: number
|
|
139
|
+
/** 센 기간 수. */
|
|
140
|
+
counted: number
|
|
141
|
+
/**
|
|
142
|
+
* 그중 **관측이 기간을 다 덮지 못한** 기간 수.
|
|
143
|
+
*
|
|
144
|
+
* 커널이 기간마다 관측 구간을 함께 낸다. 마지막 표본이 기간 끝보다 이르면 그만큼 재지 않은
|
|
145
|
+
* 것이고, 그 수를 감추면 화면이 덜 잰 합을 온전한 합으로 말한다.
|
|
146
|
+
*/
|
|
147
|
+
partial: number
|
|
148
|
+
/** 설비별 몫 — 이것은 **측정**이다(배분이 아니다). 각 설비의 적산에서 왔다. */
|
|
149
|
+
byEquipment: { id: string; kWh: number }[]
|
|
150
|
+
/**
|
|
151
|
+
* **이 창에 다 들어오지 않아 더하지 못한 기간** — 조용히 버리지 않는다.
|
|
152
|
+
*
|
|
153
|
+
* 더하는 것은 창에 온전히 든 기간뿐이다(걸친 기간을 더하면 다른 날 값이 이 창의 값인 척한다).
|
|
154
|
+
* 그런데 하루 단위 기간은 「지금부터 24시간 전」 창에 온전히 드는 일이 거의 없다 — 창이 한낮에서
|
|
155
|
+
* 시작하기 때문이다. 그래서 어제 발전량이 저널에 멀쩡히 있는데 화면이 비었고, 사용자는 그것을
|
|
156
|
+
* 「발전량이 없다」로 읽었다(2026-08-30 실측).
|
|
157
|
+
*
|
|
158
|
+
* 이 값이 있으면 화면은 **「이 구간에 다 들어오지 않은 기간이 있다」**를 말해야 한다.
|
|
159
|
+
*/
|
|
160
|
+
notCounted?: { periods: number; kWh: number; earliestStart: string; latestEnd: string }
|
|
161
|
+
/**
|
|
162
|
+
* 이용률 — 만든 양 ÷ (정격 × 그 기간의 시간).
|
|
163
|
+
*
|
|
164
|
+
* **어느 정격으로 나눴는지를 함께 낸다.** 변환기를 낀 발전은 직류 정격이 교류보다 크고,
|
|
165
|
+
* 직류로 나눠 보여 주는 시스템이 실제로 있다. 밝히지 않으면 두 화면의 수가 두 배 달라도
|
|
166
|
+
* 왜 다른지 말할 수 없다.
|
|
167
|
+
*
|
|
168
|
+
* 창 전체가 아니라 **센 기간들의 길이**로 나눈다 — 하루치 하나만 센 24시간 창에서 창 길이로
|
|
169
|
+
* 나누면 그 하루가 창 전체에 걸쳐 있었던 것처럼 작아진다.
|
|
170
|
+
*/
|
|
171
|
+
capacityFactor?: { value: number; ratedKW: number; basis: 'ac' | 'dc'; from: 'equipment' | 'location'; hours: number }
|
|
172
|
+
/**
|
|
173
|
+
* 낸 것의 값 — 기간마다 그때의 단가로 매긴다.
|
|
174
|
+
*
|
|
175
|
+
* **모든 기간에 단가가 있을 때만** 금액을 만든다. 일부만 매기고 더하면 그 합이 창 전체의 값인
|
|
176
|
+
* 척한다 — 못 매긴 기간 수를 함께 낸다.
|
|
177
|
+
*/
|
|
178
|
+
revenue?: { amount: number; currency: string; priced: number; unpriced: number }
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* `kWh` 가 어느 회계에서 왔나.
|
|
182
|
+
*
|
|
183
|
+
* 'demand-window' 우리가 표본에서 마감한 구간 (계량이 오는 현장)
|
|
184
|
+
* 'usage-period' 밖에서 마감되어 온 구간 (계량 표본이 없는 현장)
|
|
185
|
+
*
|
|
186
|
+
* 둘을 더하지 않는다 — 같은 전기를 두 번 센다. 둘 다 있으면 계량 쪽을 쓰고 그 사실을 낸다.
|
|
187
|
+
*/
|
|
188
|
+
kWhBasis?: 'demand-window' | 'usage-period'
|
|
189
|
+
/**
|
|
190
|
+
* 기본요금을 매긴 **요금적용전력과 그 근거**.
|
|
191
|
+
*
|
|
192
|
+
* 이 값은 매달 다시 정해진다. 선언에 박아 두면 사람이 갱신해야 하고, 안 하면 낡은 값으로
|
|
193
|
+
* 기본요금이 조용히 틀린다. 그래서 **청구서가 먼저다** — 원본이 매일 실어 보낸다.
|
|
194
|
+
*
|
|
195
|
+
* 'bill' 이 창을 덮는 청구서가 말한 값
|
|
196
|
+
* 'carried-forward' 가장 최근 청구서의 값을 이어 쓴다(요금 제도가 실제로 그렇게 군다)
|
|
197
|
+
* 'declaration' 청구가 오지 않는 현장 — 모델의 선언
|
|
198
|
+
*
|
|
199
|
+
* `declaredKW` 와 다르면 **선언이 낡았다**(`stale`). 그 사실을 감추면 아무도 다시 읽지 않는다.
|
|
200
|
+
*/
|
|
201
|
+
billingDemand?: {
|
|
202
|
+
kW: number
|
|
203
|
+
from: 'tariff-basis' | 'bill' | 'carried-forward' | 'declaration'
|
|
204
|
+
declaredKW?: number
|
|
205
|
+
stale?: boolean
|
|
206
|
+
/**
|
|
207
|
+
* 이 창에서 관측한 최고 수요가 그 기준을 넘었다 — **다음 주기의 기본요금이 오른다.**
|
|
208
|
+
*
|
|
209
|
+
* 계약 초과와 다른 값이다. 계약 초과는 지금 줄이면 되고, 이것은 이미 정해진 결과다.
|
|
210
|
+
*
|
|
211
|
+
* `observedTo` 는 이 최고값이 언제까지의 것인지다. 최고 수요는 구간이 마감되어야 오므로
|
|
212
|
+
* 넘은 것을 뒤늦게 안다 — 그 한계를 화면이 말할 수 있어야 한다.
|
|
213
|
+
*/
|
|
214
|
+
exceeded?: { peakKW: number; overKW: number; observedTo: string }
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* 밖에서 마감되어 온 사용 구간 — 계량 표본이 없는 현장의 유일한 근거다.
|
|
218
|
+
*
|
|
219
|
+
* `basis` 별 건수를 함께 낸다: 같은 kWh 라도 계량기가 잰 것과 공급자 화면에서 긁어온 것은 무게가
|
|
220
|
+
* 다르고, 실제로 긁어온 값이 망가져 있는 것을 본 적이 있다.
|
|
221
|
+
*/
|
|
222
|
+
usage?: {
|
|
223
|
+
kWh: number
|
|
224
|
+
counted: number
|
|
225
|
+
/** 구간마다 제 단가로 매긴 사용량 요금 — 모든 구간이 단가를 실었을 때만. */
|
|
226
|
+
charge?: number
|
|
227
|
+
currency?: string
|
|
228
|
+
/** 단가가 없어 요금을 매기지 못한 구간 수 — 조용히 빼지 않는다. */
|
|
229
|
+
unpriced: number
|
|
230
|
+
byBasis: { basis: string; counted: number }[]
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* 이 창에 온전히 든 **청구서** — 공급자가 확정한 금액.
|
|
234
|
+
*
|
|
235
|
+
* 우리 계산을 덮지 않는다. 창이 청구 주기와 정확히 같을 때만 견줄 수 있으므로 그 사실을
|
|
236
|
+
* `matchesWindow` 로 함께 낸다 — 다른 창의 두 수를 빼면 그 차이는 아무것도 뜻하지 않는다.
|
|
237
|
+
*/
|
|
238
|
+
bill?: {
|
|
239
|
+
from: string
|
|
240
|
+
to: string
|
|
241
|
+
total?: number
|
|
242
|
+
energyCharge?: number
|
|
243
|
+
demandCharge?: number
|
|
244
|
+
currency?: string
|
|
245
|
+
billingDemandKW?: number
|
|
246
|
+
matchesWindow: boolean
|
|
247
|
+
/**
|
|
248
|
+
* 창이 같을 때, **우리 계산에서 청구액을 뺀 값** — 0 이 아니면 우리 계산이 틀렸다는 뜻이다.
|
|
249
|
+
*
|
|
250
|
+
* 항목별로도 낸다(2026-08-29). 총액만 견주면 원본이 총액을 주지 않는 청구서에서는 비교가
|
|
251
|
+
* 영영 나오지 않는다 — 실제로 첫 청구서에 기본요금만 있었다. 양쪽에 있는 항목만 견준다.
|
|
252
|
+
*/
|
|
253
|
+
diff?: { total?: number; demandCharge?: number; energyCharge?: number }
|
|
254
|
+
}
|
|
138
255
|
/** 파생의 근거 — 구간 평균부하 × 길이(`mean-kw`). */
|
|
139
256
|
basis: string
|
|
140
|
-
/** 센 구간 수 · 못 센 구간 수(계측이 끊긴 구간) —
|
|
257
|
+
/** 센 구간 수 · 못 센 구간 수(계측이 끊긴 구간) — 알리지 않고 자르지 않는다. */
|
|
141
258
|
counted: number
|
|
142
259
|
skipped: number
|
|
143
260
|
/** 완료 오더당 전력량 — 답할 수 없으면 `null` 과 이유. */
|
|
@@ -157,7 +274,7 @@ export interface KpiResult {
|
|
|
157
274
|
* 붙지 않는 부하도 그 비율에 섞인다 — 화면이 그 문장을 함께 말해야 한다.
|
|
158
275
|
*/
|
|
159
276
|
byFeeder?: {
|
|
160
|
-
/** 자리(분기·수전) id. 자리를 모르는 자원은 `unknown` 으로 모인다(
|
|
277
|
+
/** 자리(분기·수전) id. 자리를 모르는 자원은 `unknown` 으로 모인다(알리지 않고 버리지 않는다). */
|
|
161
278
|
key: string
|
|
162
279
|
kWh: number
|
|
163
280
|
/**
|
|
@@ -186,7 +303,7 @@ export interface KpiResult {
|
|
|
186
303
|
peakKW?: number
|
|
187
304
|
}[]
|
|
188
305
|
/**
|
|
189
|
-
* 나누지 못한 양과 그 이유 —
|
|
306
|
+
* 나누지 못한 양과 그 이유 — **알리지 않고 0 으로 만들지 않는다.**
|
|
190
307
|
* `no-weights` = 그 창에 돌아간 자원이 없다(가동시간이 전부 0). 그러면 총량은 그대로 남는다.
|
|
191
308
|
*/
|
|
192
309
|
unattributedKWh?: number
|
|
@@ -222,7 +339,7 @@ export interface KpiResult {
|
|
|
222
339
|
utilization: { resourceId: string; busyMs: number; ratio: number }[]
|
|
223
340
|
/**
|
|
224
341
|
* **셈에서 빠진 것** — 완료는 창 안이지만 시작 이벤트를 못 찾은 건수.
|
|
225
|
-
* 조회 구간이 짧아 시작이 잘렸거나 저널이 유실된 경우다. 이 값을 숨기면 평균이
|
|
342
|
+
* 조회 구간이 짧아 시작이 잘렸거나 저널이 유실된 경우다. 이 값을 숨기면 평균이 알리지 않고 거짓이 된다.
|
|
226
343
|
*/
|
|
227
344
|
unpaired: { leadTime: number; workTime: number }
|
|
228
345
|
/**
|
|
@@ -372,6 +489,266 @@ function feederShares(
|
|
|
372
489
|
* 분기 계량 합이 뿌리보다 크면(회계가 다른 두 계량기) **그 사실을 낸다** — 값을 깎아 맞추면 계량을
|
|
373
490
|
* 우리가 고쳐 쓰는 것이다.
|
|
374
491
|
*/
|
|
492
|
+
/**
|
|
493
|
+
* 창의 **발전량** — 커널이 마감한 기간 사실을 합한다.
|
|
494
|
+
*
|
|
495
|
+
* ── 왜 합이 전부인가 ───────────────────────────────────────────────────────
|
|
496
|
+
* 기간 발전량은 커널이 이미 확정한 사실이다(적산의 종류마다 다른 규칙으로 얻는다). 여기서 다시
|
|
497
|
+
* 계산하면 규칙이 두 벌이 되고, 그때부터 두 화면이 다른 발전량을 말한다. 이 함수가 하는 일은
|
|
498
|
+
* **창에 든 것을 고르고, 설비별로 묶고, 근거가 얼마나 덮였는지 세는 것**이다.
|
|
499
|
+
*
|
|
500
|
+
* ── 걸친 기간은 세지 않는다 ─────────────────────────────────────────────────
|
|
501
|
+
* 구간 마감과 같은 규칙이다. 하루의 절반이 창에 들었다고 절반을 세면 그것은 관측이 아니라 우리가 나눈
|
|
502
|
+
* 값이고, 합이 창을 넘는다.
|
|
503
|
+
*
|
|
504
|
+
* ── 덜 덮인 기간을 센다 ─────────────────────────────────────────────────────
|
|
505
|
+
* 기간의 끝이 자정인데 마지막 표본이 저녁이었으면 그 사이는 재지 않은 것이다. 그 수를 함께 내야 화면이
|
|
506
|
+
* 「이 합은 이만큼 덜 잰 것」이라고 말할 수 있다. 얼마나 어긋나면 문제인지는 현장이 정한다.
|
|
507
|
+
*/
|
|
508
|
+
function generatedOfPeriods(
|
|
509
|
+
periods: readonly {
|
|
510
|
+
equipmentId: string
|
|
511
|
+
kWh: number
|
|
512
|
+
startMs: number
|
|
513
|
+
endMs: number
|
|
514
|
+
observedFromMs: number
|
|
515
|
+
observedToMs: number
|
|
516
|
+
}[],
|
|
517
|
+
range: { startMs: number; endMs: number },
|
|
518
|
+
/** 이용률의 분모 — 없으면 이용률을 내지 않는다(0 이나 짐작을 만들지 않는다). */
|
|
519
|
+
rated?: { kW: number; basis: 'ac' | 'dc'; from: 'equipment' | 'location' },
|
|
520
|
+
/** 기간마다 적용할 단가 — 기간을 덮는 것을 쓴다. 없으면 금액을 만들지 않는다. */
|
|
521
|
+
prices?: readonly { startMs: number; endMs: number; unitPrice: number; currency: string }[]
|
|
522
|
+
): Partial<NonNullable<KpiResult['energy']>> {
|
|
523
|
+
const inside = periods.filter(g => g.startMs >= range.startMs && g.endMs <= range.endMs)
|
|
524
|
+
|
|
525
|
+
/*
|
|
526
|
+
* ── **걸친 기간을 조용히 버리지 않는다** (2026-08-30) ──────────────────────
|
|
527
|
+
*
|
|
528
|
+
* 더하는 것은 창에 **온전히 들어온 기간**뿐이다. 그것이 맞는 셈이다 — 걸친 기간을 더하면 다른 날의
|
|
529
|
+
* 값이 이 창의 값인 척한다.
|
|
530
|
+
*
|
|
531
|
+
* 그런데 버린 것을 말하지 않았다. 하루 단위 기간은 「지금부터 24시간 전」 창에 온전히 들어오는 일이
|
|
532
|
+
* 거의 없다(창이 한낮에서 시작하므로). 그래서 어제 발전량이 저널에 멀쩡히 있는데 화면이 비었고,
|
|
533
|
+
* 사용자는 그것을 **「발전량이 없다」**로 읽었다. 실측으로 그 일이 났다.
|
|
534
|
+
*
|
|
535
|
+
* 이제 걸친 것을 세어 함께 낸다. 화면이 「이 창에 다 들어오지 않은 기간이 있다」를 말할 수 있다.
|
|
536
|
+
*/
|
|
537
|
+
const straddling = periods.filter(
|
|
538
|
+
g => !(g.startMs >= range.startMs && g.endMs <= range.endMs) && g.endMs > range.startMs && g.startMs < range.endMs
|
|
539
|
+
)
|
|
540
|
+
const notCounted = straddling.length
|
|
541
|
+
? {
|
|
542
|
+
periods: straddling.length,
|
|
543
|
+
kWh: straddling.reduce((a, g) => a + g.kWh, 0),
|
|
544
|
+
earliestStart: new Date(Math.min(...straddling.map(g => g.startMs))).toISOString(),
|
|
545
|
+
latestEnd: new Date(Math.max(...straddling.map(g => g.endMs))).toISOString()
|
|
546
|
+
}
|
|
547
|
+
: undefined
|
|
548
|
+
|
|
549
|
+
if (!inside.length) {
|
|
550
|
+
/* 온전히 들어온 것이 없다 — 그래도 걸친 것이 있으면 그 사실을 낸다(빈 값과 「없음」은 다르다). */
|
|
551
|
+
return notCounted ? { generated: { kWh: 0, counted: 0, partial: 0, byEquipment: [], notCounted } } : {}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const byEquipment = new Map<string, number>()
|
|
555
|
+
let partial = 0
|
|
556
|
+
for (const g of inside) {
|
|
557
|
+
byEquipment.set(g.equipmentId, (byEquipment.get(g.equipmentId) ?? 0) + g.kWh)
|
|
558
|
+
const coveredFrom = Number.isFinite(g.observedFromMs) ? g.observedFromMs <= g.startMs : false
|
|
559
|
+
const coveredTo = Number.isFinite(g.observedToMs) ? g.observedToMs >= g.endMs : false
|
|
560
|
+
if (!coveredFrom || !coveredTo) partial++
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const kWh = inside.reduce((a, g) => a + g.kWh, 0)
|
|
564
|
+
/*
|
|
565
|
+
* 이용률의 분모는 **센 기간들의 길이**다. 창 길이로 나누면, 하루치 하나만 센 24시간 창에서 그 하루가
|
|
566
|
+
* 창 전체에 걸쳐 있었던 것처럼 작아진다.
|
|
567
|
+
*/
|
|
568
|
+
const hours = inside.reduce((a, g) => a + (g.endMs - g.startMs) / 3_600_000, 0)
|
|
569
|
+
const capacityFactor =
|
|
570
|
+
rated && rated.kW > 0 && hours > 0
|
|
571
|
+
? { value: kWh / (rated.kW * hours), ratedKW: rated.kW, basis: rated.basis, from: rated.from, hours }
|
|
572
|
+
: undefined
|
|
573
|
+
|
|
574
|
+
/*
|
|
575
|
+
* 낸 것의 값 — 기간마다 **그 기간을 덮는 단가**로 매긴다. 걸친 단가는 쓰지 않는다: 하루 단가로
|
|
576
|
+
* 이틀치를 매기면 그 절반은 다른 날의 값이 된다.
|
|
577
|
+
*
|
|
578
|
+
* 모든 기간에 단가가 있을 때만 금액을 만든다. 일부만 매기고 더하면 그 합이 창 전체의 값인 척한다.
|
|
579
|
+
*/
|
|
580
|
+
let amount = 0
|
|
581
|
+
let priced = 0
|
|
582
|
+
let currency: string | undefined
|
|
583
|
+
for (const g of inside) {
|
|
584
|
+
const p = (prices ?? []).find(x => x.startMs <= g.startMs && x.endMs >= g.endMs)
|
|
585
|
+
if (!p) continue
|
|
586
|
+
amount += g.kWh * p.unitPrice
|
|
587
|
+
priced++
|
|
588
|
+
currency = currency ?? p.currency
|
|
589
|
+
}
|
|
590
|
+
const unpriced = inside.length - priced
|
|
591
|
+
const revenue = priced > 0 && unpriced === 0 && currency ? { amount, currency, priced, unpriced } : undefined
|
|
592
|
+
|
|
593
|
+
return {
|
|
594
|
+
generated: {
|
|
595
|
+
kWh,
|
|
596
|
+
counted: inside.length,
|
|
597
|
+
partial,
|
|
598
|
+
byEquipment: [...byEquipment.entries()]
|
|
599
|
+
.map(([id, kWh]) => ({ id, kWh }))
|
|
600
|
+
.sort((a, b) => b.kWh - a.kWh),
|
|
601
|
+
...(capacityFactor ? { capacityFactor } : {}),
|
|
602
|
+
...(revenue ? { revenue } : {}),
|
|
603
|
+
/* 이 창에 다 들어오지 않아 더하지 못한 기간 — 있으면 화면이 그것을 말한다. */
|
|
604
|
+
...(notCounted ? { notCounted } : {})
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* 밖에서 마감되어 온 사용 구간을 합한다 — **구간 마감과 더하지 않는다.**
|
|
611
|
+
*
|
|
612
|
+
* 두 회계가 같은 전기를 말한다. 더하면 두 번 세고, 둘 다 있는 현장에서는 계량 쪽이 강한 근거이므로
|
|
613
|
+
* 그쪽을 쓴다. 여기서는 이쪽 몫만 계산하고, 무엇을 쓸지는 부르는 쪽이 정한다.
|
|
614
|
+
*
|
|
615
|
+
* 단가는 **모든 구간이 실었을 때만** 요금을 만든다. 일부만 실었는데 있는 것만 더하면 그 합이 창의
|
|
616
|
+
* 요금인 척한다 — 못 매긴 구간 수를 함께 낸다.
|
|
617
|
+
*/
|
|
618
|
+
function usageOfPeriods(
|
|
619
|
+
periods: readonly { meterId: string; kWh: number; startMs: number; endMs: number; unitPrice: number; currency?: string; basis: string }[],
|
|
620
|
+
range: { startMs: number; endMs: number }
|
|
621
|
+
): NonNullable<KpiResult['energy']>['usage'] | undefined {
|
|
622
|
+
const inside = periods.filter(u => u.startMs >= range.startMs && u.endMs <= range.endMs)
|
|
623
|
+
if (!inside.length) return undefined
|
|
624
|
+
|
|
625
|
+
const byBasis = new Map<string, number>()
|
|
626
|
+
let kWh = 0
|
|
627
|
+
let charge = 0
|
|
628
|
+
let unpriced = 0
|
|
629
|
+
let currency: string | undefined
|
|
630
|
+
for (const u of inside) {
|
|
631
|
+
kWh += u.kWh
|
|
632
|
+
byBasis.set(u.basis, (byBasis.get(u.basis) ?? 0) + 1)
|
|
633
|
+
if (Number.isFinite(u.unitPrice)) {
|
|
634
|
+
charge += u.kWh * u.unitPrice
|
|
635
|
+
if (!currency) currency = u.currency
|
|
636
|
+
} else unpriced++
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return {
|
|
640
|
+
kWh,
|
|
641
|
+
counted: inside.length,
|
|
642
|
+
...(unpriced === 0 && currency ? { charge, currency } : {}),
|
|
643
|
+
unpriced,
|
|
644
|
+
byBasis: [...byBasis.entries()].map(([basis, counted]) => ({ basis, counted })).sort((a, b) => b.counted - a.counted)
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* **요금적용전력을 고른다 — 청구서가 먼저다.**
|
|
650
|
+
*
|
|
651
|
+
* 순서에 뜻이 있다. 청구서는 그 기간에 실제로 매겨진 값이고, 선언은 사람이 옮겨 적은 값이다.
|
|
652
|
+
* 그 값이 매달 바뀌므로 옮겨 적은 것은 곧 낡는다.
|
|
653
|
+
*/
|
|
654
|
+
function billingDemandOf(
|
|
655
|
+
billsInWindow: readonly { endMs: number; billingDemandKW: number }[],
|
|
656
|
+
tariffBasis: { billingDemandKW?: number } | undefined,
|
|
657
|
+
lastBill: { billingDemandKW?: number } | undefined,
|
|
658
|
+
declaredKW: number | undefined,
|
|
659
|
+
/** 이 창에서 관측한 최고 수요와 그 값이 언제까지의 것인지 — 사용 구간의 `maxKW` 에서 온다. */
|
|
660
|
+
observedPeak?: { kW: number; toMs: number }
|
|
661
|
+
): NonNullable<KpiResult['energy']>['billingDemand'] | undefined {
|
|
662
|
+
const declared = Number.isFinite(Number(declaredKW)) ? Number(declaredKW) : undefined
|
|
663
|
+
const withValue = billsInWindow.filter(b => Number.isFinite(b.billingDemandKW))
|
|
664
|
+
const fromBill = withValue.length ? withValue.reduce((a, x) => (x.endMs > a.endMs ? x : a)).billingDemandKW : undefined
|
|
665
|
+
const carried = Number.isFinite(Number(lastBill?.billingDemandKW)) ? Number(lastBill?.billingDemandKW) : undefined
|
|
666
|
+
/* 이 주기에 적용되는 기준이 가장 먼저다 — 청구서는 끝난 기간의 것이다. */
|
|
667
|
+
const fromBasis = Number.isFinite(Number(tariffBasis?.billingDemandKW)) ? Number(tariffBasis?.billingDemandKW) : undefined
|
|
668
|
+
|
|
669
|
+
const picked =
|
|
670
|
+
fromBasis !== undefined
|
|
671
|
+
? { kW: fromBasis, from: 'tariff-basis' as const }
|
|
672
|
+
: fromBill !== undefined
|
|
673
|
+
? { kW: fromBill, from: 'bill' as const }
|
|
674
|
+
: carried !== undefined
|
|
675
|
+
? { kW: carried, from: 'carried-forward' as const }
|
|
676
|
+
: declared !== undefined
|
|
677
|
+
? { kW: declared, from: 'declaration' as const }
|
|
678
|
+
: undefined
|
|
679
|
+
if (!picked) return undefined
|
|
680
|
+
|
|
681
|
+
/* 선언이 있는데 청구서와 다르면 그 사실을 낸다 — 낡은 선언은 스스로 말해야 한다. */
|
|
682
|
+
const stale = picked.from !== 'declaration' && declared !== undefined && declared !== picked.kW
|
|
683
|
+
const exceeded =
|
|
684
|
+
observedPeak && observedPeak.kW > picked.kW
|
|
685
|
+
? {
|
|
686
|
+
peakKW: observedPeak.kW,
|
|
687
|
+
overKW: observedPeak.kW - picked.kW,
|
|
688
|
+
observedTo: new Date(observedPeak.toMs).toISOString()
|
|
689
|
+
}
|
|
690
|
+
: undefined
|
|
691
|
+
|
|
692
|
+
return {
|
|
693
|
+
...picked,
|
|
694
|
+
...(declared !== undefined ? { declaredKW: declared } : {}),
|
|
695
|
+
...(stale ? { stale: true } : {}),
|
|
696
|
+
...(exceeded ? { exceeded } : {})
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* 창에 온전히 든 청구서 하나 — 가장 늦게 끝난 것.
|
|
702
|
+
*
|
|
703
|
+
* **창이 청구 주기와 같을 때만 견준다.** 한 달 청구서를 하루 창의 값과 빼면 그 차이는 아무것도 뜻하지
|
|
704
|
+
* 않는다. 그래서 같은지를 값과 함께 낸다.
|
|
705
|
+
*/
|
|
706
|
+
function billOfWindow(
|
|
707
|
+
bills: readonly {
|
|
708
|
+
from: string
|
|
709
|
+
to: string
|
|
710
|
+
startMs: number
|
|
711
|
+
endMs: number
|
|
712
|
+
total: number
|
|
713
|
+
energyCharge: number
|
|
714
|
+
demandCharge: number
|
|
715
|
+
currency?: string
|
|
716
|
+
billingDemandKW: number
|
|
717
|
+
}[],
|
|
718
|
+
range: { startMs: number; endMs: number },
|
|
719
|
+
ours?: { total?: number; demandCharge?: number; energyCharge?: number }
|
|
720
|
+
): NonNullable<KpiResult['energy']>['bill'] | undefined {
|
|
721
|
+
const inside = bills.filter(b => b.startMs >= range.startMs && b.endMs <= range.endMs)
|
|
722
|
+
if (!inside.length) return undefined
|
|
723
|
+
const b = inside.reduce((a, x) => (x.endMs > a.endMs ? x : a))
|
|
724
|
+
const matchesWindow = b.startMs === range.startMs && b.endMs === range.endMs
|
|
725
|
+
/* 양쪽에 있는 항목만 견준다 — 한쪽이 없는 것을 0 으로 두면 그 차이가 통째로 어긋남처럼 보인다. */
|
|
726
|
+
const gap = (mine: unknown, billed: number): number | undefined =>
|
|
727
|
+
Number.isFinite(Number(mine)) && Number.isFinite(billed) ? Number(mine) - billed : undefined
|
|
728
|
+
const diff = matchesWindow
|
|
729
|
+
? {
|
|
730
|
+
...(gap(ours?.total, b.total) !== undefined ? { total: gap(ours?.total, b.total) as number } : {}),
|
|
731
|
+
...(gap(ours?.demandCharge, b.demandCharge) !== undefined
|
|
732
|
+
? { demandCharge: gap(ours?.demandCharge, b.demandCharge) as number }
|
|
733
|
+
: {}),
|
|
734
|
+
...(gap(ours?.energyCharge, b.energyCharge) !== undefined
|
|
735
|
+
? { energyCharge: gap(ours?.energyCharge, b.energyCharge) as number }
|
|
736
|
+
: {})
|
|
737
|
+
}
|
|
738
|
+
: {}
|
|
739
|
+
return {
|
|
740
|
+
from: b.from,
|
|
741
|
+
to: b.to,
|
|
742
|
+
...(Number.isFinite(b.total) ? { total: b.total } : {}),
|
|
743
|
+
...(Number.isFinite(b.energyCharge) ? { energyCharge: b.energyCharge } : {}),
|
|
744
|
+
...(Number.isFinite(b.demandCharge) ? { demandCharge: b.demandCharge } : {}),
|
|
745
|
+
...(b.currency ? { currency: b.currency } : {}),
|
|
746
|
+
...(Number.isFinite(b.billingDemandKW) ? { billingDemandKW: b.billingDemandKW } : {}),
|
|
747
|
+
matchesWindow,
|
|
748
|
+
...(Object.keys(diff).length ? { diff } : {})
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
375
752
|
function measuredFeeders(
|
|
376
753
|
windows: readonly {
|
|
377
754
|
startMs: number
|
|
@@ -453,11 +830,56 @@ function measuredFeeders(
|
|
|
453
830
|
}
|
|
454
831
|
|
|
455
832
|
/** 폴드 옵션 — 관점 축과, 그 축을 만들기 위해 이벤트 밖에서 와야 하는 지도. */
|
|
833
|
+
/*
|
|
834
|
+
* ── 같은 사실이 두 벌 있어도 한 번만 센다 (2026-08-30) ──────────────────────
|
|
835
|
+
*
|
|
836
|
+
* 마감된 구간 사실은 정체성이 내용에 있다(커널 0.7.81). 그런데 저널에 이미 여러 벌 적힌 것이 있다 —
|
|
837
|
+
* 쓰는 자리에서 막기 전에 들어간 것들이다(실측: 어느 날의 발전 단가가 세 벌).
|
|
838
|
+
*
|
|
839
|
+
* 지난 기록을 지우지 않는다 — 저널은 일어난 일의 기록이다. 대신 **읽을 때 한 번만 센다.**
|
|
840
|
+
* 그러지 않으면 그 구간의 수익·사용량이 그 횟수만큼 세어진다.
|
|
841
|
+
*/
|
|
842
|
+
function onceById<T extends { payload?: any }>(rows: readonly T[]): T[] {
|
|
843
|
+
const seen = new Set<string>()
|
|
844
|
+
return rows.filter(r => {
|
|
845
|
+
const id = (r as any)?.payload?.eventId
|
|
846
|
+
if (typeof id !== 'string' || !id) return true
|
|
847
|
+
if (seen.has(id)) return false
|
|
848
|
+
seen.add(id)
|
|
849
|
+
return true
|
|
850
|
+
})
|
|
851
|
+
}
|
|
852
|
+
|
|
456
853
|
export interface KpiFoldOptions {
|
|
457
854
|
groupBy?: KpiGroupBy
|
|
855
|
+
/**
|
|
856
|
+
* 이 트윈이 이어받고 있는 **마지막 청구서** — 요금적용전력의 우선 근거(§`billingDemandOf`).
|
|
857
|
+
*
|
|
858
|
+
* 창 밖의 청구서는 저널 조회에 걸리지 않는다. 그런데 그 값은 이 창에도 적용되므로 호출부가
|
|
859
|
+
* 커널 상태에서 읽어 넘긴다.
|
|
860
|
+
*/
|
|
861
|
+
lastBill?: { to: string; billingDemandKW?: number }
|
|
862
|
+
/**
|
|
863
|
+
* 이 트윈이 이어받고 있는 **요금 기준** — 이 주기에 적용되는 값(§`ENERGY_EVENT.tariffBasis`).
|
|
864
|
+
*
|
|
865
|
+
* 청구서보다 먼저다. 청구서는 끝난 기간의 정산이라 이번 주기의 기준을 말하지 않는다.
|
|
866
|
+
*/
|
|
867
|
+
tariffBasis?: { from: string; billingDemandKW?: number }
|
|
868
|
+
/**
|
|
869
|
+
* 이 트윈의 **발전 정격**과 그 근거 — 이용률의 분모(§ 커널 `generationRated`).
|
|
870
|
+
*
|
|
871
|
+
* 모델의 선언에서 오므로 호출부가 읽어 넘긴다. 폴드는 저널만 본다.
|
|
872
|
+
*/
|
|
873
|
+
generationRated?: { kW: number; basis: 'ac' | 'dc'; from: 'equipment' | 'location' }
|
|
874
|
+
/**
|
|
875
|
+
* 이 트윈이 이어받고 있는 **발전 단가** — 창 밖의 단가도 이 창에 적용될 수 있다.
|
|
876
|
+
*
|
|
877
|
+
* 창 안의 단가 사실이 먼저다. 이것은 그 창에 단가가 없을 때 쓴다.
|
|
878
|
+
*/
|
|
879
|
+
generationPrice?: { from: string; to: string; unitPrice: number; currency: string }
|
|
458
880
|
/**
|
|
459
881
|
* 노드 → 구역 지도. **이벤트에는 구역이 없다**(노드까지만 있다) — 스냅샷·마스터가 아는 값이라
|
|
460
|
-
* 호출부가 넘긴다. 없으면 구역 축은 'unknown' 으로 모인다(
|
|
882
|
+
* 호출부가 넘긴다. 없으면 구역 축은 'unknown' 으로 모인다(알리지 않고 다른 축으로 바꾸지 않는다).
|
|
461
883
|
*/
|
|
462
884
|
locationArea?: Record<string, string>
|
|
463
885
|
/**
|
|
@@ -487,7 +909,7 @@ export interface KpiFoldOptions {
|
|
|
487
909
|
tariff?: TariffDeclaration
|
|
488
910
|
/**
|
|
489
911
|
* 교대 선언 — **이벤트에는 교대가 없다**(시각만 있다). 현장 마스터가 아는 값이라 호출부가 넘긴다.
|
|
490
|
-
* 없으면 교대 축은 'unknown' 으로 모인다(다른 축으로
|
|
912
|
+
* 없으면 교대 축은 'unknown' 으로 모인다(다른 축으로 알리지 않고 바꾸지 않는다).
|
|
491
913
|
*/
|
|
492
914
|
shifts?: WorkCalendarEntry[]
|
|
493
915
|
/**
|
|
@@ -518,7 +940,7 @@ interface CompletionRecord {
|
|
|
518
940
|
waitMs?: number
|
|
519
941
|
}
|
|
520
942
|
|
|
521
|
-
/** 이 기록이 요청한 축에서 어느 값에 속하는가. 값이 없으면 'unknown'(
|
|
943
|
+
/** 이 기록이 요청한 축에서 어느 값에 속하는가. 값이 없으면 'unknown'(알리지 않고 버리지 않는다). */
|
|
522
944
|
function facetKey(rec: CompletionRecord, options: KpiFoldOptions): string {
|
|
523
945
|
const f = rec.facets
|
|
524
946
|
switch (options.groupBy) {
|
|
@@ -547,7 +969,7 @@ function facetKey(rec: CompletionRecord, options: KpiFoldOptions): string {
|
|
|
547
969
|
*/
|
|
548
970
|
if (!options.shifts?.length) return 'unknown'
|
|
549
971
|
/* 시각까지 넘긴다 — **휴일에 일어난 일은 어느 교대에도 속하지 않는다.** 되풀이만 보면 그날 서지도
|
|
550
|
-
않은 교대에 집계돼, 교대별 성과가
|
|
972
|
+
않은 교대에 집계돼, 교대별 성과가 알리지 않고 거짓이 된다. */
|
|
551
973
|
return activeShiftAt(options.shifts, rec.at, options.utcOffsetMinutes) ?? 'unknown'
|
|
552
974
|
default:
|
|
553
975
|
return 'unknown'
|
|
@@ -576,7 +998,7 @@ function qualityOf(rows: CompletionRecord[]): { quality?: { good: number; scrap:
|
|
|
576
998
|
* 기록을 축으로 묶어 축별 통계를 낸다 — **전체와 같은 규칙**으로(같은 stats 함수, 같은 창).
|
|
577
999
|
*
|
|
578
1000
|
* 축의 값이 아주 많을 수 있다(오더 축은 수천 개). 그래서 건수 큰 것부터 상한까지만 돌려주고
|
|
579
|
-
* **잘라낸 수를 함께 알린다** —
|
|
1001
|
+
* **잘라낸 수를 함께 알린다** — 들어온 것이 없는 절단은 "이게 전부" 라는 거짓을 만든다.
|
|
580
1002
|
*/
|
|
581
1003
|
function groupRecords(
|
|
582
1004
|
records: CompletionRecord[],
|
|
@@ -644,11 +1066,11 @@ function stats(values: number[]): DurationStats {
|
|
|
644
1066
|
}
|
|
645
1067
|
|
|
646
1068
|
/**
|
|
647
|
-
* 이벤트를
|
|
1069
|
+
* 이벤트를 계산해 시간창 KPI 를 만든다.
|
|
648
1070
|
*
|
|
649
1071
|
* **호출부 규약**: `events` 는 창보다 **넉넉히 앞까지** 담아 넘긴다(예: 창 시작 − 리드타임 상한).
|
|
650
1072
|
* 완료된 작업의 시작 이벤트가 창 밖에 있을 수 있기 때문이다. 못 찾은 것은 `unpaired` 로 알린다 —
|
|
651
|
-
* 평균에서
|
|
1073
|
+
* 평균에서 오류 없이 빠지면 지표가 거짓이 된다.
|
|
652
1074
|
*
|
|
653
1075
|
* 시간축은 `eventTime`(ISO) 차분이다. sim 클록이 실제 시각과 오프셋이 있어도 **차분은 정확**하다
|
|
654
1076
|
* (고정 오프셋은 상쇄된다). 파싱 불가한 줄은 버린다.
|
|
@@ -676,7 +1098,7 @@ export function foldKpi(events: KpiEvent[], window: KpiWindow, options: KpiFoldO
|
|
|
676
1098
|
* **끝났나는 커널이 답한다** (2026-08-23) — `isOrderTerminal`.
|
|
677
1099
|
*
|
|
678
1100
|
* 예전에는 여기서 **동의어 목록**으로 추론했다(`completed`·`fulfilled`·`done`·`finished`).
|
|
679
|
-
* 그 방식이 낸 대가가 둘이었고 둘 다
|
|
1101
|
+
* 그 방식이 낸 대가가 둘이었고 둘 다 들어온 것이 없었다.
|
|
680
1102
|
*
|
|
681
1103
|
* ① 원본의 낱말이 목록에 없으면 완료가 0 이 된다. 실제로 그랬다 — 첫 실 시스템의 `FINISHED`
|
|
682
1104
|
* 가 빠져 있었고, 그때 목록에 낱말을 더한 것은 진짜 빈 자리(그 커넥터가 자기 상태 표를
|
|
@@ -693,7 +1115,7 @@ export function foldKpi(events: KpiEvent[], window: KpiWindow, options: KpiFoldO
|
|
|
693
1115
|
if (at >= window.fromMs && at <= window.toMs) {
|
|
694
1116
|
/*
|
|
695
1117
|
* **오더당 한 번만 센다.** 폴링하는 원본은 같은 오더의 종결을 여러 주기에 걸쳐 다시 보낼 수
|
|
696
|
-
* 있고(그것은 결함이 아니라 폴링의 성질이다), 사건마다 세면 처리량이
|
|
1118
|
+
* 있고(그것은 결함이 아니라 폴링의 성질이다), 사건마다 세면 처리량이 알리지 않고 부풀려진다.
|
|
697
1119
|
* 작업 쪽은 커널의 짝맞춤이 이미 그 규율을 갖고 있다(「완료는 작업당 하나」) — 오더만 없었다.
|
|
698
1120
|
*/
|
|
699
1121
|
if (done) {
|
|
@@ -701,7 +1123,7 @@ export function foldKpi(events: KpiEvent[], window: KpiWindow, options: KpiFoldO
|
|
|
701
1123
|
if (id && !completedOrderIds.has(id)) {
|
|
702
1124
|
completedOrderIds.add(id)
|
|
703
1125
|
completedOrders++
|
|
704
|
-
} else if (!id) completedOrders++ // 정체를 모르면 셈에서 빼지 않는다(
|
|
1126
|
+
} else if (!id) completedOrders++ // 정체를 모르면 셈에서 빼지 않는다(알리지 않고 줄이지 않는다)
|
|
705
1127
|
}
|
|
706
1128
|
/*
|
|
707
1129
|
* **완료로 세지 않은 낱말의 인구조사.** 「못 읽었다」가 아니다 — `picking` 처럼 완료가 아닌
|
|
@@ -744,6 +1166,76 @@ export function foldKpi(events: KpiEvent[], window: KpiWindow, options: KpiFoldO
|
|
|
744
1166
|
})
|
|
745
1167
|
.filter(w => Number.isFinite(w.startMs) && Number.isFinite(w.endMs))
|
|
746
1168
|
|
|
1169
|
+
/*
|
|
1170
|
+
* 발전 기간 사실 — 커널이 기간을 마감할 때 낸 것이다. 구간 마감과 같은 규칙으로 **창에 온전히 든
|
|
1171
|
+
* 것만** 센다(걸친 기간을 반만 세면 합이 창을 넘는다).
|
|
1172
|
+
*
|
|
1173
|
+
* 관측 구간(`observedFrom`·`observedTo`)을 함께 읽는다 — 값의 근거가 얼마나 덮였는지가 그 둘에 있다.
|
|
1174
|
+
*/
|
|
1175
|
+
const generationPeriods = onceById(events.filter(e => e.eventType === ENERGY_EVENT.generationPeriod))
|
|
1176
|
+
.map(e => {
|
|
1177
|
+
const d = e.payload?.data ?? e.payload ?? {}
|
|
1178
|
+
return {
|
|
1179
|
+
equipmentId: String(d.equipmentId ?? ''),
|
|
1180
|
+
kWh: Number(d.kWh),
|
|
1181
|
+
startMs: Date.parse(String(d.periodStart ?? '')),
|
|
1182
|
+
endMs: Date.parse(String(d.periodEnd ?? '')),
|
|
1183
|
+
observedFromMs: Date.parse(String(d.observedFrom ?? '')),
|
|
1184
|
+
observedToMs: Date.parse(String(d.observedTo ?? ''))
|
|
1185
|
+
}
|
|
1186
|
+
})
|
|
1187
|
+
.filter(g => g.equipmentId && Number.isFinite(g.kWh) && Number.isFinite(g.startMs) && Number.isFinite(g.endMs))
|
|
1188
|
+
|
|
1189
|
+
/*
|
|
1190
|
+
* 밖에서 마감되어 온 사용 구간 — 구간 마감과 같은 규칙으로 **창에 온전히 든 것만** 센다.
|
|
1191
|
+
*/
|
|
1192
|
+
const usagePeriods = onceById(events.filter(e => e.eventType === ENERGY_EVENT.usagePeriod))
|
|
1193
|
+
.map(e => {
|
|
1194
|
+
const d = e.payload?.data ?? e.payload ?? {}
|
|
1195
|
+
return {
|
|
1196
|
+
meterId: String(d.meterId ?? ''),
|
|
1197
|
+
kWh: Number(d.kWh),
|
|
1198
|
+
startMs: Date.parse(String(d.from ?? '')),
|
|
1199
|
+
endMs: Date.parse(String(d.to ?? '')),
|
|
1200
|
+
unitPrice: Number(d.unitPrice),
|
|
1201
|
+
maxKW: Number(d.maxKW),
|
|
1202
|
+
currency: d.currency ? String(d.currency) : undefined,
|
|
1203
|
+
basis: d.basis ? String(d.basis) : 'unstated'
|
|
1204
|
+
}
|
|
1205
|
+
})
|
|
1206
|
+
.filter(u => u.meterId && Number.isFinite(u.kWh) && Number.isFinite(u.startMs) && Number.isFinite(u.endMs))
|
|
1207
|
+
|
|
1208
|
+
/* 청구서 — 창에 온전히 든 것 중 가장 늦게 끝난 것 하나. */
|
|
1209
|
+
const bills = onceById(events.filter(e => e.eventType === ENERGY_EVENT.bill))
|
|
1210
|
+
.map(e => {
|
|
1211
|
+
const d = e.payload?.data ?? e.payload ?? {}
|
|
1212
|
+
return {
|
|
1213
|
+
from: String(d.from ?? ''),
|
|
1214
|
+
to: String(d.to ?? ''),
|
|
1215
|
+
startMs: Date.parse(String(d.from ?? '')),
|
|
1216
|
+
endMs: Date.parse(String(d.to ?? '')),
|
|
1217
|
+
total: Number(d.total),
|
|
1218
|
+
energyCharge: Number(d.energyCharge),
|
|
1219
|
+
demandCharge: Number(d.demandCharge),
|
|
1220
|
+
currency: d.currency ? String(d.currency) : undefined,
|
|
1221
|
+
billingDemandKW: Number(d.billingDemandKW)
|
|
1222
|
+
}
|
|
1223
|
+
})
|
|
1224
|
+
.filter(b => Number.isFinite(b.startMs) && Number.isFinite(b.endMs))
|
|
1225
|
+
|
|
1226
|
+
/* 발전 단가 — 기간마다 그때의 값이다(날마다 바뀐다). */
|
|
1227
|
+
const generationPrices = onceById(events.filter(e => e.eventType === ENERGY_EVENT.generationPrice))
|
|
1228
|
+
.map(e => {
|
|
1229
|
+
const d = e.payload?.data ?? e.payload ?? {}
|
|
1230
|
+
return {
|
|
1231
|
+
startMs: Date.parse(String(d.from ?? '')),
|
|
1232
|
+
endMs: Date.parse(String(d.to ?? '')),
|
|
1233
|
+
unitPrice: Number(d.unitPrice),
|
|
1234
|
+
currency: String(d.currency ?? '')
|
|
1235
|
+
}
|
|
1236
|
+
})
|
|
1237
|
+
.filter(p => Number.isFinite(p.startMs) && Number.isFinite(p.endMs) && Number.isFinite(p.unitPrice) && p.currency)
|
|
1238
|
+
|
|
747
1239
|
const leads: number[] = []
|
|
748
1240
|
const works: number[] = []
|
|
749
1241
|
const waits: number[] = []
|
|
@@ -785,18 +1277,54 @@ export function foldKpi(events: KpiEvent[], window: KpiWindow, options: KpiFoldO
|
|
|
785
1277
|
return {
|
|
786
1278
|
window: { fromMs: window.fromMs, toMs: window.toMs },
|
|
787
1279
|
throughput: { tasks: tasksInWindow, orders: completedOrders },
|
|
788
|
-
|
|
1280
|
+
/* 발전만 있는 현장(계량 없는 발전소)도 답이 나와야 한다 — 둘 중 하나만 있어도 블록을 낸다. */
|
|
1281
|
+
...(demandWindows.length || generationPeriods.length || usagePeriods.length || bills.length
|
|
789
1282
|
? (() => {
|
|
790
1283
|
const e = energyOfWindows(demandWindows, { startMs: window.fromMs, endMs: window.toMs })
|
|
1284
|
+
const usage = usageOfPeriods(usagePeriods, { startMs: window.fromMs, endMs: window.toMs })
|
|
1285
|
+
/*
|
|
1286
|
+
* 어느 회계를 쓸지 정한다 — **더하지 않는다.** 계량 표본에서 우리가 마감한 값이 있으면 그것을
|
|
1287
|
+
* 쓰고(강한 근거), 없으면 밖에서 마감되어 온 값을 쓴다. 무엇을 썼는지 함께 낸다.
|
|
1288
|
+
*/
|
|
1289
|
+
const usedKWh = e.kWh !== undefined ? e.kWh : usage?.kWh
|
|
1290
|
+
const kWhBasis = e.kWh !== undefined ? 'demand-window' : usage ? 'usage-period' : undefined
|
|
791
1291
|
/* 분모는 **완료 오더**다(대당 전력). 창에 완료가 없으면 커널 규칙이 「정의되지 않음」으로 답한다. */
|
|
792
1292
|
const i = energyIntensity({
|
|
793
|
-
kWh:
|
|
1293
|
+
kWh: usedKWh,
|
|
794
1294
|
energyWindow: e.window,
|
|
795
1295
|
denominator: { kind: 'output', value: completedOrders, window: e.window }
|
|
796
1296
|
})
|
|
1297
|
+
/*
|
|
1298
|
+
* 요금은 **구간마다 제 단가로** 매긴 값이 있으면 그것을 쓴다. 단일 단가로 접으면 시간대마다
|
|
1299
|
+
* 다른 단가가 사라지고, 「피크를 깎아 얼마를 아끼나」가 그 차이 위에 선다.
|
|
1300
|
+
*/
|
|
1301
|
+
/* 요금적용전력은 청구서가 먼저다 — 선언은 청구가 오지 않는 현장의 몫이다. */
|
|
1302
|
+
/* 이 창의 최고 수요 — 사용 구간이 실어 온 값 중 가장 큰 것(원본 계량기가 잰 값이다). */
|
|
1303
|
+
const peaks = usagePeriods
|
|
1304
|
+
.filter(u => u.startMs >= window.fromMs && u.endMs <= window.toMs && Number.isFinite(u.maxKW))
|
|
1305
|
+
.map(u => ({ kW: u.maxKW as number, toMs: u.endMs }))
|
|
1306
|
+
const observedPeak = peaks.length ? peaks.reduce((a, x) => (x.kW > a.kW ? x : a)) : undefined
|
|
1307
|
+
const billingDemand = billingDemandOf(
|
|
1308
|
+
bills.filter(b => Number.isFinite(b.billingDemandKW)).map(b => ({ endMs: b.endMs, billingDemandKW: b.billingDemandKW })),
|
|
1309
|
+
options.tariffBasis,
|
|
1310
|
+
options.lastBill,
|
|
1311
|
+
options.tariff?.billingDemandKW,
|
|
1312
|
+
observedPeak
|
|
1313
|
+
)
|
|
1314
|
+
const cost = options.tariff
|
|
1315
|
+
? electricityCost({
|
|
1316
|
+
kWh: usedKWh,
|
|
1317
|
+
peakKW: e.peakKW,
|
|
1318
|
+
tariff: { ...options.tariff, ...(billingDemand ? { billingDemandKW: billingDemand.kW } : {}) },
|
|
1319
|
+
...(usage?.charge !== undefined ? { energyChargeFromPeriods: usage.charge } : {})
|
|
1320
|
+
})
|
|
1321
|
+
: undefined
|
|
797
1322
|
return {
|
|
798
1323
|
energy: {
|
|
799
|
-
...(
|
|
1324
|
+
...(usedKWh !== undefined ? { kWh: usedKWh } : {}),
|
|
1325
|
+
...(kWhBasis ? { kWhBasis } : {}),
|
|
1326
|
+
...(billingDemand ? { billingDemand } : {}),
|
|
1327
|
+
...(usage ? { usage } : {}),
|
|
800
1328
|
/* 이 창의 최대수요 — 계약 대비 판정이 보는 수다(전력량과 같은 구간 집합에서 왔다). */
|
|
801
1329
|
...(e.peakKW !== undefined ? { peakKW: e.peakKW, peakStartMs: e.peakStartMs } : {}),
|
|
802
1330
|
/* 깎인 양은 **같은 구간의 두 수**의 차이다 — 여기서 한 번만 계산해 두 곳이 갈라지지 않게. */
|
|
@@ -813,7 +1341,30 @@ export function foldKpi(events: KpiEvent[], window: KpiWindow, options: KpiFoldO
|
|
|
813
1341
|
...(measuredFeeders(demandWindows, { startMs: window.fromMs, endMs: window.toMs }, e.kWh, options.meterFeeder) ??
|
|
814
1342
|
feederShares(e.kWh, utilization, options.resourceFeeder)),
|
|
815
1343
|
/* 금액은 같은 창의 전력량·최대수요에서 나온다 — 창이 하나뿐이라 어긋날 수 없다. */
|
|
816
|
-
...(
|
|
1344
|
+
...(cost ? { cost } : {}),
|
|
1345
|
+
...generatedOfPeriods(
|
|
1346
|
+
generationPeriods,
|
|
1347
|
+
{ startMs: window.fromMs, endMs: window.toMs },
|
|
1348
|
+
options.generationRated,
|
|
1349
|
+
/* 창 안의 단가 사실이 먼저이고, 없으면 이어받은 마지막 단가를 쓴다. */
|
|
1350
|
+
generationPrices.length
|
|
1351
|
+
? generationPrices
|
|
1352
|
+
: options.generationPrice
|
|
1353
|
+
? [
|
|
1354
|
+
{
|
|
1355
|
+
startMs: Date.parse(options.generationPrice.from),
|
|
1356
|
+
endMs: Date.parse(options.generationPrice.to),
|
|
1357
|
+
unitPrice: options.generationPrice.unitPrice,
|
|
1358
|
+
currency: options.generationPrice.currency
|
|
1359
|
+
}
|
|
1360
|
+
]
|
|
1361
|
+
: []
|
|
1362
|
+
),
|
|
1363
|
+
/* 청구서는 사실이다 — 우리 계산을 덮지 않고 나란히 둔다(§`billOfWindow`). */
|
|
1364
|
+
...(() => {
|
|
1365
|
+
const b = billOfWindow(bills, { startMs: window.fromMs, endMs: window.toMs }, cost as any)
|
|
1366
|
+
return b ? { bill: b } : {}
|
|
1367
|
+
})()
|
|
817
1368
|
}
|
|
818
1369
|
}
|
|
819
1370
|
})()
|
|
@@ -876,7 +1427,7 @@ export function crossGroups(
|
|
|
876
1427
|
|
|
877
1428
|
/* ── 시간 비교 ──────────────────────────────────────────────────────────────
|
|
878
1429
|
* 숫자 하나로는 좋아졌는지 나빠졌는지 알 수 없다("29초" 가 개선인지 악화인지). 같은 길이의 창을 옮겨
|
|
879
|
-
* 한 번 더
|
|
1430
|
+
* 한 번 더 계산하면 되므로(폴드는 순수) 값싸다. 창 옮기기와 차이 계산은 계산 규칙이라 여기 있다 —
|
|
880
1431
|
* 조회 층에 두면 테스트가 DB 를 끌고 와야 한다.
|
|
881
1432
|
*/
|
|
882
1433
|
|
|
@@ -913,7 +1464,7 @@ export interface KpiBucket {
|
|
|
913
1464
|
}
|
|
914
1465
|
|
|
915
1466
|
/**
|
|
916
|
-
* 창을 N 등분해 구간별로
|
|
1467
|
+
* 창을 N 등분해 구간별로 처리한다 — **같은 이벤트를 창만 바꿔** 다시 계산한다(조회 추가 없음).
|
|
917
1468
|
*
|
|
918
1469
|
* 마지막 구간은 나머지를 흡수한다(부동소수 나눗셈으로 끝이 밀리면 마지막 완료가 빠진다).
|
|
919
1470
|
* `count <= 1` 이면 쪼갤 것이 없으므로 빈 배열 — 호출부가 "추세 없음" 으로 다룬다.
|