@things-factory/headless-twin 10.0.8 → 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/attention-digest.d.ts +52 -0
- package/dist-server/engine/attention-digest.js +76 -0
- package/dist-server/engine/attention-digest.js.map +1 -0
- 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.d.ts +33 -0
- package/dist-server/engine/command-routing.js +53 -0
- package/dist-server/engine/command-routing.js.map +1 -0
- 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 +11 -0
- package/dist-server/engine/index.js +17 -0
- package/dist-server/engine/index.js.map +1 -1
- package/dist-server/engine/kpi-baseline.d.ts +78 -0
- package/dist-server/engine/kpi-baseline.js +123 -0
- package/dist-server/engine/kpi-baseline.js.map +1 -0
- 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 +32 -1
- package/dist-server/engine/kpi-query.js +262 -20
- 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-attentions.d.ts +1 -0
- package/dist-server/engine/live-attentions.js +7 -1
- package/dist-server/engine/live-attentions.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/model-vocabulary.d.ts +17 -0
- package/dist-server/engine/model-vocabulary.js +81 -0
- 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 +15 -0
- package/dist-server/engine/runtime-key.js +64 -0
- package/dist-server/engine/runtime-key.js.map +1 -0
- 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 +21 -0
- package/dist-server/engine/state-axes.js +59 -0
- package/dist-server/engine/state-axes.js.map +1 -0
- 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 +418 -57
- package/dist-server/engine/twin-engine.js +1599 -306
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/engine/warm-start.d.ts +93 -13
- package/dist-server/engine/warm-start.js +114 -14
- 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 +34 -0
- package/dist-server/service/reference/discovery-result.js +84 -0
- package/dist-server/service/reference/discovery-result.js.map +1 -0
- package/dist-server/service/reference/ingest-space.d.ts +53 -0
- package/dist-server/service/reference/ingest-space.js +79 -0
- package/dist-server/service/reference/ingest-space.js.map +1 -0
- package/dist-server/service/reference/knob-defaults.d.ts +20 -0
- package/dist-server/service/reference/knob-defaults.js +59 -0
- package/dist-server/service/reference/knob-defaults.js.map +1 -0
- package/dist-server/service/reference/reference-live.d.ts +23 -0
- package/dist-server/service/reference/reference-live.js +123 -6
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-master.d.ts +227 -11
- package/dist-server/service/reference/reference-master.js +206 -25
- package/dist-server/service/reference/reference-master.js.map +1 -1
- package/dist-server/service/reference/reference-resolver.d.ts +33 -3
- package/dist-server/service/reference/reference-resolver.js +290 -23
- 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-attention/twin-attention-query.d.ts +8 -1
- package/dist-server/service/twin-attention/twin-attention-query.js +39 -8
- package/dist-server/service/twin-attention/twin-attention-query.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.d.ts +2 -0
- package/dist-server/service/twin-control/twin-control-mutation.js +50 -11
- 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 +219 -39
- 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 -18
- package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
- package/dist-server/service/twin-journal/twin-journal-query.d.ts +11 -4
- package/dist-server/service/twin-journal/twin-journal-query.js +95 -15
- package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +11 -5
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +46 -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 +63 -1
- 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 +365 -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-state/twin-state-subscription.js +1 -1
- package/dist-server/service/twin-state/twin-state-subscription.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 -7
- package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
- package/dist-server/service/twin-target/twin-target-resolver.js +21 -4
- package/dist-server/service/twin-target/twin-target-resolver.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-shared/entity-delta.d.ts +29 -0
- package/dist-shared/entity-delta.js +287 -0
- package/dist-shared/entity-delta.js.map +1 -0
- package/dist-shared/kpi-broadcast.d.ts +4 -0
- package/dist-shared/kpi-broadcast.js +16 -0
- package/dist-shared/kpi-broadcast.js.map +1 -0
- package/dist-shared/twin-level.d.ts +23 -0
- package/dist-shared/twin-level.js +52 -0
- package/dist-shared/twin-level.js.map +1 -0
- package/package.json +14 -12
- package/server/engine/attention-digest.ts +102 -0
- package/server/engine/canonical-ingest.ts +36 -4
- package/server/engine/command-routing.ts +67 -0
- package/server/engine/energy-topology.ts +189 -0
- package/server/engine/index.ts +17 -0
- package/server/engine/kpi-baseline.ts +202 -0
- package/server/engine/kpi-fold.ts +389 -1
- package/server/engine/kpi-query.ts +295 -22
- package/server/engine/kpi-target.ts +24 -2
- package/server/engine/live-attentions.ts +7 -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/model-vocabulary.ts +82 -0
- 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 +58 -0
- package/server/engine/spec-coverage.ts +3 -3
- package/server/engine/state-axes.ts +60 -0
- package/server/engine/structure-diff.ts +1 -1
- package/server/engine/travel-estimator.ts +11 -6
- package/server/engine/twin-engine.ts +1710 -297
- package/server/engine/warm-start.ts +207 -22
- 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 +95 -0
- package/server/service/reference/ingest-space.ts +104 -0
- package/server/service/reference/knob-defaults.ts +59 -0
- package/server/service/reference/reference-live.ts +121 -6
- package/server/service/reference/reference-master.ts +362 -35
- package/server/service/reference/reference-resolver.ts +307 -23
- package/server/service/reference/template-registry.ts +20 -5
- package/server/service/twin-attention/twin-attention-query.ts +43 -6
- 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 +53 -13
- 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 +211 -38
- package/server/service/twin-instance/twin-instance.ts +127 -22
- package/server/service/twin-journal/twin-journal-query.ts +108 -14
- package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +40 -23
- package/server/service/twin-metrics/twin-metrics-query.ts +60 -3
- 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 +365 -22
- package/server/service/twin-space/twin-space.ts +40 -6
- package/server/service/twin-state/twin-state-subscription.ts +1 -1
- package/server/service/twin-structure/twin-structure.ts +10 -7
- package/server/service/twin-target/twin-target-resolver.ts +22 -4
- package/shared/axis-read.ts +28 -0
- package/shared/entity-delta.ts +286 -0
- package/shared/kpi-broadcast.ts +13 -0
- package/shared/twin-level.ts +48 -0
- package/test/adopt-structure-live.test.ts +133 -0
- package/test/attention-digest.test.ts +135 -0
- 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 +5 -5
- package/test/command-routing.test.ts +61 -0
- package/test/declaration-reaches-model.test.ts +178 -0
- package/test/discovery-result.test.ts +75 -0
- package/test/energy-topology.test.ts +113 -0
- package/test/entity-delta.test.ts +195 -25
- 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 +9 -9
- package/test/ingest-running-guard.test.ts +136 -0
- package/test/ingest-space.test.ts +78 -0
- package/test/instance-cache-lifecycle.test.ts +110 -0
- package/test/kernel-kind-guard.test.ts +96 -0
- package/test/knob-defaults.test.ts +72 -0
- package/test/kpi-baseline-db.test.ts +216 -0
- package/test/kpi-baseline.test.ts +196 -0
- package/test/kpi-fold.test.ts +393 -2
- package/test/kpi-query-bench.test.ts +130 -0
- 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 +54 -14
- package/test/load-meter.test.ts +314 -0
- package/test/local-declarations.test.ts +554 -0
- package/test/master-to-twin.test.ts +128 -36
- package/test/model-basis.test.ts +56 -1
- package/test/model-vocabulary.test.ts +114 -0
- package/test/move-space.test.ts +149 -0
- package/test/mutation-gate.test.ts +122 -0
- package/test/oee-accumulator.test.ts +90 -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/registry-key-guard.test.ts +80 -0
- package/test/resync-origin-site.test.ts +77 -0
- package/test/runtime-key.test.ts +66 -0
- package/test/scale-twin-bench.test.ts +7 -7
- 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 +74 -0
- package/test/streamline-e2e.test.ts +16 -16
- package/test/structure-revision-db.test.ts +16 -15
- package/test/tenant-registry-db.test.ts +149 -0
- 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 +140 -0
- package/test/warm-start.test.ts +223 -4
- 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/entity-delta.d.ts +0 -19
- package/dist-server/engine/entity-delta.js +0 -161
- package/dist-server/engine/entity-delta.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/engine/entity-delta.ts +0 -169
- package/server/service/twin-event/backfill-keys.ts +0 -72
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
import { test } from 'node:test'
|
|
12
12
|
import assert from 'node:assert/strict'
|
|
13
13
|
|
|
14
|
-
import { buildEntityDeltas, ITEMS_AGGREGATE_LIMIT,
|
|
14
|
+
import { buildEntityDeltas, ITEMS_AGGREGATE_LIMIT, itemsTag, ordersTag, personsTag, tasksTag, energyTag } from '../shared/entity-delta.ts'
|
|
15
15
|
|
|
16
16
|
const byTag = (arr: any[], tag: string) => arr.find(x => x.tag === tag)
|
|
17
17
|
|
|
18
18
|
test('노드: 계약의 속성이 전부 나간다 — parentId(소속 구역)를 포함해', () => {
|
|
19
19
|
const d = buildEntityDeltas({
|
|
20
20
|
locations: [{ id: 'bin-1', occupancy: 3, capacity: 10, status: 'idle', type: 'storage', parentId: 'zone-a' }]
|
|
21
|
-
})
|
|
21
|
+
}, 't1')
|
|
22
22
|
assert.deepEqual(byTag(d, 'bin-1').data, {
|
|
23
23
|
entity: 'location',
|
|
24
24
|
occupancy: 3,
|
|
@@ -32,7 +32,7 @@ test('노드: 계약의 속성이 전부 나간다 — parentId(소속 구역)
|
|
|
32
32
|
test('표식은 entity, kind 는 도메인 것 — 무버 종류가 표식에 덮이지 않는다', () => {
|
|
33
33
|
const d = buildEntityDeltas({
|
|
34
34
|
equipment: [{ id: 'cutter-1', kind: 'cutter', location: 'cut', status: 'down', held: true }]
|
|
35
|
-
})
|
|
35
|
+
}, 't1')
|
|
36
36
|
const m = byTag(d, 'cutter-1').data
|
|
37
37
|
assert.equal(m.entity, 'equipment', '표식')
|
|
38
38
|
assert.equal(m.kind, 'cutter', '도메인 속성 — 예전엔 표식에 덮여 사라졌다')
|
|
@@ -50,7 +50,7 @@ test('무버: OEE 는 정수 퍼센트로 정형화하고 원시 누적은 빼
|
|
|
50
50
|
oee: { overall: 0.8234, availability: 0.9, performance: 0.95, quality: 0.96, goodCount: 12, scrapCount: 3, idleMs: 99999 }
|
|
51
51
|
}
|
|
52
52
|
]
|
|
53
|
-
})
|
|
53
|
+
}, 't1')
|
|
54
54
|
assert.deepEqual(byTag(d, 'cutter-1').data.oee, {
|
|
55
55
|
overall: 82,
|
|
56
56
|
availability: 90,
|
|
@@ -62,7 +62,7 @@ test('무버: OEE 는 정수 퍼센트로 정형화하고 원시 누적은 빼
|
|
|
62
62
|
})
|
|
63
63
|
|
|
64
64
|
test('무버: oee 미측정이면 실리지 않는다', () => {
|
|
65
|
-
const d = buildEntityDeltas({ equipment: [{ id: 'fk-1', kind: 'forklift', status: 'idle' }] })
|
|
65
|
+
const d = buildEntityDeltas({ equipment: [{ id: 'fk-1', kind: 'forklift', status: 'idle' }] }, 't1')
|
|
66
66
|
assert.equal(byTag(d, 'fk-1').data.oee, undefined)
|
|
67
67
|
})
|
|
68
68
|
|
|
@@ -72,7 +72,7 @@ test('작업: 개별 + 집약으로 나간다 — 예전엔 아예 방출되지
|
|
|
72
72
|
{ id: 't1', kind: 'putaway', status: 'in-progress', fromNode: 'dock', toNode: 'rack-1', resourceRef: 'fk-1', orderId: 'o1', progress: 0.4 },
|
|
73
73
|
{ id: 't2', kind: 'pick', status: 'created' }
|
|
74
74
|
]
|
|
75
|
-
})
|
|
75
|
+
}, 't1')
|
|
76
76
|
assert.deepEqual(byTag(d, 't1').data, {
|
|
77
77
|
entity: 'task',
|
|
78
78
|
kind: 'putaway',
|
|
@@ -83,7 +83,7 @@ test('작업: 개별 + 집약으로 나간다 — 예전엔 아예 방출되지
|
|
|
83
83
|
orderId: 'o1',
|
|
84
84
|
progress: 0.4
|
|
85
85
|
})
|
|
86
|
-
const agg = byTag(d,
|
|
86
|
+
const agg = byTag(d, tasksTag('t1')).data
|
|
87
87
|
assert.equal(agg.entity, 'tasks')
|
|
88
88
|
assert.equal(agg.tasks.length, 2)
|
|
89
89
|
assert.equal(agg.tasks[0].id, 't1', '집약에는 식별자가 있다')
|
|
@@ -93,7 +93,7 @@ test('작업: 개별 + 집약으로 나간다 — 예전엔 아예 방출되지
|
|
|
93
93
|
test('물품: 개별(태그=epc) + 집약으로 나간다 — 수량·소속 팔레트·만료까지', () => {
|
|
94
94
|
const d = buildEntityDeltas({
|
|
95
95
|
items: [{ epc: 'urn:epc:id:sgtin:1.1.1', gtin: 'g1', location: 'rack-1', disposition: 'sellable', parent: 'sscc-1', qty: 40, expiry: 1234 }]
|
|
96
|
-
})
|
|
96
|
+
}, 't1')
|
|
97
97
|
assert.deepEqual(byTag(d, 'urn:epc:id:sgtin:1.1.1').data, {
|
|
98
98
|
entity: 'item',
|
|
99
99
|
gtin: 'g1',
|
|
@@ -103,7 +103,7 @@ test('물품: 개별(태그=epc) + 집약으로 나간다 — 수량·소속 팔
|
|
|
103
103
|
qty: 40,
|
|
104
104
|
expiry: 1234
|
|
105
105
|
})
|
|
106
|
-
const agg = byTag(d,
|
|
106
|
+
const agg = byTag(d, itemsTag('t1')).data
|
|
107
107
|
assert.equal(agg.entity, 'items')
|
|
108
108
|
assert.equal(agg.items[0].epc, 'urn:epc:id:sgtin:1.1.1')
|
|
109
109
|
assert.equal(agg.items[0].qty, 40)
|
|
@@ -113,8 +113,8 @@ test('물품: 개별(태그=epc) + 집약으로 나간다 — 수량·소속 팔
|
|
|
113
113
|
|
|
114
114
|
test('물품 집약: 상한을 넘으면 잘린 사실을 밝힌다 — 조용한 절단 금지', () => {
|
|
115
115
|
const items = Array.from({ length: ITEMS_AGGREGATE_LIMIT + 7 }, (_, i) => ({ epc: `e${i}`, location: 'rack-1' }))
|
|
116
|
-
const d = buildEntityDeltas({ items })
|
|
117
|
-
const agg = byTag(d,
|
|
116
|
+
const d = buildEntityDeltas({ items }, 't1')
|
|
117
|
+
const agg = byTag(d, itemsTag('t1')).data
|
|
118
118
|
assert.equal(agg.items.length, ITEMS_AGGREGATE_LIMIT)
|
|
119
119
|
assert.equal(agg.total, ITEMS_AGGREGATE_LIMIT + 7)
|
|
120
120
|
assert.equal(agg.truncated, 7)
|
|
@@ -128,9 +128,9 @@ test('오더: 개별 + 집약, 도메인 kind 유지', () => {
|
|
|
128
128
|
{ id: 'o1', kind: 'workorder', status: 'picking', progress: 0.5, held: false },
|
|
129
129
|
{ id: 'o2', kind: 'outbound', status: 'created', progress: 0, held: true }
|
|
130
130
|
]
|
|
131
|
-
})
|
|
131
|
+
}, 't1')
|
|
132
132
|
assert.equal(byTag(d, 'o1').data.kind, 'workorder')
|
|
133
|
-
const agg = byTag(d,
|
|
133
|
+
const agg = byTag(d, ordersTag('t1')).data
|
|
134
134
|
assert.equal(agg.entity, 'orders')
|
|
135
135
|
assert.equal(agg.orders.length, 2)
|
|
136
136
|
assert.equal(agg.orders[1].held, true, '집약에 held 반영')
|
|
@@ -138,23 +138,23 @@ test('오더: 개별 + 집약, 도메인 kind 유지', () => {
|
|
|
138
138
|
|
|
139
139
|
test('계약에 새 속성이 생기면 손대지 않아도 도달한다 — 화이트리스트 재발 방지', () => {
|
|
140
140
|
/* 이 테스트가 실패하면 누군가 필드를 손으로 골라 담기 시작한 것이다. */
|
|
141
|
-
const d = buildEntityDeltas({ locations: [{ id: 'n1', occupancy: 0, temperatureC: -18, hazmatClass: '3' }] })
|
|
141
|
+
const d = buildEntityDeltas({ locations: [{ id: 'n1', occupancy: 0, temperatureC: -18, hazmatClass: '3' }] }, 't1')
|
|
142
142
|
const n = byTag(d, 'n1').data
|
|
143
143
|
assert.equal(n.temperatureC, -18, '알지 못하는 새 속성도 통과해야 한다')
|
|
144
144
|
assert.equal(n.hazmatClass, '3')
|
|
145
145
|
})
|
|
146
146
|
|
|
147
147
|
test('undefined 값은 싣지 않는다 — 키 유무가 시그니처를 흔들지 않게', () => {
|
|
148
|
-
const d = buildEntityDeltas({ equipment: [{ id: 'fk-1', kind: 'forklift', location: undefined, status: 'idle' }] })
|
|
148
|
+
const d = buildEntityDeltas({ equipment: [{ id: 'fk-1', kind: 'forklift', location: undefined, status: 'idle' }] }, 't1')
|
|
149
149
|
assert.equal('location' in byTag(d, 'fk-1').data, false)
|
|
150
150
|
})
|
|
151
151
|
|
|
152
152
|
test('빈 상태 — 집약 태그 셋은 항상 방출(빈 배열)', () => {
|
|
153
|
-
const d = buildEntityDeltas({})
|
|
154
|
-
assert.deepEqual(byTag(d,
|
|
155
|
-
assert.deepEqual(byTag(d,
|
|
156
|
-
assert.deepEqual(byTag(d,
|
|
157
|
-
assert.equal(byTag(d,
|
|
153
|
+
const d = buildEntityDeltas({}, 't1')
|
|
154
|
+
assert.deepEqual(byTag(d, ordersTag('t1')).data.orders, [])
|
|
155
|
+
assert.deepEqual(byTag(d, tasksTag('t1')).data.tasks, [])
|
|
156
|
+
assert.deepEqual(byTag(d, itemsTag('t1')).data.items, [])
|
|
157
|
+
assert.equal(byTag(d, itemsTag('t1')).data.total, 0)
|
|
158
158
|
})
|
|
159
159
|
|
|
160
160
|
/* ── 사람 ────────────────────────────────────────────────────────────────
|
|
@@ -165,7 +165,7 @@ test('빈 상태 — 집약 태그 셋은 항상 방출(빈 배열)', () => {
|
|
|
165
165
|
test('사람이 개별 태그로 나간다 — 등급·교대가 그대로', () => {
|
|
166
166
|
const deltas = buildEntityDeltas({
|
|
167
167
|
persons: [{ id: 'w1', personnelClass: 'welder-op', status: 'busy', taskId: 't1', offShift: undefined }]
|
|
168
|
-
})
|
|
168
|
+
}, 't1')
|
|
169
169
|
const one = deltas.find(d => d.tag === 'w1')!
|
|
170
170
|
assert.equal(one.data.entity, 'person')
|
|
171
171
|
assert.equal(one.data.personnelClass, 'welder-op')
|
|
@@ -174,9 +174,179 @@ test('사람이 개별 태그로 나간다 — 등급·교대가 그대로', ()
|
|
|
174
174
|
})
|
|
175
175
|
|
|
176
176
|
test('사람 집약 태그가 항상 나간다 — 선언하지 않은 트윈은 빈 배열(없음과 모름을 구별)', () => {
|
|
177
|
-
const withNone = buildEntityDeltas({})
|
|
178
|
-
const agg = withNone.find(d => d.tag ===
|
|
177
|
+
const withNone = buildEntityDeltas({}, 't1')
|
|
178
|
+
const agg = withNone.find(d => d.tag === personsTag('t1'))!
|
|
179
179
|
assert.deepEqual(agg.data.persons, [])
|
|
180
|
-
const withTwo = buildEntityDeltas({ persons: [{ id: 'w1' }, { id: 'w2' }] })
|
|
181
|
-
assert.equal(withTwo.find(d => d.tag ===
|
|
180
|
+
const withTwo = buildEntityDeltas({ persons: [{ id: 'w1' }, { id: 'w2' }] }, 't1')
|
|
181
|
+
assert.equal(withTwo.find(d => d.tag === personsTag('t1'))!.data.persons.length, 2)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
/*
|
|
185
|
+
* ── 에너지 (EMS 6단계) ──────────────────────────────────────────────────────
|
|
186
|
+
*
|
|
187
|
+
* 계측은 `state.energy` 에 산다 — 자원 축이 아니다. 이 매핑이 없으면 보드에 놓은 계량기가 값을
|
|
188
|
+
* 받지 못하고 **영원히 빈 모습**이 된다(컴포넌트는 있는데 데이터가 없는, 눈에 안 보이는 결함).
|
|
189
|
+
*/
|
|
190
|
+
test('계량 지점의 마지막 관측이 그 설비 태그로 나간다', () => {
|
|
191
|
+
const deltas = buildEntityDeltas(
|
|
192
|
+
{
|
|
193
|
+
locations: [],
|
|
194
|
+
equipment: [],
|
|
195
|
+
energy: {
|
|
196
|
+
points: [
|
|
197
|
+
{ id: 'meter-incoming', kW: 1200.5, kWh: 98_000, powerFactor: 0.95, atMs: Date.parse('2026-08-14T09:01:00.000Z'), samplesInWindow: 3 },
|
|
198
|
+
{ id: 'meter-quiet', samplesInWindow: 0 }
|
|
199
|
+
],
|
|
200
|
+
closed: [],
|
|
201
|
+
closedTotal: 0
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
'ems-1'
|
|
205
|
+
)
|
|
206
|
+
const m = deltas.find(d => d.tag === 'meter-incoming')!
|
|
207
|
+
assert.equal(m.data.entity, 'meter')
|
|
208
|
+
assert.equal(m.data.kW, 1200.5)
|
|
209
|
+
assert.equal(m.data.measuredAt, '2026-08-14T09:01:00.000Z', '시각을 함께 낸다 — 화면이 계측 끊김을 스스로 안다')
|
|
210
|
+
|
|
211
|
+
/* 값이 없는 지점: 키를 넣지 않는다(0 으로 만들면 「전기를 안 썼다」가 된다). */
|
|
212
|
+
const q = deltas.find(d => d.tag === 'meter-quiet')!
|
|
213
|
+
assert.equal('kW' in q.data, false)
|
|
214
|
+
assert.equal(q.data.samplesInWindow, 0, '받은 표본 수는 사실이다')
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
test('수요 구간·피크·계약은 집약 태그로 나간다 — 개체의 사실이 아니다', () => {
|
|
218
|
+
const deltas = buildEntityDeltas(
|
|
219
|
+
{
|
|
220
|
+
locations: [],
|
|
221
|
+
equipment: [],
|
|
222
|
+
energy: {
|
|
223
|
+
points: [],
|
|
224
|
+
contractKW: 4000,
|
|
225
|
+
open: { startMs: 1, endMs: 2, samples: 1, maxKW: 4200 },
|
|
226
|
+
peakSince: { kW: 4200, windowStartMs: 1 },
|
|
227
|
+
closed: Array.from({ length: 20 }, (_, i) => ({ startMs: i, endMs: i + 1, samples: 1, maxKW: 100 + i })),
|
|
228
|
+
closedTotal: 137
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
'ems-1'
|
|
232
|
+
)
|
|
233
|
+
const agg = deltas.find(d => d.tag === energyTag('ems-1'))!
|
|
234
|
+
assert.ok(agg, '집약 태그가 있어야 한다')
|
|
235
|
+
assert.equal(agg.data.contractKW, 4000)
|
|
236
|
+
assert.equal(agg.data.peakSince.kW, 4200)
|
|
237
|
+
/* 하루치를 매 방송에 실으면 채널이 그것으로 찬다 — 최근 것만 내고 전체 수를 함께 낸다. */
|
|
238
|
+
assert.equal(agg.data.recent.length, 8)
|
|
239
|
+
assert.equal(agg.data.closedTotal, 137, '자른 사실을 조용히 두지 않는다')
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
test('에너지가 없는 트윈은 에너지 태그를 내지 않는다 — 없는 것을 만들지 않는다', () => {
|
|
243
|
+
const deltas = buildEntityDeltas({ locations: [], equipment: [] }, 'wms-1')
|
|
244
|
+
assert.equal(deltas.some(d => d.tag === energyTag('wms-1')), false)
|
|
245
|
+
assert.equal(deltas.some(d => d.data?.entity === 'meter'), false)
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
test('자리에도 에너지가 실린다 — 능력이 「잰다」고 말하는데 값이 없던 자리', () => {
|
|
249
|
+
const deltas = buildEntityDeltas(
|
|
250
|
+
{
|
|
251
|
+
locations: [
|
|
252
|
+
{ id: 'incoming-1', type: 'incoming' },
|
|
253
|
+
{ id: 'feeder-a', type: 'feeder' },
|
|
254
|
+
{ id: 'feeder-b', type: 'feeder' }
|
|
255
|
+
],
|
|
256
|
+
equipment: [
|
|
257
|
+
{ id: 'm-main', kind: 'meter', homeLocation: 'incoming-1' },
|
|
258
|
+
{ id: 'm-a1', kind: 'meter', homeLocation: 'feeder-a' },
|
|
259
|
+
{ id: 'm-a2', kind: 'meter', homeLocation: 'feeder-a' }
|
|
260
|
+
],
|
|
261
|
+
energy: {
|
|
262
|
+
points: [
|
|
263
|
+
{ id: 'm-main', kW: 3800, kWh: 1000, atMs: Date.parse('2026-08-14T09:00:00.000Z'), samplesInWindow: 3 },
|
|
264
|
+
{ id: 'm-a1', kW: 1000, atMs: Date.parse('2026-08-14T09:01:00.000Z'), samplesInWindow: 2 },
|
|
265
|
+
{ id: 'm-a2', kW: 600, atMs: Date.parse('2026-08-14T09:02:00.000Z'), samplesInWindow: 1 }
|
|
266
|
+
],
|
|
267
|
+
closed: [],
|
|
268
|
+
closedTotal: 0
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
'ems-1'
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
const incoming = deltas.find(d => d.tag === 'incoming-1')!.data
|
|
275
|
+
assert.equal(incoming.entity, 'location', '자리 payload 를 덮지 않고 합친다')
|
|
276
|
+
assert.equal(incoming.kW, 3800)
|
|
277
|
+
assert.deepEqual(incoming.meters, ['m-main'], '어느 계량기에서 온 수인지 되짚을 수 있어야 한다')
|
|
278
|
+
|
|
279
|
+
/* 자리에 계량기가 둘이면 그 자리의 수는 둘의 합이고, 나이는 **가장 최근**이다. */
|
|
280
|
+
const a = deltas.find(d => d.tag === 'feeder-a')!.data
|
|
281
|
+
assert.equal(a.kW, 1600)
|
|
282
|
+
assert.equal(a.samplesInWindow, 3)
|
|
283
|
+
assert.equal(a.measuredAt, '2026-08-14T09:02:00.000Z')
|
|
284
|
+
assert.deepEqual(a.meters, ['m-a1', 'm-a2'])
|
|
285
|
+
|
|
286
|
+
/* 계량기가 없는 자리는 예전 그대로 — 에너지 키를 만들지 않는다(0 은 「안 썼다」가 된다). */
|
|
287
|
+
const b = deltas.find(d => d.tag === 'feeder-b')!.data
|
|
288
|
+
assert.equal('kW' in b, false)
|
|
289
|
+
assert.equal('meters' in b, false)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
test('자리를 모르는 계량기는 자리에 실리지 않는다 — 설비 태그로만 나간다', () => {
|
|
293
|
+
const deltas = buildEntityDeltas(
|
|
294
|
+
{
|
|
295
|
+
locations: [{ id: 'incoming-1', type: 'incoming' }],
|
|
296
|
+
equipment: [],
|
|
297
|
+
energy: { points: [{ id: 'm-orphan', kW: 500, samplesInWindow: 1 }], closed: [], closedTotal: 0 }
|
|
298
|
+
},
|
|
299
|
+
'ems-1'
|
|
300
|
+
)
|
|
301
|
+
assert.equal('kW' in deltas.find(d => d.tag === 'incoming-1')!.data, false)
|
|
302
|
+
assert.equal(deltas.find(d => d.tag === 'm-orphan')!.data.kW, 500, '설비 태그로는 나간다')
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
/*
|
|
306
|
+
* ── 능력 넷의 값이 컴포넌트까지 간다 (2026-08-15) ───────────────────────────
|
|
307
|
+
* 보드에 놓인 컴포넌트는 `data(tag)` 로만 값을 받는다. 커널이 값을 들고 있어도 이 페이로드에 실리지
|
|
308
|
+
* 않으면 화면은 영원히 「—」다 — 실제로 그 부류를 두 번 겪었다(스냅샷 투영이 필드를 빠뜨렸고,
|
|
309
|
+
* 값의 시각을 버렸다).
|
|
310
|
+
*
|
|
311
|
+
* 그래서 **설비 태그의 payload** 를 직접 못 박는다: 값과 그 값의 나이가 함께 실려야 한다.
|
|
312
|
+
*/
|
|
313
|
+
test('발전·저장·감축·개폐 값과 그 나이가 설비 태그로 나간다', () => {
|
|
314
|
+
const deltas = buildEntityDeltas(
|
|
315
|
+
{
|
|
316
|
+
locations: [{ id: 'incoming-1', type: 'incoming' }],
|
|
317
|
+
equipment: [
|
|
318
|
+
{ id: 'pv-1', kind: 'pv-array', homeLocation: 'incoming-1', status: 'idle', generatedKW: 210, exportKW: 40, measuredAt: '2026-08-15T09:00:00.000Z' },
|
|
319
|
+
{ id: 'ess-1', kind: 'battery', homeLocation: 'incoming-1', status: 'idle', soc: 62, chargeKW: 0, dischargeKW: 85, measuredAt: '2026-08-15T09:00:00.000Z' },
|
|
320
|
+
{ id: 'load-1', kind: 'curtailable-load', homeLocation: 'incoming-1', status: 'busy', curtailable: true, minKW: 30, measuredAt: '2026-08-15T09:00:00.000Z' },
|
|
321
|
+
{ id: 'cb-1', kind: 'breaker', homeLocation: 'incoming-1', status: 'idle', position: 'closed', measuredAt: '2026-08-15T09:00:00.000Z' }
|
|
322
|
+
]
|
|
323
|
+
},
|
|
324
|
+
'ems-1'
|
|
325
|
+
)
|
|
326
|
+
const by = Object.fromEntries(deltas.map(d => [d.tag, d.data as any]))
|
|
327
|
+
|
|
328
|
+
assert.equal(by['pv-1'].generatedKW, 210)
|
|
329
|
+
assert.equal(by['pv-1'].exportKW, 40)
|
|
330
|
+
assert.equal(by['ess-1'].soc, 62)
|
|
331
|
+
assert.equal(by['ess-1'].dischargeKW, 85)
|
|
332
|
+
assert.equal(by['load-1'].curtailable, true)
|
|
333
|
+
assert.equal(by['load-1'].minKW, 30)
|
|
334
|
+
assert.equal(by['cb-1'].position, 'closed')
|
|
335
|
+
/* 나이가 없으면 화면이 멈춘 값을 지금 값처럼 그린다 — 네 컴포넌트가 모두 이 키를 읽는다. */
|
|
336
|
+
for (const id of ['pv-1', 'ess-1', 'load-1', 'cb-1']) {
|
|
337
|
+
assert.equal(by[id].measuredAt, '2026-08-15T09:00:00.000Z', `${id}: 값의 나이가 함께 가야 한다`)
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
test('값이 없는 설비에는 그 키를 만들지 않는다 — 0 으로 그리면 「안 쓴다」가 된다', () => {
|
|
342
|
+
const deltas = buildEntityDeltas(
|
|
343
|
+
{
|
|
344
|
+
locations: [{ id: 'incoming-1', type: 'incoming' }],
|
|
345
|
+
equipment: [{ id: 'pv-2', kind: 'pv-array', homeLocation: 'incoming-1', status: 'idle' }]
|
|
346
|
+
},
|
|
347
|
+
'ems-1'
|
|
348
|
+
)
|
|
349
|
+
const pv = deltas.find(d => d.tag === 'pv-2')!.data as any
|
|
350
|
+
assert.equal('generatedKW' in pv, false)
|
|
351
|
+
assert.equal('measuredAt' in pv, false)
|
|
182
352
|
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* eventTime 이 **날짜 컬럼**이라는 사실을 지킨다.
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 있나 (2026-08-08) ────────────────────────────────────────────────────
|
|
5
|
+
* 예전에는 문자열 컬럼이었고, "UTC ISO 만 들어오니 사전식 비교가 맞는다" 는 전제 위에서 정렬·범위
|
|
6
|
+
* 질의가 돌았다. 그 전제는 컬럼 어디에도 적혀 있지 않았다.
|
|
7
|
+
*
|
|
8
|
+
* 날짜 컬럼으로 바꾸면서 **조용한 함정**이 하나 드러났다: sqlite 는 datetime 을 공백 구분 형식
|
|
9
|
+
* (`2026-08-07 11:00:00.000`)으로 저장하는데, 화면은 ISO 문자열(`…T11:00:00.000Z`)을 보낸다.
|
|
10
|
+
* 문자열 그대로 비교하면 사전식으로 어긋나 **오류 없이 0건**이 나온다 — 원장 시간창이 조용히
|
|
11
|
+
* 비어 버리고, 사용자는 "그 시간에 아무 일도 없었다" 로 읽는다.
|
|
12
|
+
*
|
|
13
|
+
* 그래서 두 가지를 못 박는다: 저장 형식이 실제로 그렇다는 것과, 경계에서 날짜로 옮기면 걸린다는 것.
|
|
14
|
+
* 이 검사가 깨지면 typeorm/드라이버의 거동이 바뀐 것이므로 변환 지점을 다시 봐야 한다.
|
|
15
|
+
*/
|
|
16
|
+
import { test } from 'node:test'
|
|
17
|
+
import assert from 'node:assert/strict'
|
|
18
|
+
import { createRequire } from 'node:module'
|
|
19
|
+
|
|
20
|
+
const req = createRequire(import.meta.url)
|
|
21
|
+
const { DataSource, EntitySchema, Between } = req('typeorm')
|
|
22
|
+
|
|
23
|
+
const Ev = new EntitySchema({
|
|
24
|
+
name: 'ev',
|
|
25
|
+
tableName: 'ev',
|
|
26
|
+
columns: {
|
|
27
|
+
id: { primary: true, type: 'int', generated: true },
|
|
28
|
+
eventTime: { type: 'datetime', nullable: true }
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
async function seeded() {
|
|
33
|
+
const ds = new DataSource({ type: 'sqlite', database: ':memory:', entities: [Ev], synchronize: true })
|
|
34
|
+
await ds.initialize()
|
|
35
|
+
const repo = ds.getRepository('ev')
|
|
36
|
+
await repo.save([
|
|
37
|
+
{ eventTime: new Date('2026-08-07T11:00:00.000Z') },
|
|
38
|
+
{ eventTime: new Date('2026-08-07T13:00:00.000Z') }
|
|
39
|
+
])
|
|
40
|
+
return { ds, repo }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
test('sqlite 는 datetime 을 공백 구분 형식으로 저장한다 — ISO 문자열과 사전식으로 어긋난다', async () => {
|
|
44
|
+
const { ds } = await seeded()
|
|
45
|
+
const raw = await ds.query('select eventTime from ev order by id')
|
|
46
|
+
assert.match(raw[0].eventTime, /^\d{4}-\d{2}-\d{2} /, '공백 구분이 아니면 이 검사의 전제가 바뀐 것이다')
|
|
47
|
+
assert.ok(!String(raw[0].eventTime).includes('T'), 'T 가 들어가면 ISO 와 같은 형식이므로 함정이 사라진 것이다')
|
|
48
|
+
await ds.destroy()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('ISO 문자열을 그대로 넘기면 **조용히 0건** — 오류가 아니라 빈 결과라 더 위험하다', async () => {
|
|
52
|
+
const { ds, repo } = await seeded()
|
|
53
|
+
const rows = await repo.find({
|
|
54
|
+
where: { eventTime: Between('2026-08-07T10:00:00.000Z', '2026-08-07T12:00:00.000Z') }
|
|
55
|
+
})
|
|
56
|
+
assert.equal(rows.length, 0, '이 검사가 깨졌다면 드라이버가 문자열을 받아 주게 된 것 — 변환 지점을 재검토할 것')
|
|
57
|
+
await ds.destroy()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('경계에서 날짜로 옮기면 제대로 걸린다 — 질의 전에 반드시 변환한다', async () => {
|
|
61
|
+
const { ds, repo } = await seeded()
|
|
62
|
+
const rows = await repo.find({
|
|
63
|
+
where: { eventTime: Between(new Date('2026-08-07T10:00:00.000Z'), new Date('2026-08-07T12:00:00.000Z')) }
|
|
64
|
+
})
|
|
65
|
+
assert.equal(rows.length, 1)
|
|
66
|
+
await ds.destroy()
|
|
67
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 예측 지표 — **스냅샷에서 무엇을 세는가.**
|
|
3
|
+
*
|
|
4
|
+
* 여기서 틀리면 예측 그래프가 조용히 다른 것을 그린다(오류 없이 곡선이 나온다). 특히 에너지 지표는
|
|
5
|
+
* 「없음」과 「0」이 같은 그림이 되기 쉬워서, 그 경계를 명시로 잡아 둔다.
|
|
6
|
+
*/
|
|
7
|
+
import { test } from 'node:test'
|
|
8
|
+
import assert from 'node:assert/strict'
|
|
9
|
+
|
|
10
|
+
import { FORECAST_METRICS, metricsForKind } from '../dist-server/service/twin-forecast/forecast-metrics.js'
|
|
11
|
+
|
|
12
|
+
test('물류 지표 — 기존 여섯은 그대로 센다', () => {
|
|
13
|
+
const s = {
|
|
14
|
+
items: [1, 2, 3],
|
|
15
|
+
locations: [{ occupancy: 2 }, { occupancy: 5 }],
|
|
16
|
+
tasks: [{}, {}],
|
|
17
|
+
orders: [{ status: 'shipped' }, { status: 'picking' }, { status: 'cancelled' }]
|
|
18
|
+
}
|
|
19
|
+
assert.equal(FORECAST_METRICS.items(s), 3)
|
|
20
|
+
assert.equal(FORECAST_METRICS.occupancy(s), 7)
|
|
21
|
+
assert.equal(FORECAST_METRICS.tasks(s), 2)
|
|
22
|
+
assert.equal(FORECAST_METRICS.orders(s), 3)
|
|
23
|
+
assert.equal(FORECAST_METRICS.shipped(s), 1)
|
|
24
|
+
/* 취소는 백로그가 아니다 — 대기와 취소를 같이 세면 「안 빠지는 일감」이 부풀려진다. */
|
|
25
|
+
assert.equal(FORECAST_METRICS.backlog(s), 1)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('에너지 지표 — 커널이 낸 값을 그대로 읽는다(다시 계산하지 않는다)', () => {
|
|
29
|
+
const s = {
|
|
30
|
+
energy: {
|
|
31
|
+
peakSince: { kW: 812 },
|
|
32
|
+
open: { meanKW: 300, projectedKW: 455, projectionBasis: 'mean-so-far' },
|
|
33
|
+
closed: [{ overContract: true }, { overContract: false }, { overContract: true }]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
assert.equal(FORECAST_METRICS.peakKW(s), 812)
|
|
37
|
+
assert.equal(FORECAST_METRICS.projectedKW(s), 455)
|
|
38
|
+
assert.equal(FORECAST_METRICS.overWindows(s), 2)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('투영이 아직 없으면 지금까지의 평균으로 — 0 으로 떨어뜨리지 않는다', () => {
|
|
42
|
+
assert.equal(FORECAST_METRICS.projectedKW({ energy: { open: { meanKW: 120 } } }), 120)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('에너지가 없는 스냅샷 — 세 지표 모두 0(그리지 않을 값이지, 관측한 0 이 아니다)', () => {
|
|
46
|
+
assert.equal(FORECAST_METRICS.peakKW({ items: [] }), 0)
|
|
47
|
+
assert.equal(FORECAST_METRICS.projectedKW({ items: [] }), 0)
|
|
48
|
+
assert.equal(FORECAST_METRICS.overWindows({ items: [] }), 0)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('계약을 모르면 초과 구간은 0 — overContract 가 없는 구간을 초과로 세지 않는다', () => {
|
|
52
|
+
const s = { energy: { closed: [{ maxKW: 900 }, { maxKW: 1200 }] } }
|
|
53
|
+
assert.equal(FORECAST_METRICS.overWindows(s), 0)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('종류별 지표 목록 — 에너지 트윈에는 물류 지표가 섞이지 않는다', () => {
|
|
57
|
+
const ems = metricsForKind('ems')
|
|
58
|
+
assert.deepEqual(ems, ['projectedKW', 'peakKW', 'overWindows'])
|
|
59
|
+
assert.equal(ems.includes('items' as any), false)
|
|
60
|
+
assert.equal(metricsForKind('wms').includes('items'), true)
|
|
61
|
+
/* 종류를 모르면 물류로 — 기존 트윈(kind 미선언)이 지표를 잃지 않는다. */
|
|
62
|
+
assert.equal(metricsForKind(undefined).includes('items'), true)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('목록의 모든 키가 실제로 계산 가능하다 — 화면에 뜨는데 서버가 모르는 지표는 없다', () => {
|
|
66
|
+
for (const kind of ['ems', 'wms', 'mes', 'yms']) {
|
|
67
|
+
for (const key of metricsForKind(kind)) {
|
|
68
|
+
assert.equal(typeof (FORECAST_METRICS as any)[key], 'function', `${kind}:${key}`)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 예측 틱 크기 — **지평선이 정하고, 양끝은 묶인다.**
|
|
3
|
+
*
|
|
4
|
+
* 이 규칙이 틀리면 예측이 조용히 비싸지거나(1초 고정 시절 9,000틱·28.5초) 조용히 성글어진다
|
|
5
|
+
* (짧은 작업이 한 틱에 삼켜진다). 둘 다 화면에는 그럴듯한 숫자로 나오므로 시험이 지킨다.
|
|
6
|
+
*/
|
|
7
|
+
import { test } from 'node:test'
|
|
8
|
+
import assert from 'node:assert/strict'
|
|
9
|
+
|
|
10
|
+
import { forecastStepMs, TARGET_TICKS_PER_RUN, MAX_STEP_MS, MIN_STEP_MS } from '../dist-server/service/twin-forecast/forecast-tuning.js'
|
|
11
|
+
|
|
12
|
+
test('기본 지평선(5분)은 5초 단위 — 실측으로 1초와 답이 같고 5배 싸다', () => {
|
|
13
|
+
assert.equal(forecastStepMs(300_000), 5_000)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('짧은 지평선은 더 잘게 — 1분이면 1초', () => {
|
|
17
|
+
assert.equal(forecastStepMs(60_000), 1_000)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('아주 짧아도 커널 시각 단위 아래로 내려가지 않는다', () => {
|
|
21
|
+
assert.equal(forecastStepMs(5_000), MIN_STEP_MS)
|
|
22
|
+
assert.equal(forecastStepMs(0), MIN_STEP_MS)
|
|
23
|
+
assert.equal(forecastStepMs(Number.NaN), MIN_STEP_MS)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('아무리 길어도 상한을 넘지 않는다 — 짧은 작업이 한 틱에 삼켜진다', () => {
|
|
27
|
+
assert.equal(forecastStepMs(2 * 60 * 60_000), MAX_STEP_MS)
|
|
28
|
+
assert.equal(forecastStepMs(24 * 60 * 60_000), MAX_STEP_MS)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('틱 크기는 초 단위로 떨어진다 — 3,333ms 는 사람도 화면도 설명할 수 없다', () => {
|
|
32
|
+
for (const h of [100_000, 200_000, 333_000, 777_000]) {
|
|
33
|
+
assert.equal(forecastStepMs(h) % 1000, 0, `${h}ms`)
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('상한에 걸리지 않는 구간에서는 목표 틱 수 근처로 돈다', () => {
|
|
38
|
+
for (const h of [60_000, 120_000, 300_000, 600_000]) {
|
|
39
|
+
const ticks = h / forecastStepMs(h)
|
|
40
|
+
assert.ok(ticks >= TARGET_TICKS_PER_RUN * 0.8 && ticks <= TARGET_TICKS_PER_RUN * 1.25, `${h}ms → ${ticks}틱`)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
@@ -52,11 +52,38 @@ test('learnedCalibration — biasShift=평균오차, 편향 제거로 정확도
|
|
|
52
52
|
assert.equal(learn.accAfter, 100, '일정 편향을 완전 제거 → 잔차 0 → 100%')
|
|
53
53
|
})
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
/* 같은 격자 위의 점 — vantage 를 명시한다(예전 헬퍼는 t 를 늘 0 으로 두어 시간축을 전혀 시험하지 않았다). */
|
|
56
|
+
const at = (t: number, error: number, actual = 100) => ({ ...pt(error, actual), t })
|
|
57
|
+
|
|
58
|
+
test('aggregatePoints — 공동배치 합산(공장 총량)', () => {
|
|
59
|
+
const a = [at(10, 2, 30), at(20, 4, 30)]
|
|
60
|
+
const b = [at(10, 1, 20), at(20, 2, 20)]
|
|
61
|
+
const { points, droppedVantages } = aggregatePoints([a, b])
|
|
62
|
+
assert.equal(points.length, 2)
|
|
63
|
+
assert.equal(droppedVantages, 0)
|
|
64
|
+
assert.equal(points[0].actual, 50, '실제 30+20')
|
|
65
|
+
assert.equal(points[0].t, 10, '합산점의 시각은 그 vantage 다')
|
|
66
|
+
assert.equal(points[0].error, points[0].actual - points[0].p50, 'error = 실제−예측(합산 후 재계산)')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('한 트윈이 못 잰 vantage 는 통째로 뺀다 — 빠진 트윈을 0 으로 세지 않는다', () => {
|
|
70
|
+
/*
|
|
71
|
+
* 예전에는 `max` 길이로 돌며 있는 것만 더했다. 그래서 이 경우 t=20 의 공장 총량이 20(b 뿐)으로
|
|
72
|
+
* 나왔다 — a 가 그 시각에 아무것도 안 했다는 뜻이 아니라, **못 쟀다**는 뜻인데도.
|
|
73
|
+
* 공장 합계 곡선이 꼬리에서 아래로 꺾이고, 그 곡선으로 예측 정확도를 판정했다.
|
|
74
|
+
*/
|
|
75
|
+
const a = [at(10, 2, 30), null]
|
|
76
|
+
const b = [at(10, 1, 20), at(20, 2, 20)]
|
|
77
|
+
const { points, droppedVantages } = aggregatePoints([a as any, b])
|
|
78
|
+
assert.equal(points.length, 1, '모두가 값을 가진 vantage 만 남는다')
|
|
79
|
+
assert.equal(points[0].t, 10)
|
|
80
|
+
assert.equal(droppedVantages, 1, '뺀 개수를 돌려줘야 화면이 "2점 중 1점" 이라고 말할 수 있다')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('길이가 다르면 뒤쪽은 뺀다 — 남은 트윈만의 합을 공장 총량이라 부르지 않는다', () => {
|
|
84
|
+
const a = [at(10, 2, 30)]
|
|
85
|
+
const b = [at(10, 1, 20), at(20, 2, 20)]
|
|
86
|
+
const { points, droppedVantages } = aggregatePoints([a, b])
|
|
87
|
+
assert.equal(points.length, 1)
|
|
88
|
+
assert.equal(droppedVantages, 1)
|
|
62
89
|
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* 커널 인제스트 인터페이스 부하 실측(가정 아닌 측정) — Face1/커널 동기 인터페이스의 처리량 특성.
|
|
3
|
-
* apply(이벤트 흡수) · snapshot(전체 투영) · buildEntityDeltas(방송 payload) 의 비용을 규모별로 잰다.
|
|
3
|
+
* apply(이벤트 흡수) · snapshot(전체 투영) · buildEntityDeltas(방송 payload, 't1') 의 비용을 규모별로 잰다.
|
|
4
4
|
* 결론은 design/integration/ingest-scale.md §커널 인터페이스에 스펙으로 반영.
|
|
5
5
|
* (순수 커널+호스트 매핑, shell 무관 → node:test.)
|
|
6
6
|
*/
|
|
@@ -8,10 +8,10 @@ import { test } from 'node:test'
|
|
|
8
8
|
import assert from 'node:assert/strict'
|
|
9
9
|
|
|
10
10
|
import { StateProjector, objectEvent, BIZSTEP, DISP } from '@operato/twin-kernel'
|
|
11
|
-
import { buildEntityDeltas } from '../
|
|
11
|
+
import { buildEntityDeltas } from '../shared/entity-delta.ts'
|
|
12
12
|
|
|
13
13
|
const T0 = '2026-07-19T00:00:00.000Z'
|
|
14
|
-
const
|
|
14
|
+
const model = (nNodes: number) => ({
|
|
15
15
|
locations: Array.from({ length: nNodes }, (_, i) => ({ id: `bin-${i}`, type: 'storage', capacity: 100000 })),
|
|
16
16
|
equipment: []
|
|
17
17
|
})
|
|
@@ -23,7 +23,7 @@ function envelope(epc: string, location: string): any {
|
|
|
23
23
|
test('bench: apply 처리량(이벤트 흡수)', () => {
|
|
24
24
|
const nNodes = 200
|
|
25
25
|
const M = 200_000
|
|
26
|
-
const proj = new StateProjector(
|
|
26
|
+
const proj = new StateProjector(model(nNodes))
|
|
27
27
|
const t0 = performance.now()
|
|
28
28
|
for (let i = 0; i < M; i++) proj.apply(envelope(`urn:epc:id:sscc:0614141.${i}`, `bin-${i % nNodes}`))
|
|
29
29
|
const dt = performance.now() - t0
|
|
@@ -33,7 +33,7 @@ test('bench: apply 처리량(이벤트 흡수)', () => {
|
|
|
33
33
|
|
|
34
34
|
test('bench: snapshot 비용 vs item 수 (전체 O(N) 재구성)', () => {
|
|
35
35
|
for (const N of [1000, 10000, 50000]) {
|
|
36
|
-
const proj = new StateProjector(
|
|
36
|
+
const proj = new StateProjector(model(200))
|
|
37
37
|
for (let i = 0; i < N; i++) proj.apply(envelope(`urn:epc:id:sscc:0614141.${i}`, `bin-${i % 200}`))
|
|
38
38
|
const iters = 200
|
|
39
39
|
const t0 = performance.now()
|
|
@@ -45,12 +45,12 @@ test('bench: snapshot 비용 vs item 수 (전체 O(N) 재구성)', () => {
|
|
|
45
45
|
|
|
46
46
|
test('bench: buildEntityDeltas 비용 vs 엔티티 수 (전 엔티티 매번 방출)', () => {
|
|
47
47
|
for (const N of [100, 1000, 5000]) {
|
|
48
|
-
const proj = new StateProjector(
|
|
48
|
+
const proj = new StateProjector(model(N))
|
|
49
49
|
for (let i = 0; i < N * 5; i++) proj.apply(envelope(`urn:epc:id:sscc:0614141.${i}`, `bin-${i % N}`))
|
|
50
50
|
const snap = proj.snapshot()
|
|
51
51
|
const iters = 200
|
|
52
52
|
const t0 = performance.now()
|
|
53
|
-
for (let k = 0; k < iters; k++) buildEntityDeltas(snap)
|
|
53
|
+
for (let k = 0; k < iters; k++) buildEntityDeltas(snap, 't1')
|
|
54
54
|
const per = (performance.now() - t0) / iters
|
|
55
55
|
console.log(` [buildEntityDeltas] @ ${N.toLocaleString()} locations: ${per.toFixed(3)}ms/call`)
|
|
56
56
|
}
|
|
@@ -58,11 +58,11 @@ test('bench: buildEntityDeltas 비용 vs 엔티티 수 (전 엔티티 매번 방
|
|
|
58
58
|
|
|
59
59
|
test('bench: 방송 1회 총비용(snapshot + buildEntityDeltas) vs 상태 규모', () => {
|
|
60
60
|
for (const [locations, items] of [[200, 5000], [1000, 50000], [2000, 100000]]) {
|
|
61
|
-
const proj = new StateProjector(
|
|
61
|
+
const proj = new StateProjector(model(locations))
|
|
62
62
|
for (let i = 0; i < items; i++) proj.apply(envelope(`urn:epc:id:sscc:0614141.${i}`, `bin-${i % locations}`))
|
|
63
63
|
const iters = 100
|
|
64
64
|
const t0 = performance.now()
|
|
65
|
-
for (let k = 0; k < iters; k++) buildEntityDeltas(proj.snapshot())
|
|
65
|
+
for (let k = 0; k < iters; k++) buildEntityDeltas(proj.snapshot(), 't1')
|
|
66
66
|
const per = (performance.now() - t0) / iters
|
|
67
67
|
console.log(` [방송1회] ${locations.toLocaleString()} locations / ${items.toLocaleString()} items: ${per.toFixed(3)}ms → 최대 ${Math.round(1000 / per).toLocaleString()} 방송/s`)
|
|
68
68
|
}
|