@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
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ 주의 신호 요약 — 신호가 많은 트윈의 전송량.
|
|
3
|
+
*
|
|
4
|
+
* ── 계기 ────────────────────────────────────────────────────────────────────
|
|
5
|
+
* 축 선택으로 리프레시를 80KB → 2KB 로 줄였는데 **신호가 많은 트윈은 거기서 막혔다**:
|
|
6
|
+
* `hatio-yard` 는 신호 397건이 108KB 라 5배 감소에 그쳤다. 그런데 화면이 그 397건으로 하는 일은
|
|
7
|
+
* 둘뿐이다 — 레일에 20건만 그리고, 자리 7곳의 색을 뽑는다.
|
|
8
|
+
*
|
|
9
|
+
* ── 이 테스트가 지키는 것 ───────────────────────────────────────────────────
|
|
10
|
+
* ① **자리 색은 전량을 근거로** 접힌다. 상위 N 으로 접으면 20위 밖의 신호를 가진 자리가 색을 잃는다.
|
|
11
|
+
* ② **조용히 자르지 않는다.** 전체 개수를 함께 보내야 레일이 "N건 더" 를 말할 수 있다.
|
|
12
|
+
* ③ **순서가 화면과 같다.** 서버가 고른 20건이 화면이 고를 20건과 달라지면 아무 데서도 안 드러난다.
|
|
13
|
+
*/
|
|
14
|
+
import { test } from 'node:test'
|
|
15
|
+
import assert from 'node:assert/strict'
|
|
16
|
+
|
|
17
|
+
import { digestAttentions, compareAttentions, mergeLensAttentions } from '../server/engine/attention-digest.ts'
|
|
18
|
+
|
|
19
|
+
const attn = (id: string, severity: string, locationId?: string, state?: string) => ({ id, severity, state, anchor: locationId ? { locationId } : undefined })
|
|
20
|
+
|
|
21
|
+
test('상위 N 만 남기고 전체 개수를 함께 낸다 — 조용히 자르지 않는다', () => {
|
|
22
|
+
const all = Array.from({ length: 50 }, (_, i) => attn(`a${i}`, 'low', `loc-${i % 3}`))
|
|
23
|
+
const d = digestAttentions(all, 20)
|
|
24
|
+
assert.equal(d.attentions.length, 20)
|
|
25
|
+
assert.equal(d.attentionTotal, 50, '전체 개수가 없으면 화면이 "괜찮아졌다" 로 읽는다')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('자리 색은 전량을 근거로 접는다 — 상위 N 밖의 자리도 색을 갖는다', () => {
|
|
29
|
+
/* 앞 20건은 loc-hot 에, 21번째 한 건만 loc-cold 에 — 잘린 뒤에 접으면 loc-cold 가 색을 잃는다. */
|
|
30
|
+
const all = [...Array.from({ length: 20 }, (_, i) => attn(`hot${i}`, 'high', 'loc-hot')), attn('cold', 'critical', 'loc-cold')]
|
|
31
|
+
const d = digestAttentions(all, 20)
|
|
32
|
+
assert.equal(d.attentions.length, 20)
|
|
33
|
+
assert.equal(d.severityByLocation['loc-cold'], 'critical', '상위 N 밖의 신호도 자리 색에는 반영돼야 한다')
|
|
34
|
+
assert.equal(d.severityByLocation['loc-hot'], 'high')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('자리마다 가장 급한 심각도가 남는다', () => {
|
|
38
|
+
const d = digestAttentions([attn('a', 'low', 'n1'), attn('b', 'critical', 'n1'), attn('c', 'medium', 'n1')])
|
|
39
|
+
assert.equal(d.severityByLocation['n1'], 'critical')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('순서는 화면과 같다 — 확인된 것은 아래로, 그다음 심각도순', () => {
|
|
43
|
+
const all = [attn('acked-crit', 'critical', 'n1', 'acknowledged'), attn('low', 'low', 'n2'), attn('high', 'high', 'n3')]
|
|
44
|
+
const d = digestAttentions(all, 2)
|
|
45
|
+
assert.deepEqual(d.attentions.map(a => a.id), ['high', 'low'], '확인된 critical 보다 확인 안 된 것이 먼저다')
|
|
46
|
+
assert.equal(d.attentionTotal, 3)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('한도를 지정하지 않으면 자르지 않는다 — 이 값을 모르는 소비처가 일부만 받으면 안 된다', () => {
|
|
50
|
+
const all = Array.from({ length: 30 }, (_, i) => attn(`a${i}`, 'low'))
|
|
51
|
+
assert.equal(digestAttentions(all).attentions.length, 30)
|
|
52
|
+
assert.equal(digestAttentions(all, 0).attentions.length, 30)
|
|
53
|
+
assert.equal(digestAttentions(all, -5).attentions.length, 30)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('자리에 붙지 않은 신호는 색 요약에 들어가지 않는다 — 없는 자리를 만들지 않는다', () => {
|
|
57
|
+
const d = digestAttentions([attn('orphan', 'critical'), attn('anchored', 'low', 'n1')])
|
|
58
|
+
assert.deepEqual(Object.keys(d.severityByLocation), ['n1'])
|
|
59
|
+
assert.equal(d.attentionTotal, 2, '색에 안 들어가도 개수에는 든다')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('신호가 없으면 빈 요약 — 없는 것을 만들지 않는다', () => {
|
|
63
|
+
for (const v of [undefined, null, []]) {
|
|
64
|
+
const d = digestAttentions(v as any, 20)
|
|
65
|
+
assert.deepEqual(d.attentions, [])
|
|
66
|
+
assert.equal(d.attentionTotal, 0)
|
|
67
|
+
assert.deepEqual(d.severityByLocation, {})
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('정렬 함수는 원본을 바꾸지 않는다 — 스냅샷을 제자리에서 흔들면 다른 소비처가 영향받는다', () => {
|
|
72
|
+
const all = [attn('low', 'low'), attn('crit', 'critical')]
|
|
73
|
+
const before = all.map(a => a.id)
|
|
74
|
+
digestAttentions(all, 1)
|
|
75
|
+
assert.deepEqual(all.map(a => a.id), before)
|
|
76
|
+
assert.equal(compareAttentions(all[1], all[0]) < 0, true, 'critical 이 앞이다')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
/*
|
|
80
|
+
* ── 여러 렌즈를 한 현장으로 모으기 (2026-08-05) ────────────────────────────
|
|
81
|
+
* 한 공장에 WMS·MES·YMS 가 함께 있으므로 "이 현장에서 봐야 할 것" 은 렌즈 하나로 답할 수 없다.
|
|
82
|
+
* 예전에는 화면이 인스턴스마다 물어 손으로 합쳤고, 그러면 정렬 규칙이 화면마다 갈린다.
|
|
83
|
+
*
|
|
84
|
+
* 모으면서 조용히 틀릴 수 있는 자리 셋을 여기서 못 박는다.
|
|
85
|
+
*/
|
|
86
|
+
test('★ 어느 트윈에서 온 신호인지 붙는다 — 조치는 결국 그 트윈에 보내야 한다', () => {
|
|
87
|
+
const d = mergeLensAttentions([
|
|
88
|
+
{ instanceId: 'wms', attentions: [attn('a', 'high', 'dock-1')] },
|
|
89
|
+
{ instanceId: 'yms', attentions: [attn('b', 'low', 'gate-1')] }
|
|
90
|
+
])
|
|
91
|
+
assert.deepEqual(d.attentions.map(a => [a.id, (a as any).instanceId]), [['a', 'wms'], ['b', 'yms']])
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('★ 순서는 렌즈 순서가 아니라 급한 순서다 — 뒤 렌즈의 심각한 신호가 밀려나면 안 된다', () => {
|
|
95
|
+
const d = mergeLensAttentions(
|
|
96
|
+
[
|
|
97
|
+
{ instanceId: 'wms', attentions: [attn('low-1', 'low'), attn('low-2', 'low')] },
|
|
98
|
+
{ instanceId: 'mes', attentions: [attn('crit', 'critical')] }
|
|
99
|
+
],
|
|
100
|
+
1
|
|
101
|
+
)
|
|
102
|
+
assert.deepEqual(d.attentions.map(a => a.id), ['crit'], '상위 1건은 뒤 렌즈의 critical 이어야 한다')
|
|
103
|
+
assert.equal(d.attentionTotal, 3)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('★ 두 렌즈가 같은 대상을 지목하면 더 급한 쪽이 색을 정한다 — 하나의 현실이므로', () => {
|
|
107
|
+
const d = mergeLensAttentions([
|
|
108
|
+
{ instanceId: 'wms', attentions: [attn('a', 'low', 'dock-1')] },
|
|
109
|
+
{ instanceId: 'yms', attentions: [attn('b', 'critical', 'dock-1')] }
|
|
110
|
+
])
|
|
111
|
+
assert.equal(d.severityByLocation['dock-1'], 'critical')
|
|
112
|
+
/* 렌즈 순서를 뒤집어도 같아야 한다 — 색이 순서에 따라 달라지면 화면이 새로고침마다 바뀐다. */
|
|
113
|
+
const flipped = mergeLensAttentions([
|
|
114
|
+
{ instanceId: 'yms', attentions: [attn('b', 'critical', 'dock-1')] },
|
|
115
|
+
{ instanceId: 'wms', attentions: [attn('a', 'low', 'dock-1')] }
|
|
116
|
+
])
|
|
117
|
+
assert.equal(flipped.severityByLocation['dock-1'], 'critical')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('★ 신호가 없는 렌즈는 아무것도 보태지 않는다 — 빈 렌즈가 개수를 늘리지 않게', () => {
|
|
121
|
+
const d = mergeLensAttentions([
|
|
122
|
+
{ instanceId: 'wms', attentions: [attn('a', 'low')] },
|
|
123
|
+
{ instanceId: 'mes', attentions: [] },
|
|
124
|
+
{ instanceId: 'yms', attentions: undefined }
|
|
125
|
+
])
|
|
126
|
+
assert.equal(d.attentionTotal, 1)
|
|
127
|
+
assert.deepEqual(d.attentions.map(a => a.id), ['a'])
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('★ 렌즈가 하나도 없으면 빈 요약 — 공간에 배치된 트윈이 없을 때', () => {
|
|
131
|
+
const d = mergeLensAttentions([], 20)
|
|
132
|
+
assert.deepEqual(d.attentions, [])
|
|
133
|
+
assert.equal(d.attentionTotal, 0)
|
|
134
|
+
assert.deepEqual(d.severityByLocation, {})
|
|
135
|
+
})
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 축의 값을 어디서 읽나 — **선언이 말한 자리에서** (§10 6.5단계)
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 있나 ────────────────────────────────────────────────────────────────
|
|
5
|
+
* 개념 지도는 상태 축을 `state[축이름]` 으로 읽었다. 에너지의 수요 구간은 `state.energy.closed` 에
|
|
6
|
+
* 살아서 그 규칙으로는 **언제나 `undefined`** 다 — 카드가 「관측 없음」으로 굳는다. 축은 선언돼 있고
|
|
7
|
+
* 상태에도 값이 있는데 이름이 안 맞을 뿐이라, 화면도 서버도 그 원인을 말해 주지 않는다.
|
|
8
|
+
*
|
|
9
|
+
* 이 시험이 지키는 것은 **모름과 없음의 구별**이다: 경로 중간이 비면 `undefined`(모른다)이고, 배열이
|
|
10
|
+
* 비면 빈 배열(없다)이다. 그 둘을 섞으면 「아직 안 온 것」과 「하나도 없는 것」이 같아진다.
|
|
11
|
+
*/
|
|
12
|
+
import { test } from 'node:test'
|
|
13
|
+
import assert from 'node:assert/strict'
|
|
14
|
+
|
|
15
|
+
import { atPath, axisValueOf } from '../shared/axis-read.ts'
|
|
16
|
+
|
|
17
|
+
test('경로가 없는 축은 이름으로 읽는다 — 지금까지의 거동', () => {
|
|
18
|
+
const state = { orders: [{ id: 'o1' }], tasks: [] }
|
|
19
|
+
assert.deepEqual(axisValueOf(state, { axis: 'orders' }), [{ id: 'o1' }])
|
|
20
|
+
assert.deepEqual(axisValueOf(state, { axis: 'tasks' }), [], '빈 배열은 「없다」다 — 모른다가 아니다')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('경로가 있으면 그 경로로 읽는다 — 중첩된 축이 조용히 사라지지 않게', () => {
|
|
24
|
+
const state = { energy: { closed: [{ startMs: 1 }, { startMs: 2 }] } }
|
|
25
|
+
const value = axisValueOf(state, { axis: 'demandWindows', path: 'energy.closed' })
|
|
26
|
+
assert.equal(Array.isArray(value), true)
|
|
27
|
+
assert.equal(value.length, 2)
|
|
28
|
+
/* 축 이름으로 읽으면 없다 — 그것이 이 함수가 생긴 이유다. */
|
|
29
|
+
assert.equal((state as any).demandWindows, undefined)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('경로 중간이 비면 모른다 — 빈 배열로 메우지 않는다', () => {
|
|
33
|
+
assert.equal(axisValueOf({}, { axis: 'demandWindows', path: 'energy.closed' }), undefined)
|
|
34
|
+
assert.equal(axisValueOf({ energy: {} }, { axis: 'demandWindows', path: 'energy.closed' }), undefined)
|
|
35
|
+
/* 에너지 트윈이 아직 표본을 못 받았을 때가 이 경우다 — 「구간 0」이 아니라 「모름」이다. */
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('상태가 없으면 모른다 — 커널이 멈춰 있으면 0 이 아니다', () => {
|
|
39
|
+
assert.equal(axisValueOf(null, { axis: 'orders' }), undefined)
|
|
40
|
+
assert.equal(axisValueOf(undefined, { axis: 'demandWindows', path: 'energy.closed' }), undefined)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('경로 따라가기는 중간 부재에서 멈춘다 — 던지지 않는다', () => {
|
|
44
|
+
assert.equal(atPath({ a: { b: 1 } }, 'a.b'), 1)
|
|
45
|
+
assert.equal(atPath({ a: null }, 'a.b'), undefined)
|
|
46
|
+
assert.equal(atPath(undefined, 'a.b'), undefined)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('커널 선언과 상태의 모양이 맞는다 — 선언한 경로에 정말 값이 있다', async () => {
|
|
50
|
+
/*
|
|
51
|
+
* 선언(`path`)과 실제 상태의 모양이 어긋나면 이 배선은 조용히 빈 카드를 만든다. 그래서 **커널이 낸
|
|
52
|
+
* 상태로** 확인한다 — 선언을 읽는 쪽과 값을 만드는 쪽이 같은 모양을 쓰는지 보는 것이 요점이다.
|
|
53
|
+
*/
|
|
54
|
+
const { EmsKernel, axisInfo, ENERGY_EVENT, DEMAND_WINDOW_MS } = await import('@operato/twin-kernel')
|
|
55
|
+
const info = axisInfo('demandWindows')!
|
|
56
|
+
assert.ok(info, '커널이 그 축을 선언해야 한다')
|
|
57
|
+
assert.equal(info.path, 'energy.closed')
|
|
58
|
+
|
|
59
|
+
const k = new EmsKernel('t')
|
|
60
|
+
k.loadTwinModel({ locations: [{ id: 'i', type: 'incoming', capacity: 0 }], equipment: [] })
|
|
61
|
+
const at = Date.parse('2026-08-14T05:00:00.000Z')
|
|
62
|
+
const sample = (whenMs: number, kW: number) => ({
|
|
63
|
+
eventId: `e${whenMs}`,
|
|
64
|
+
eventType: ENERGY_EVENT.measured,
|
|
65
|
+
eventTime: new Date(whenMs).toISOString(),
|
|
66
|
+
tenantId: 't',
|
|
67
|
+
data: { meterId: 'm', kW, at: new Date(whenMs).toISOString() }
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
/*
|
|
71
|
+
* 표본이 오기 전에는 **빈 목록**이다 — 그리고 그것이 맞다.
|
|
72
|
+
*
|
|
73
|
+
* 처음에는 `undefined`(모름)를 기대했다가 이 시험에 걸렸다. 판단해 보면 빈 목록이 옳다: 커널은
|
|
74
|
+
* 관측 중이고, 수요 구간은 15분이 지나야 닫힌다. 「아직 하나도 마감되지 않았다」는 **참인 사실**이고
|
|
75
|
+
* 개념 지도가 그것을 `none-yet` 으로 그린다.
|
|
76
|
+
*
|
|
77
|
+
* 「모른다」는 다른 경우다 — 커널이 멈춰 있어 상태 자체가 없을 때(위 시험). 그 둘을 섞지 않는 것이
|
|
78
|
+
* 이 배선의 요점이다.
|
|
79
|
+
*/
|
|
80
|
+
assert.deepEqual(axisValueOf(k.getSnapshot(), info), [], '관측 중인데 아직 마감이 없으면 빈 목록이다')
|
|
81
|
+
|
|
82
|
+
k.apply(sample(at + 60_000, 100) as any)
|
|
83
|
+
k.apply(sample(at + DEMAND_WINDOW_MS + 60_000, 120) as any) // 앞 구간 마감
|
|
84
|
+
const windows = axisValueOf(k.getSnapshot(), info)
|
|
85
|
+
assert.equal(Array.isArray(windows), true, '선언한 경로에 목록이 있어야 한다')
|
|
86
|
+
assert.equal(windows.length, 1, '마감된 구간 하나')
|
|
87
|
+
})
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 재기동을 넘어 **도는 트윈이 계속 돈다** — 그리고 되살리지 못하면 그렇게 말한다.
|
|
3
|
+
*
|
|
4
|
+
* ── 실측 (2026-08-14) ───────────────────────────────────────────────────────
|
|
5
|
+
* 서버를 한 번 재기동하면 등록부는 `running` 이라 말하는데 **아무 커널도 돌지 않았다**.
|
|
6
|
+
* `bootstrap()` 은 상태만 되찾아 `recovered` 에 담았고, 커널을 세우는 것은 명시 mutation 뿐이었다.
|
|
7
|
+
* 미러 트윈에서는 계측까지 끊겼다 — `startReferenceLiveFeed` 를 부르는 곳이 `importTwinReference`
|
|
8
|
+
* 하나뿐이어서, 붙어 있던 커넥터가 조용히 떨어졌다(오류도 로그도 없이).
|
|
9
|
+
*
|
|
10
|
+
* 이 시험이 지키는 것 넷:
|
|
11
|
+
* ① 부팅이 런타임까지 되살린다(상태 복구만으로는 도는 트윈이 아니다)
|
|
12
|
+
* ② 모드를 지어내지 않는다 — 미러였던 것은 미러로(시뮬로 되살리면 없던 움직임을 만든다)
|
|
13
|
+
* ③ 되살리지 못한 행은 `stopped` 로 적는다(우리가 고치려던 거짓말이 남지 않게)
|
|
14
|
+
* ④ 부팅 훅이 **엔진 다음에** 피드를 다시 붙인다
|
|
15
|
+
*
|
|
16
|
+
* 엔진과 레퍼런스 계층은 DB·shell 을 물고 있어 단위 시험이 불러올 수 없다. 그래서 **배선의 모양**을
|
|
17
|
+
* 소스로 지킨다 — 이 부류의 결함은 「부르는 곳이 없다」는 모양이고, 그건 소스에서 보인다.
|
|
18
|
+
*/
|
|
19
|
+
import { test } from 'node:test'
|
|
20
|
+
import assert from 'node:assert/strict'
|
|
21
|
+
import { readFileSync } from 'node:fs'
|
|
22
|
+
import { fileURLToPath } from 'node:url'
|
|
23
|
+
|
|
24
|
+
const engine = readFileSync(fileURLToPath(new URL('../server/engine/twin-engine.ts', import.meta.url)), 'utf-8')
|
|
25
|
+
const live = readFileSync(fileURLToPath(new URL('../server/service/reference/reference-live.ts', import.meta.url)), 'utf-8')
|
|
26
|
+
const boot = readFileSync(fileURLToPath(new URL('../server/index.ts', import.meta.url)), 'utf-8')
|
|
27
|
+
|
|
28
|
+
/** 그 정적 메서드의 본문만 — 다음 메서드 선언까지. */
|
|
29
|
+
function methodOf(src: string, name: string): string {
|
|
30
|
+
const at = [`static ${name}(`, `static async ${name}(`].map(sig => src.indexOf(sig)).filter(i => i >= 0).sort((a, b) => a - b)[0]
|
|
31
|
+
assert.ok(at !== undefined, `${name} 가 없다 — 이름이 바뀌었으면 이 검사도 함께 고친다`)
|
|
32
|
+
const ends = ['\n static ', '\n private static ', '\n /**'].map(m => src.indexOf(m, at + 10)).filter(i => i >= 0)
|
|
33
|
+
const end = ends.sort((a, b) => a - b)[0]
|
|
34
|
+
return src.slice(at, end === undefined ? undefined : end)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
test('부팅이 런타임까지 되살린다 — 상태 복구만으로는 도는 트윈이 아니다', () => {
|
|
38
|
+
const body = methodOf(engine, 'bootstrap')
|
|
39
|
+
assert.match(body, /resumeRow\(row\)/, 'bootstrap 이 행마다 재개를 시도해야 한다')
|
|
40
|
+
/* 예전 모양: 스냅샷을 `recovered` 에 담고 `continue` 로 끝났다 — 그것으로 되돌아가지 않게. */
|
|
41
|
+
assert.match(body, /for \(const row of rows\) await this\.resumeRow\(row\)/, '복구 뒤 재개 루프가 있어야 한다')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('모드를 지어내지 않는다 — 미러였던 것은 미러로 되살린다', () => {
|
|
45
|
+
const body = methodOf(engine, 'resumeRow')
|
|
46
|
+
assert.match(body, /realityMode === 'mirror'/, '선언된 모드를 보고 갈라야 한다')
|
|
47
|
+
assert.match(body, /this\.startLive\(/, '미러는 관측 구동으로 세운다')
|
|
48
|
+
assert.match(body, /this\.startFromRegistry\(/, '시뮬은 등록부의 모드 그대로 세운다')
|
|
49
|
+
/*
|
|
50
|
+
* 미러 가지 안을 그대로 떠서 본다 — 미러를 시뮬로 세우면 관측 트윈이 **스스로 물건을 옮긴다**
|
|
51
|
+
* (없던 움직임을 만드는 것이다). 단어의 거리로 재면 로그·주석에 걸려 헛돈다.
|
|
52
|
+
*/
|
|
53
|
+
const at = body.indexOf("realityMode === 'mirror'")
|
|
54
|
+
const mirrorBranch = body.slice(at, body.indexOf('} else {', at))
|
|
55
|
+
assert.match(mirrorBranch, /startLive\(/, '미러 가지는 관측 구동으로 세워야 한다')
|
|
56
|
+
assert.equal(mirrorBranch.includes('startFromRegistry'), false, '미러 가지에서 시뮬 기동을 부른다 — 없던 움직임을 만든다')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('되살리지 못한 행은 stopped 로 적는다 — 「도는 중」이라 말하는 채로 두지 않는다', () => {
|
|
60
|
+
const resume = methodOf(engine, 'resumeRow')
|
|
61
|
+
assert.match(resume, /catch[\s\S]{0,80}markStopped/, '실패를 삼키면 등록부가 계속 거짓을 말한다')
|
|
62
|
+
const mark = methodOf(engine, 'markStopped')
|
|
63
|
+
assert.match(mark, /status: 'stopped'/, '등록부에 실제로 적어야 한다')
|
|
64
|
+
assert.match(mark, /console\.(warn|error)/, '되살리지 못한 사실은 로그에도 남아야 한다')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('벤치 사본은 무인으로 되살아나지 않는다 — 실험은 재기동을 넘기지 못한다', () => {
|
|
68
|
+
const body = methodOf(engine, 'resumeRow')
|
|
69
|
+
assert.match(body, /purpose === 'bench'/, '격리 사본을 갈라야 한다')
|
|
70
|
+
assert.match(body, /bench[\s\S]{0,200}markStopped/, '되살리지 않았으면 등록부도 그렇게 적어야 한다')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('피드 재개는 등록부가 「도는 미러」라고 말하는 것만 — 멈춘 것을 다시 켜지 않는다', () => {
|
|
74
|
+
assert.match(live, /export async function resumeReferenceLiveFeeds/, '재개 함수가 있어야 한다')
|
|
75
|
+
assert.match(live, /r\.status === 'running' && r\.realityMode === 'mirror'/, '도는 미러만 골라야 한다')
|
|
76
|
+
assert.match(live, /discoverSites/, '사이트 서술자는 다시 찾아서 얻는다(hint 를 남기지 않았다)')
|
|
77
|
+
/* 붙지 않은 미러는 「도는데 아무것도 오지 않는」 트윈이다 — 조용히 넘기지 않는다. */
|
|
78
|
+
assert.match(live, /NOT reattached/, '못 붙인 것은 이유와 함께 남겨야 한다')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('부팅 훅이 엔진 다음에 피드를 붙인다 — 순서가 뒤집히면 없는 커널에 계측을 붓는다', () => {
|
|
82
|
+
assert.match(boot, /resumeReferenceLiveFeeds/, '부팅 훅이 재개를 불러야 한다(부르는 곳이 없으면 없는 기능이다)')
|
|
83
|
+
const engineAt = boot.indexOf('TwinEngine.bootstrap()')
|
|
84
|
+
const feedAt = boot.indexOf('resumeReferenceLiveFeeds()')
|
|
85
|
+
assert.ok(engineAt >= 0 && feedAt > engineAt, '엔진 복구가 피드 재개보다 앞이어야 한다')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
/*
|
|
89
|
+
* ── 부팅 재개가 드러낸 것 (2026-08-14 실측) ─────────────────────────────────
|
|
90
|
+
* 트윈 14개가 실제로 돌기 시작하자 호스트가 죽었다. 원인은 재개가 아니라 **방송 두 가지**였다:
|
|
91
|
+
* ① 시뮬은 delta 마다 곧바로 전 상태를 투영해 방송했다 — 한 tick 이 사실을 수백 개 내면 그 반복이
|
|
92
|
+
* 이벤트 루프를 35초 막았고(단계 합은 32ms 였다), 그 사이 구독자는 아무것도 빼내지 못했다.
|
|
93
|
+
* ② 밀린 push 가 1024를 넘자 pubsub 이 던졌고, 타이머 콜백을 타고 올라온 그 예외가 프로세스를 끝냈다.
|
|
94
|
+
* 둘 다 재개 없이도 있던 결함이다 — 재개가 그것을 매일 아침 만나게 만들었을 뿐이다.
|
|
95
|
+
*/
|
|
96
|
+
test('시뮬 방송도 모아서 한 번 — delta 마다 전 상태를 투영하지 않는다', () => {
|
|
97
|
+
const at = engine.indexOf("if (msg.kind === 'delta')")
|
|
98
|
+
assert.ok(at >= 0, 'sim delta 처리 자리가 없다')
|
|
99
|
+
const body = engine.slice(at, at + 1400)
|
|
100
|
+
assert.match(body, /inst\.dirty = true/, 'dirty 로 표시하고')
|
|
101
|
+
assert.match(body, /ensureBroadcastCoalescer\(\)/, '주기 flush 에 맡겨야 한다')
|
|
102
|
+
assert.equal(/this\.publishEntityData\(inst\)/.test(body), false, 'delta 마다 곧바로 방송하는 모양으로 되돌아갔다')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('flush 가 시뮬도 본다 — 라이브만 보면 시뮬 방송이 멈춘 채 남는다', () => {
|
|
106
|
+
const body = methodOf(engine, 'flushLiveBroadcasts')
|
|
107
|
+
assert.equal(/mode !== 'live'\) continue/.test(body), false, '시뮬을 건너뛰는 모양으로 되돌아갔다')
|
|
108
|
+
assert.match(body, /const isLive = inst\.mode === 'live'/, '모드는 가르되 둘 다 방송해야 한다')
|
|
109
|
+
/* 저널·state 채널은 시뮬이 자기 콜백에서 이미 처리한다 — 여기서 또 하면 두 번 적힌다. */
|
|
110
|
+
assert.match(body, /if \(!isLive\) continue/, '시뮬은 엔티티 방송까지만')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('구독자 하나가 호스트를 죽이지 못한다 — 그리고 못 보낸 것을 보낸 것으로 적지 않는다', () => {
|
|
114
|
+
/* 직접 방송은 **가드 안에 하나**여야 한다 — 그 밖의 호출은 예외가 그대로 올라가는 길이다. */
|
|
115
|
+
const guardBody = methodOf(engine, 'publishGuarded')
|
|
116
|
+
assert.equal((engine.match(/pubsub\.publish\(/g) ?? []).length, 1, '가드를 거치지 않는 직접 방송이 남아 있다')
|
|
117
|
+
assert.match(guardBody, /pubsub\.publish\(/, '그 하나는 가드 안에 있어야 한다')
|
|
118
|
+
assert.match(guardBody, /catch/, '방송 실패를 잡아야 한다(타이머 콜백에서 던지면 프로세스가 끝난다)')
|
|
119
|
+
assert.match(guardBody, /console\.warn/, '못 보낸 사실은 남아야 한다(삼키면 「보냈는데 낡았다」가 된다)')
|
|
120
|
+
/* 시그니처를 남겨 두면 다음 주기에 「무변화」로 건너뛰고 그 태그는 영원히 낡는다. */
|
|
121
|
+
assert.match(engine, /if \(!this\.publishGuarded\('data'[\s\S]{0,80}sigs\.delete\(tag\)/, '실패한 태그의 시그니처를 되돌려야 한다')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
/*
|
|
125
|
+
* ── 굶김 안전망 (2026-08-14) ─────────────────────────────────────────────────
|
|
126
|
+
* 시뮬 틱은 메인 이벤트 루프에서 돈다 — 한 트윈의 틱이 길어지면 그 시간만큼 호스트 전체가 멈춘다
|
|
127
|
+
* (실측 34.9초). 방송 반복은 걷어냈지만 커널 틱 자체는 여전히 메인 루프에 있고, 근본 해결(분산)은
|
|
128
|
+
* 이연됐다. 그 사이의 안전망이 이것이다: **호스트를 굶기는 트윈은 멈춘다.**
|
|
129
|
+
*/
|
|
130
|
+
test('굶김은 예산과 다른 문턱으로 판정한다 — 조금 느린 트윈을 내리지 않는다', () => {
|
|
131
|
+
const body = methodOf(engine, 'guardStarvation')
|
|
132
|
+
assert.match(body, /this\.TICK_MS \* this\.STARVE_FACTOR/, '굶김 문턱은 틱 간격의 배수다')
|
|
133
|
+
/* 예산(500ms)으로 내리면 계기판이 말해야 할 「조금 느림」에 트윈이 죽는다. */
|
|
134
|
+
assert.equal(/budgetMs/.test(body), false, '예산으로 내리는 모양 — 경고와 정지를 섞지 않는다')
|
|
135
|
+
assert.ok(/STARVE_FACTOR = \d+/.test(engine) && /STARVE_STREAK = \d+/.test(engine), '문턱과 연속 횟수가 상수로 서 있어야 한다')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('한 번으로 내리지 않고, 한 번 내려오면 연속이 끊긴다', () => {
|
|
139
|
+
const body = methodOf(engine, 'guardStarvation')
|
|
140
|
+
assert.match(body, /streak < this\.STARVE_STREAK\) return/, '연속이 차기 전에는 멈추지 않는다')
|
|
141
|
+
/* 웜스타트 직후 첫 틱은 원래 무겁다(복구한 상태를 처음 접는다) — 그것으로 내리면 재개가 무의미해진다. */
|
|
142
|
+
assert.match(body, /this\.starveStreak\.delete\(key\)[\s\S]{0,40}return/, '문턱 아래면 연속을 끊어야 한다')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('멈추는 길은 하나 — 타이머만 끄면 등록부가 계속 「도는 중」이라 말한다', () => {
|
|
146
|
+
const guard = methodOf(engine, 'stopWithNote')
|
|
147
|
+
assert.match(guard, /this\.stop\(domainId, id\)/, '공용 stop 을 써야 한다(스냅샷·피드 정리·등록부가 그 안에 있다)')
|
|
148
|
+
/* 예전 tick 실패 경로는 `clearInterval` 만 했다 — 그 모양으로 되돌아가지 않게. */
|
|
149
|
+
const tick = methodOf(engine, 'tickGuarded')
|
|
150
|
+
assert.equal(/clearInterval\(inst\.timer\)/.test(tick), false, '타이머만 끄는 모양으로 되돌아갔다')
|
|
151
|
+
assert.match(tick, /stopWithNote\(/, '실패도 이유와 함께 멈춰야 한다')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('왜 멈췄는지 코드로 남고, 다시 세우면 지운다', () => {
|
|
155
|
+
assert.match(engine, /export interface StopNote/, '이유는 문장이 아니라 코드다(화면이 옮긴다)')
|
|
156
|
+
assert.match(engine, /code: 'starved', params: \{ streak/, '굶김은 근거 수치와 함께')
|
|
157
|
+
assert.match(engine, /code: 'tick-failed', params: \{ reason/, '틱 실패는 원문 그대로(옮길 어휘가 아니다)')
|
|
158
|
+
assert.match(methodOf(engine, 'list'), /stopNote: this\.stopNotes\.get\(/, '목록이 그 사실을 함께 내야 한다')
|
|
159
|
+
/* 남겨 두면 다시 돌고 있는 트윈이 「굶겨서 멈췄다」고 말한다. */
|
|
160
|
+
assert.match(engine, /this\.stopNotes\.delete\(key\)/, '다시 세울 때 지워야 한다')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
/*
|
|
164
|
+
* ── 같은 현장 이웃 설비 상태 전달 (2026-08-14) ──────────────────────────────
|
|
165
|
+
* 시뮬 에너지 트윈은 계측이 없으므로 스스로 부하를 만든다. 그 재료 하나가 「무엇이 돌고 있나」인데,
|
|
166
|
+
* 그것은 같은 현장 공장 트윈이 아는 사실이다. 호스트는 **누가 이웃인지**만 알려 주고, 무엇을 읽어
|
|
167
|
+
* 어떻게 부하로 바꿀지는 커널이 정한다 — 호스트가 kW 를 계산해 주면 규칙이 어디에도 안 적힌 채
|
|
168
|
+
* 트윈마다 갈라진다.
|
|
169
|
+
*/
|
|
170
|
+
test('이웃 상태는 커널이 요청할 때만, 같은 현장에서만 전달한다', () => {
|
|
171
|
+
const body = methodOf(engine, 'feedPeerEquipment')
|
|
172
|
+
assert.match(body, /observePeerEquipment !== 'function'\) return/, '이웃을 읽지 않는 커널에는 넘기지 않는다')
|
|
173
|
+
assert.match(body, /other\.spaceId !== inst\.spaceId\) continue/, '같은 현장만 이웃이다')
|
|
174
|
+
assert.match(body, /other === inst/, '자기 자신은 이웃이 아니다')
|
|
175
|
+
/* 상태만 넘긴다 — 스냅샷을 통째로 넘기면 매 tick 상태 크기에 비례한 비용을 치른다. */
|
|
176
|
+
assert.equal(/snapshot\(\)/.test(body), false, '스냅샷을 넘기는 모양으로 되돌아갔다')
|
|
177
|
+
assert.match(body, /status: String/, '상태만 실어 보낸다')
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('현장이 없으면 이웃 목록을 비운다 — 옛 상태가 남아 부하를 만들지 않게', () => {
|
|
181
|
+
const body = methodOf(engine, 'feedPeerEquipment')
|
|
182
|
+
assert.match(body, /!inst\.spaceId[\s\S]{0,120}observePeerEquipment\(\[\]\)/, '빈 목록으로 덮어야 한다')
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('tick 이 이웃 상태를 먼저 싣는다 — 커널이 그것을 읽어 부하를 만든다', () => {
|
|
186
|
+
const tick = methodOf(engine, 'tickGuarded')
|
|
187
|
+
const feedAt = tick.indexOf('feedPeerEquipment')
|
|
188
|
+
const tickAt = tick.indexOf('runtime.tick(')
|
|
189
|
+
assert.ok(feedAt >= 0 && tickAt > feedAt, '이웃 상태를 실은 뒤에 tick 해야 한다')
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
/*
|
|
193
|
+
* ── 시뮬 트윈의 시각축 (2026-08-15) ─────────────────────────────────────────
|
|
194
|
+
* 커널의 기본 기준점은 상수(2026-01-01)다. 호스트가 그것을 실제 시각으로 옮기지 않으면 시뮬 트윈의
|
|
195
|
+
* 저널 시각이 **재기동마다 되감긴다** — 며칠 치 사건이 전부 같은 분에 몰리고, 실 시각으로 창을 자르는
|
|
196
|
+
* 성과·이력 질의에는 그 트윈이 보이지 않는다. 수요 구간(15분)도 가짜 시각 위에 서게 된다.
|
|
197
|
+
*/
|
|
198
|
+
test('시뮬 기동이 커널의 시각 기준점을 지금으로 맞춘다', () => {
|
|
199
|
+
const body = methodOf(engine, 'start')
|
|
200
|
+
assert.match(body, /setClockOrigin\?\.\(Date\.now\(\)\)/, '기동 순간의 실제 시각을 줘야 한다')
|
|
201
|
+
/* 커널을 세운 **직후**여야 한다 — 시계가 흐른 뒤에 옮기면 그전 사실과 시간축이 어긋난다. */
|
|
202
|
+
const at = body.indexOf('setClockOrigin')
|
|
203
|
+
const runtimeAt = body.indexOf('new TwinRuntime(')
|
|
204
|
+
assert.ok(at >= 0 && runtimeAt > at, '런타임을 세우기 전에 기준점을 맞춘다')
|
|
205
|
+
})
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 정규 인제스트 — **어휘가 둘이다**(물류 EPCIS · 에너지 계측).
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 있나 (2026-08-14, EMS 5단계 준비) ────────────────────────────────────
|
|
5
|
+
* 이 seam 은 무조건 `type: 'ObjectEvent'` 를 만들었다. 그래서 커넥터가 계측을 이 길로 보내면 둘 중
|
|
6
|
+
* 하나가 됐다: `epc` 가 없어 거부되거나(그나마 나은 쪽), **엉뚱한 물품 관측**으로 읽혀 재고가 흔들린다.
|
|
7
|
+
* 오류 없이 틀리는 쪽이 더 나쁘다.
|
|
8
|
+
*
|
|
9
|
+
* 이 시험이 지키는 것 둘:
|
|
10
|
+
* ① 한 배치에 두 어휘가 섞여 와도 각자의 봉투가 된다(커넥터가 나눠 보낼 의무를 지지 않는다).
|
|
11
|
+
* ② **거부분이 사라지지 않는다** — 예전에는 호출부가 `rejected` 를 받지도 않았다.
|
|
12
|
+
*/
|
|
13
|
+
import { test } from 'node:test'
|
|
14
|
+
import assert from 'node:assert/strict'
|
|
15
|
+
|
|
16
|
+
import { ingestCanonicalRecords } from '../server/engine/canonical-ingest.ts'
|
|
17
|
+
|
|
18
|
+
const NOW = '2026-08-14T09:00:00.000Z'
|
|
19
|
+
|
|
20
|
+
const epcisRecord = {
|
|
21
|
+
epc: 'urn:epc:id:sgtin:0614141.107346.2017',
|
|
22
|
+
action: 'OBSERVE',
|
|
23
|
+
bizStep: 'urn:epcglobal:cbv:bizstep:storing',
|
|
24
|
+
disposition: 'urn:epcglobal:cbv:disp:in_progress',
|
|
25
|
+
readPoint: 'rack-1'
|
|
26
|
+
}
|
|
27
|
+
const energyRecord = { meterId: 'meter-incoming', kW: 1200, at: '2026-08-14T09:00:30.000Z' }
|
|
28
|
+
|
|
29
|
+
test('한 배치에 두 어휘가 섞여도 각자의 봉투가 된다', () => {
|
|
30
|
+
const { accepted, rejected } = ingestCanonicalRecords([epcisRecord, energyRecord] as any, 't', NOW)
|
|
31
|
+
assert.equal(rejected.length, 0, `거부 없음이어야 한다: ${JSON.stringify(rejected)}`)
|
|
32
|
+
const types = accepted.map(e => e.eventType).sort()
|
|
33
|
+
assert.deepEqual(types, ['energy.measured', 'epcis.ObjectEvent'])
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('계측을 물품 관측으로 만들지 않는다 — 이것이 이 갈림의 이유다', () => {
|
|
37
|
+
const { accepted } = ingestCanonicalRecords([energyRecord] as any, 't', NOW)
|
|
38
|
+
assert.equal(accepted.length, 1)
|
|
39
|
+
assert.equal(accepted[0].eventType, 'energy.measured')
|
|
40
|
+
/* EPCIS 페이로드의 흔적이 없어야 한다 — 있으면 미러가 물품을 지어낸다. */
|
|
41
|
+
assert.equal((accepted[0].data as any).epcList, undefined)
|
|
42
|
+
assert.equal((accepted[0].data as any).type, undefined)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('거부분이 사라지지 않는다 — 커넥터 매핑이 틀린 것을 알 수 있어야 한다', () => {
|
|
46
|
+
const { accepted, rejected } = ingestCanonicalRecords(
|
|
47
|
+
[
|
|
48
|
+
energyRecord,
|
|
49
|
+
{ meterId: 'm-2', kW: 10, at: '어제' }, // 시각 형식 오류
|
|
50
|
+
{ epc: '', action: 'OBSERVE', bizStep: 'x' } // EPCIS 쪽 결손
|
|
51
|
+
] as any,
|
|
52
|
+
't',
|
|
53
|
+
NOW
|
|
54
|
+
)
|
|
55
|
+
assert.equal(accepted.length, 1, '유효한 것은 통과한다')
|
|
56
|
+
assert.equal(rejected.length, 2)
|
|
57
|
+
for (const r of rejected) assert.ok(r.errors.length > 0, '이유 없는 거부가 없다')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('빈 입력에서 두 경로 모두 조용하다 — 없는 것을 만들지 않는다', () => {
|
|
61
|
+
const empty = ingestCanonicalRecords([], 't', NOW)
|
|
62
|
+
assert.deepEqual(empty, { accepted: [], rejected: [] })
|
|
63
|
+
const nothing = ingestCanonicalRecords(null, 't', NOW)
|
|
64
|
+
assert.deepEqual(nothing, { accepted: [], rejected: [] })
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('시각이 없는 계측은 호출자의 기준 시각을 쓴다 — 도착 시각이 근거다', () => {
|
|
68
|
+
const { accepted } = ingestCanonicalRecords([{ meterId: 'm-1', kW: 5 }] as any, 't', NOW)
|
|
69
|
+
assert.equal(accepted[0].eventTime, NOW)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
/*
|
|
73
|
+
* ── 세 번째 어휘: 설비 에너지 상태 (2026-08-15) ─────────────────────────────
|
|
74
|
+
* 능력 계약은 태양광·축전지·감축 부하·차단기의 상태 필드를 선언해 두었는데 받는 길이 없었다.
|
|
75
|
+
* 계량과 갈라 두는 이유는 커널에 적혀 있다 — 계량은 구간에 누적되고, 이것들은 그 설비의 지금이다.
|
|
76
|
+
*/
|
|
77
|
+
test('설비 에너지 상태는 EPCIS 도 계량도 아닌 자기 어휘로 들어온다', () => {
|
|
78
|
+
const { accepted, rejected } = ingestCanonicalRecords(
|
|
79
|
+
[
|
|
80
|
+
{ equipmentId: 'pv-1', at: '2026-08-15T09:00:00.000Z', generatedKW: 210 },
|
|
81
|
+
{ meterId: 'm-1', kW: 800, at: '2026-08-15T09:00:00.000Z' },
|
|
82
|
+
{ epc: 'urn:epc:id:sgtin:1.2.3', action: 'OBSERVE', bizStep: 'urn:epcglobal:cbv:bizstep:storing' }
|
|
83
|
+
] as any,
|
|
84
|
+
'tenant-1',
|
|
85
|
+
'2026-08-15T09:00:00.000Z'
|
|
86
|
+
)
|
|
87
|
+
assert.equal(rejected.length, 0, JSON.stringify(rejected))
|
|
88
|
+
const types = accepted.map(a => a.eventType).sort()
|
|
89
|
+
assert.deepEqual(types, ['energy.equipment', 'energy.measured', 'epcis.ObjectEvent'].sort(), '세 어휘가 각자 봉투로')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('바꿀 상태가 없는 보고는 거부한다 — 이유와 함께', () => {
|
|
93
|
+
const { accepted, rejected } = ingestCanonicalRecords(
|
|
94
|
+
[{ equipmentId: 'pv-1', at: '2026-08-15T09:00:00.000Z' }] as any,
|
|
95
|
+
'tenant-1',
|
|
96
|
+
'2026-08-15T09:00:00.000Z'
|
|
97
|
+
)
|
|
98
|
+
assert.equal(accepted.length, 0)
|
|
99
|
+
assert.match(rejected[0].errors.join(' '), /바꿀 상태/)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('충전율이 백분율 범위를 벗어나면 거부한다 — 단위를 잘못 맞춘 커넥터를 통과시키지 않는다', () => {
|
|
103
|
+
const { rejected } = ingestCanonicalRecords(
|
|
104
|
+
[{ equipmentId: 'ess-1', at: '2026-08-15T09:00:00.000Z', soc: 380 }] as any,
|
|
105
|
+
'tenant-1',
|
|
106
|
+
'2026-08-15T09:00:00.000Z'
|
|
107
|
+
)
|
|
108
|
+
assert.equal(rejected.length, 1)
|
|
109
|
+
assert.match(rejected[0].errors.join(' '), /soc/)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('개폐 위치는 표준의 네 값만 받는다 — 과도·불량을 열림/닫힘으로 반올림하지 않는다', () => {
|
|
113
|
+
const ok = ingestCanonicalRecords([{ equipmentId: 'cb-1', at: '2026-08-15T09:00:00.000Z', position: 'intermediate' }] as any, 't', '2026-08-15T09:00:00.000Z')
|
|
114
|
+
assert.equal(ok.accepted.length, 1)
|
|
115
|
+
const bad = ingestCanonicalRecords([{ equipmentId: 'cb-1', at: '2026-08-15T09:00:00.000Z', position: 'ON' }] as any, 't', '2026-08-15T09:00:00.000Z')
|
|
116
|
+
assert.equal(bad.rejected.length, 1)
|
|
117
|
+
assert.match(bad.rejected[0].errors.join(' '), /position/)
|
|
118
|
+
})
|
|
@@ -9,7 +9,7 @@ import { test } from 'node:test'
|
|
|
9
9
|
import assert from 'node:assert/strict'
|
|
10
10
|
|
|
11
11
|
import { DOMAIN_CATALOG, CAPABILITIES, CAPABILITY_KEYS, capabilitiesForType, stateFieldsOf } from '@operato/twin-kernel'
|
|
12
|
-
import { buildEntityDeltas } from '../
|
|
12
|
+
import { buildEntityDeltas } from '../shared/entity-delta.ts'
|
|
13
13
|
|
|
14
14
|
const SYSTEMS = ['wms', 'yms', 'mes'] as const
|
|
15
15
|
|
|
@@ -63,17 +63,17 @@ test('능력 소싱 구조: 4 자원/위치 능력은 카탈로그 타입에서,
|
|
|
63
63
|
|
|
64
64
|
test('★ 스트림라인 브릿지: 능력의 상태필드가 라이브 payload 에 실림 (커널→보드 도달)', () => {
|
|
65
65
|
// storable 노드 → occupancy/capacity (+ operable status)
|
|
66
|
-
const nodeD: any = buildEntityDeltas({ locations: [{ id: 'n', type: 'storage', occupancy: 2, capacity: 9, status: 'idle' }] })[0].data
|
|
66
|
+
const nodeD: any = buildEntityDeltas({ locations: [{ id: 'n', type: 'storage', occupancy: 2, capacity: 9, status: 'idle' }] }, 't1')[0].data
|
|
67
67
|
for (const f of stateFieldsOf(['storable'])) assert.ok(f in nodeD, `storable payload.${f}`)
|
|
68
68
|
assert.ok('status' in nodeD, 'operable(노드 status) payload')
|
|
69
69
|
|
|
70
70
|
// mobile+operable 무버 → location/motion/status
|
|
71
|
-
const equipmentD: any = buildEntityDeltas({ equipment: [{ id: 'm', kind: 'forklift', location: 'n', status: 'busy', motion: { toNode: 'n2', progress: 0.3 } }] }).find(x => x.tag === 'm')!.data
|
|
71
|
+
const equipmentD: any = buildEntityDeltas({ equipment: [{ id: 'm', kind: 'forklift', location: 'n', status: 'busy', motion: { toNode: 'n2', progress: 0.3 } }] }, 't1').find(x => x.tag === 'm')!.data
|
|
72
72
|
for (const f of stateFieldsOf(['mobile'])) assert.ok(f in equipmentD, `mobile payload.${f}`)
|
|
73
73
|
for (const f of stateFieldsOf(['operable'])) assert.ok(f in equipmentD, `operable payload.${f}`)
|
|
74
74
|
|
|
75
75
|
// trackable 오더 → progress/held (lifecycle 은 payload 에서 'status' 로 표면화 — 이름-매핑 시임)
|
|
76
|
-
const orderD: any = buildEntityDeltas({ orders: [{ id: 'o', status: 'picking', progress: 0.5, held: false }] }).find(x => x.tag === 'o')!.data
|
|
76
|
+
const orderD: any = buildEntityDeltas({ orders: [{ id: 'o', status: 'picking', progress: 0.5, held: false }] }, 't1').find(x => x.tag === 'o')!.data
|
|
77
77
|
assert.ok('progress' in orderD && 'held' in orderD, 'trackable progress/held payload')
|
|
78
78
|
assert.ok('status' in orderD, 'trackable lifecycle → payload.status (이름-매핑)')
|
|
79
79
|
})
|
|
@@ -82,7 +82,7 @@ test('시임 문서화 · processable output(양품/불량)은 payload oee.good/
|
|
|
82
82
|
// 계약 stateField = 'output' 이나, 라이브 payload 는 이를 oee(정수%+good/scrap)로 집약해 표면화.
|
|
83
83
|
const d: any = buildEntityDeltas({
|
|
84
84
|
equipment: [{ id: 'c', kind: 'cutter', status: 'busy', oee: { overall: 0.8, availability: 0.9, performance: 0.9, quality: 0.98, goodCount: 10, scrapCount: 1 } }]
|
|
85
|
-
}).find(x => x.tag === 'c')!.data
|
|
85
|
+
}, 't1').find(x => x.tag === 'c')!.data
|
|
86
86
|
assert.ok(d.oee, 'processable → payload.oee 존재')
|
|
87
87
|
assert.equal(d.oee.good, 10, 'output.good = oee.good')
|
|
88
88
|
assert.equal(d.oee.scrap, 1, 'output.scrap = oee.scrap')
|