@things-factory/headless-twin 10.0.9 → 10.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-server/engine/canonical-ingest.d.ts +2 -2
- package/dist-server/engine/canonical-ingest.js +24 -2
- package/dist-server/engine/canonical-ingest.js.map +1 -1
- package/dist-server/engine/command-routing.js +1 -1
- package/dist-server/engine/command-routing.js.map +1 -1
- package/dist-server/engine/energy-topology.d.ts +86 -0
- package/dist-server/engine/energy-topology.js +144 -0
- package/dist-server/engine/energy-topology.js.map +1 -0
- package/dist-server/engine/index.d.ts +8 -3
- package/dist-server/engine/index.js +10 -3
- package/dist-server/engine/index.js.map +1 -1
- package/dist-server/engine/kpi-baseline.d.ts +1 -1
- package/dist-server/engine/kpi-baseline.js.map +1 -1
- package/dist-server/engine/kpi-fold.d.ts +137 -1
- package/dist-server/engine/kpi-fold.js +225 -0
- package/dist-server/engine/kpi-fold.js.map +1 -1
- package/dist-server/engine/kpi-query.d.ts +1 -1
- package/dist-server/engine/kpi-query.js +212 -18
- package/dist-server/engine/kpi-query.js.map +1 -1
- package/dist-server/engine/kpi-target.d.ts +17 -0
- package/dist-server/engine/kpi-target.js +24 -2
- package/dist-server/engine/kpi-target.js.map +1 -1
- package/dist-server/engine/live-feed-registry.d.ts +25 -0
- package/dist-server/engine/live-feed-registry.js +51 -0
- package/dist-server/engine/live-feed-registry.js.map +1 -0
- package/dist-server/engine/load-meter.d.ts +181 -0
- package/dist-server/engine/load-meter.js +267 -0
- package/dist-server/engine/load-meter.js.map +1 -0
- package/dist-server/engine/local-declarations.d.ts +262 -0
- package/dist-server/engine/local-declarations.js +528 -0
- package/dist-server/engine/local-declarations.js.map +1 -0
- package/dist-server/engine/model-basis.d.ts +39 -6
- package/dist-server/engine/model-basis.js +69 -9
- package/dist-server/engine/model-basis.js.map +1 -1
- package/dist-server/engine/{board-vocabulary.d.ts → model-vocabulary.d.ts} +4 -4
- package/dist-server/engine/{board-vocabulary.js → model-vocabulary.js} +37 -19
- package/dist-server/engine/model-vocabulary.js.map +1 -0
- package/dist-server/engine/oee-accumulator.d.ts +28 -0
- package/dist-server/engine/oee-accumulator.js +26 -1
- package/dist-server/engine/oee-accumulator.js.map +1 -1
- package/dist-server/engine/operation-basis.d.ts +32 -0
- package/dist-server/engine/operation-basis.js +87 -0
- package/dist-server/engine/operation-basis.js.map +1 -0
- package/dist-server/engine/property-effects.d.ts +30 -0
- package/dist-server/engine/property-effects.js +192 -0
- package/dist-server/engine/property-effects.js.map +1 -0
- package/dist-server/engine/runtime-key.d.ts +1 -1
- package/dist-server/engine/runtime-key.js +1 -1
- package/dist-server/engine/runtime-key.js.map +1 -1
- package/dist-server/engine/spec-coverage.js +3 -3
- package/dist-server/engine/spec-coverage.js.map +1 -1
- package/dist-server/engine/state-axes.d.ts +7 -2
- package/dist-server/engine/state-axes.js +8 -3
- package/dist-server/engine/state-axes.js.map +1 -1
- package/dist-server/engine/structure-diff.js +1 -1
- package/dist-server/engine/structure-diff.js.map +1 -1
- package/dist-server/engine/travel-estimator.d.ts +9 -2
- package/dist-server/engine/travel-estimator.js +11 -6
- package/dist-server/engine/travel-estimator.js.map +1 -1
- package/dist-server/engine/twin-engine.d.ts +332 -47
- package/dist-server/engine/twin-engine.js +1314 -279
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/engine/warm-start.d.ts +38 -4
- package/dist-server/engine/warm-start.js +38 -9
- package/dist-server/engine/warm-start.js.map +1 -1
- package/dist-server/index.d.ts +1 -0
- package/dist-server/index.js +17 -11
- package/dist-server/index.js.map +1 -1
- package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.d.ts +5 -0
- package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js +48 -0
- package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js.map +1 -0
- package/dist-server/migrations/index.d.ts +2 -0
- package/dist-server/migrations/index.js +10 -0
- package/dist-server/migrations/index.js.map +1 -0
- package/dist-server/service/index.d.ts +3 -2
- package/dist-server/service/index.js +25 -17
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/reference/discovery-result.d.ts +1 -1
- package/dist-server/service/reference/discovery-result.js +2 -2
- package/dist-server/service/reference/discovery-result.js.map +1 -1
- package/dist-server/service/reference/ingest-space.d.ts +24 -1
- package/dist-server/service/reference/ingest-space.js +17 -1
- package/dist-server/service/reference/ingest-space.js.map +1 -1
- package/dist-server/service/reference/reference-live.d.ts +23 -0
- package/dist-server/service/reference/reference-live.js +121 -4
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-master.d.ts +194 -13
- package/dist-server/service/reference/reference-master.js +159 -28
- package/dist-server/service/reference/reference-master.js.map +1 -1
- package/dist-server/service/reference/reference-resolver.d.ts +31 -1
- package/dist-server/service/reference/reference-resolver.js +223 -12
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/reference/template-registry.d.ts +19 -3
- package/dist-server/service/reference/template-registry.js.map +1 -1
- package/dist-server/service/twin-audit/command-audit.d.ts +37 -0
- package/dist-server/service/twin-audit/command-audit.js +53 -0
- package/dist-server/service/twin-audit/command-audit.js.map +1 -0
- package/dist-server/service/twin-audit/index.d.ts +4 -0
- package/dist-server/service/twin-audit/index.js +8 -0
- package/dist-server/service/twin-audit/index.js.map +1 -0
- package/dist-server/service/twin-audit/twin-audit-event.d.ts +24 -0
- package/dist-server/service/twin-audit/twin-audit-event.js +141 -0
- package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -0
- package/dist-server/service/twin-audit/twin-audit-query.d.ts +4 -0
- package/dist-server/service/twin-audit/twin-audit-query.js +76 -0
- package/dist-server/service/twin-audit/twin-audit-query.js.map +1 -0
- package/dist-server/service/twin-control/twin-control-mutation.js +21 -1
- package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
- package/dist-server/service/twin-event/twin-event-keys.d.ts +1 -1
- package/dist-server/service/twin-event/twin-event-keys.js +1 -1
- package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
- package/dist-server/service/twin-event/twin-event.d.ts +14 -5
- package/dist-server/service/twin-event/twin-event.js +45 -13
- package/dist-server/service/twin-event/twin-event.js.map +1 -1
- package/dist-server/service/twin-forecast/forecast-metrics.d.ts +13 -0
- package/dist-server/service/twin-forecast/forecast-metrics.js +57 -0
- package/dist-server/service/twin-forecast/forecast-metrics.js.map +1 -0
- package/dist-server/service/twin-forecast/forecast-tuning.d.ts +12 -0
- package/dist-server/service/twin-forecast/forecast-tuning.js +44 -0
- package/dist-server/service/twin-forecast/forecast-tuning.js.map +1 -0
- package/dist-server/service/twin-forecast/gap-analytics.d.ts +28 -2
- package/dist-server/service/twin-forecast/gap-analytics.js +43 -17
- package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
- package/dist-server/service/twin-forecast/twin-forecast-query.js +217 -38
- package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
- package/dist-server/service/twin-instance/twin-instance.d.ts +38 -5
- package/dist-server/service/twin-instance/twin-instance.js +66 -20
- package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
- package/dist-server/service/twin-journal/twin-journal-query.d.ts +5 -2
- package/dist-server/service/twin-journal/twin-journal-query.js +71 -9
- package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +3 -5
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +24 -27
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
- package/dist-server/service/twin-metrics/twin-metrics-query.d.ts +1 -0
- package/dist-server/service/twin-metrics/twin-metrics-query.js +62 -0
- package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
- package/dist-server/service/twin-model/epcis-coverage.d.ts +14 -0
- package/dist-server/service/twin-model/epcis-coverage.js +174 -0
- package/dist-server/service/twin-model/epcis-coverage.js.map +1 -0
- package/dist-server/service/twin-model/iec61850-coverage.d.ts +14 -0
- package/dist-server/service/twin-model/iec61850-coverage.js +98 -0
- package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -0
- package/dist-server/service/twin-model/index.d.ts +13 -0
- package/dist-server/service/twin-model/index.js +29 -0
- package/dist-server/service/twin-model/index.js.map +1 -0
- package/dist-server/service/twin-model/isa95-coverage.d.ts +48 -0
- package/dist-server/service/twin-model/isa95-coverage.js +155 -0
- package/dist-server/service/twin-model/isa95-coverage.js.map +1 -0
- package/dist-server/service/twin-model/project-structure.d.ts +16 -0
- package/dist-server/service/twin-model/project-structure.js +173 -0
- package/dist-server/service/twin-model/project-structure.js.map +1 -0
- package/dist-server/service/twin-model/standard-coverage.d.ts +9 -0
- package/dist-server/service/twin-model/standard-coverage.js +37 -0
- package/dist-server/service/twin-model/standard-coverage.js.map +1 -0
- package/dist-server/service/twin-model/twin-equipment.d.ts +64 -0
- package/dist-server/service/twin-model/twin-equipment.js +134 -0
- package/dist-server/service/twin-model/twin-equipment.js.map +1 -0
- package/dist-server/service/twin-model/twin-lineage-query.d.ts +3 -0
- package/dist-server/service/twin-model/twin-lineage-query.js +206 -0
- package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -0
- package/dist-server/service/twin-model/twin-location.d.ts +58 -0
- package/dist-server/service/twin-model/twin-location.js +130 -0
- package/dist-server/service/twin-model/twin-location.js.map +1 -0
- package/dist-server/service/twin-model/twin-model-item-query.d.ts +141 -0
- package/dist-server/service/twin-model/twin-model-item-query.js +852 -0
- package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -0
- package/dist-server/service/twin-model/twin-model-mutation.d.ts +57 -0
- package/dist-server/service/twin-model/twin-model-mutation.js +490 -0
- package/dist-server/service/twin-model/twin-model-mutation.js.map +1 -0
- package/dist-server/service/twin-model/twin-model-query.d.ts +43 -0
- package/dist-server/service/twin-model/twin-model-query.js +551 -0
- package/dist-server/service/twin-model/twin-model-query.js.map +1 -0
- package/dist-server/service/twin-model/twin-model-tree-query.d.ts +3 -0
- package/dist-server/service/twin-model/twin-model-tree-query.js +158 -0
- package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -0
- package/dist-server/service/twin-model/twin-operation.d.ts +61 -0
- package/dist-server/service/twin-model/twin-operation.js +137 -0
- package/dist-server/service/twin-model/twin-operation.js.map +1 -0
- package/dist-server/service/twin-space/move-space.d.ts +65 -0
- package/dist-server/service/twin-space/move-space.js +166 -0
- package/dist-server/service/twin-space/move-space.js.map +1 -0
- package/dist-server/service/twin-space/space-integrity.d.ts +97 -0
- package/dist-server/service/twin-space/space-integrity.js +182 -0
- package/dist-server/service/twin-space/space-integrity.js.map +1 -0
- package/dist-server/service/twin-space/twin-space-area.js +4 -4
- package/dist-server/service/twin-space/twin-space-area.js.map +1 -1
- package/dist-server/service/twin-space/twin-space-representation.js +6 -6
- package/dist-server/service/twin-space/twin-space-representation.js.map +1 -1
- package/dist-server/service/twin-space/twin-space-resolver.d.ts +63 -1
- package/dist-server/service/twin-space/twin-space-resolver.js +356 -18
- package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
- package/dist-server/service/twin-space/twin-space.d.ts +17 -1
- package/dist-server/service/twin-space/twin-space.js +15 -5
- package/dist-server/service/twin-space/twin-space.js.map +1 -1
- package/dist-server/service/twin-structure/twin-structure.d.ts +1 -1
- package/dist-server/service/twin-structure/twin-structure.js +8 -8
- package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
- package/dist-shared/axis-read.d.ts +12 -0
- package/dist-shared/axis-read.js +32 -0
- package/dist-shared/axis-read.js.map +1 -0
- package/{dist-server/engine → dist-shared}/entity-delta.d.ts +3 -0
- package/{dist-server/engine → dist-shared}/entity-delta.js +102 -2
- package/dist-shared/entity-delta.js.map +1 -0
- package/dist-shared/kpi-broadcast.js.map +1 -0
- package/dist-shared/twin-level.js.map +1 -0
- package/package.json +14 -12
- package/server/engine/canonical-ingest.ts +36 -4
- package/server/engine/command-routing.ts +1 -1
- package/server/engine/energy-topology.ts +189 -0
- package/server/engine/index.ts +10 -3
- package/server/engine/kpi-baseline.ts +1 -1
- package/server/engine/kpi-fold.ts +389 -1
- package/server/engine/kpi-query.ts +214 -19
- package/server/engine/kpi-target.ts +24 -2
- package/server/engine/live-feed-registry.ts +58 -0
- package/server/engine/load-meter.ts +384 -0
- package/server/engine/local-declarations.ts +700 -0
- package/server/engine/model-basis.ts +78 -10
- package/server/engine/{board-vocabulary.ts → model-vocabulary.ts} +36 -15
- package/server/engine/oee-accumulator.ts +34 -1
- package/server/engine/operation-basis.ts +100 -0
- package/server/engine/property-effects.ts +199 -0
- package/server/engine/runtime-key.ts +1 -1
- package/server/engine/spec-coverage.ts +3 -3
- package/server/engine/state-axes.ts +8 -3
- package/server/engine/structure-diff.ts +1 -1
- package/server/engine/travel-estimator.ts +11 -6
- package/server/engine/twin-engine.ts +1414 -274
- package/server/engine/warm-start.ts +83 -12
- package/server/index.ts +13 -10
- package/server/migrations/1786000000000-RenameTwinInstanceBoardToModel.ts +43 -0
- package/server/migrations/index.ts +7 -0
- package/server/service/index.ts +9 -1
- package/server/service/reference/discovery-result.ts +2 -2
- package/server/service/reference/ingest-space.ts +35 -1
- package/server/service/reference/reference-live.ts +119 -4
- package/server/service/reference/reference-master.ts +276 -35
- package/server/service/reference/reference-resolver.ts +232 -12
- package/server/service/reference/template-registry.ts +20 -5
- package/server/service/twin-audit/command-audit.ts +81 -0
- package/server/service/twin-audit/index.ts +5 -0
- package/server/service/twin-audit/twin-audit-event.ts +138 -0
- package/server/service/twin-audit/twin-audit-query.ts +72 -0
- package/server/service/twin-control/twin-control-mutation.ts +22 -1
- package/server/service/twin-event/twin-event-keys.ts +1 -1
- package/server/service/twin-event/twin-event.ts +59 -14
- package/server/service/twin-forecast/forecast-metrics.ts +60 -0
- package/server/service/twin-forecast/forecast-tuning.ts +40 -0
- package/server/service/twin-forecast/gap-analytics.ts +48 -9
- package/server/service/twin-forecast/twin-forecast-query.ts +208 -36
- package/server/service/twin-instance/twin-instance.ts +126 -25
- package/server/service/twin-journal/twin-journal-query.ts +74 -10
- package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +23 -22
- package/server/service/twin-metrics/twin-metrics-query.ts +59 -2
- package/server/service/twin-model/epcis-coverage.ts +195 -0
- package/server/service/twin-model/iec61850-coverage.ts +117 -0
- package/server/service/twin-model/index.ts +25 -0
- package/server/service/twin-model/isa95-coverage.ts +190 -0
- package/server/service/twin-model/project-structure.ts +199 -0
- package/server/service/twin-model/standard-coverage.ts +35 -0
- package/server/service/twin-model/twin-equipment.ts +157 -0
- package/server/service/twin-model/twin-lineage-query.ts +193 -0
- package/server/service/twin-model/twin-location.ts +147 -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 +157 -0
- package/server/service/twin-space/move-space.ts +262 -0
- package/server/service/twin-space/space-integrity.ts +293 -0
- package/server/service/twin-space/twin-space-area.ts +4 -4
- package/server/service/twin-space/twin-space-representation.ts +6 -6
- package/server/service/twin-space/twin-space-resolver.ts +355 -21
- package/server/service/twin-space/twin-space.ts +40 -6
- package/server/service/twin-structure/twin-structure.ts +10 -10
- package/shared/axis-read.ts +28 -0
- package/{server/engine → shared}/entity-delta.ts +96 -1
- package/test/adopt-structure-live.test.ts +133 -0
- package/test/attention-digest.test.ts +3 -3
- package/test/axis-read.test.ts +87 -0
- package/test/boot-resume.test.ts +205 -0
- package/test/canonical-ingest-vocabularies.test.ts +118 -0
- package/test/capability-mapping.test.ts +1 -1
- package/test/column-type-portability.test.ts +122 -0
- package/test/command-routing.test.ts +1 -1
- package/test/declaration-reaches-model.test.ts +178 -0
- package/test/discovery-result.test.ts +1 -1
- package/test/energy-topology.test.ts +113 -0
- package/test/entity-delta.test.ts +171 -1
- package/test/event-time-column.test.ts +67 -0
- package/test/forecast-metrics.test.ts +71 -0
- package/test/forecast-tuning.test.ts +42 -0
- package/test/gap-analytics.test.ts +34 -7
- package/test/ingest-bench.test.ts +6 -6
- package/test/ingest-running-guard.test.ts +136 -0
- package/test/ingest-space.test.ts +29 -1
- package/test/instance-cache-lifecycle.test.ts +110 -0
- package/test/kernel-kind-guard.test.ts +96 -0
- package/test/kpi-baseline-db.test.ts +3 -1
- package/test/kpi-baseline.test.ts +3 -3
- package/test/kpi-fold.test.ts +393 -2
- package/test/kpi-query-bench.test.ts +5 -3
- package/test/lineage-survives-restart.test.ts +203 -0
- package/test/live-feed-registry.test.ts +67 -0
- package/test/live-kernel-facts.test.ts +161 -0
- package/test/live-mirror-parity.test.ts +22 -15
- package/test/load-meter.test.ts +314 -0
- package/test/local-declarations.test.ts +554 -0
- package/test/master-to-twin.test.ts +121 -33
- package/test/model-basis.test.ts +56 -1
- package/test/{board-vocabulary.test.ts → model-vocabulary.test.ts} +25 -25
- package/test/move-space.test.ts +149 -0
- package/test/mutation-gate.test.ts +19 -5
- package/test/oee-accumulator.test.ts +26 -7
- package/test/operation-basis.test.ts +100 -0
- package/test/operations-capability-db.test.ts +165 -0
- package/test/project-structure-db.test.ts +226 -0
- package/test/projection-reaches-screen.test.ts +272 -0
- package/test/property-effects.test.ts +96 -0
- package/test/resync-origin-site.test.ts +77 -0
- package/test/scale-twin-bench.test.ts +5 -5
- package/test/snapshot-freshness.test.ts +60 -0
- package/test/space-integrity.test.ts +223 -0
- package/test/standard-coverage.test.ts +94 -0
- package/test/state-axes.test.ts +1 -1
- package/test/streamline-e2e.test.ts +14 -14
- package/test/structure-revision-db.test.ts +11 -11
- package/test/tenant-registry-db.test.ts +5 -5
- package/test/twin-audit.test.ts +83 -0
- package/test/twin-model-item-db.test.ts +275 -0
- package/test/twin-model-tree-db.test.ts +176 -0
- package/test/twin-origin-resync.test.ts +114 -0
- package/test/warm-start-seam.test.ts +6 -6
- package/test/warm-start.test.ts +88 -3
- package/tsconfig.json +6 -1
- package/tsconfig.shared.json +23 -0
- package/tsconfig.shared.tsbuildinfo +1 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/dist-server/engine/board-vocabulary.js.map +0 -1
- package/dist-server/engine/entity-delta.js.map +0 -1
- package/dist-server/engine/kpi-broadcast.js.map +0 -1
- package/dist-server/engine/twin-level.js.map +0 -1
- package/dist-server/service/twin-event/backfill-keys.d.ts +0 -11
- package/dist-server/service/twin-event/backfill-keys.js +0 -63
- package/dist-server/service/twin-event/backfill-keys.js.map +0 -1
- package/dist-server/tsconfig.tsbuildinfo +0 -1
- package/server/service/twin-event/backfill-keys.ts +0 -72
- /package/{dist-server/engine → dist-shared}/kpi-broadcast.d.ts +0 -0
- /package/{dist-server/engine → dist-shared}/kpi-broadcast.js +0 -0
- /package/{dist-server/engine → dist-shared}/twin-level.d.ts +0 -0
- /package/{dist-server/engine → dist-shared}/twin-level.js +0 -0
- /package/{server/engine → shared}/kpi-broadcast.ts +0 -0
- /package/{server/engine → shared}/twin-level.ts +0 -0
|
@@ -8,7 +8,7 @@ import assert from 'node:assert/strict'
|
|
|
8
8
|
|
|
9
9
|
import { WmsKernel, StateProjector } from '@operato/twin-kernel'
|
|
10
10
|
import { masterToTwin } from '../server/service/reference/reference-master.ts'
|
|
11
|
-
import { buildEntityDeltas } from '../
|
|
11
|
+
import { buildEntityDeltas } from '../shared/entity-delta.ts'
|
|
12
12
|
import { OeeAccumulator, withLiveOee } from '../server/engine/oee-accumulator.ts'
|
|
13
13
|
|
|
14
14
|
const BASE_EPOCH = Date.parse('2026-01-01T00:00:00Z') // 커널 now() 고정 오프셋(flow-engine BASE_EPOCH)
|
|
@@ -28,15 +28,15 @@ function runSimCollect() {
|
|
|
28
28
|
],
|
|
29
29
|
equipment: [{ id: 'fk-1', kind: 'forklift', homeLocationId: 'dock-recv', mtbfMs: 5000, mttrMs: 8000 }]
|
|
30
30
|
}
|
|
31
|
-
const
|
|
31
|
+
const model = masterToTwin(m).model
|
|
32
32
|
const k = new WmsKernel('t')
|
|
33
33
|
const events: any[] = []
|
|
34
34
|
k.onEvent((e: any) => events.push(e))
|
|
35
|
-
k.
|
|
35
|
+
k.loadTwinModel(model)
|
|
36
36
|
k.scenario.load({ seed: 9, generators: [{ kind: 'inbound-arrival', rate: { distribution: 'constant', meanPerHour: 300 }, content: { skuMix: [{ gtin: GTIN, weight: 1 }], qtyPerLine: { min: 1, max: 1 } } }] })
|
|
37
37
|
k.scenario.start()
|
|
38
38
|
for (let i = 0; i < 200; i++) k.tick(TICK)
|
|
39
|
-
return { events, snap: k.getSnapshot() as any,
|
|
39
|
+
return { events, snap: k.getSnapshot() as any, model }
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
test('★ OEE 누적기: downMs 정확 · busyMs=run+setup · 근사 OEE 유효', () => {
|
|
@@ -83,8 +83,8 @@ test('★ OEE 누적기: quality.output 델타 소비 → quality 정확 (good/s
|
|
|
83
83
|
})
|
|
84
84
|
|
|
85
85
|
test('★ withLiveOee: projector 미러(oee 없음) + 누적기 → live 설비 payload 에 oee 포함 (원 갭 종료)', () => {
|
|
86
|
-
const { events,
|
|
87
|
-
const proj = new StateProjector(
|
|
86
|
+
const { events, model } = runSimCollect()
|
|
87
|
+
const proj = new StateProjector(model)
|
|
88
88
|
const acc = new OeeAccumulator()
|
|
89
89
|
for (const e of events) { proj.apply(e); acc.apply(e) }
|
|
90
90
|
|
|
@@ -111,7 +111,7 @@ test('★ withLiveOee: projector 미러(oee 없음) + 누적기 → live 설비
|
|
|
111
111
|
* 태운다 — 이 테스트는 그 재계산이 라이브와 **같은 값**을 내는지, 그리고 시간여행에서 **그 시점까지만**
|
|
112
112
|
* 세는지를 못 박는다.
|
|
113
113
|
*/
|
|
114
|
-
test('★ 저널을 다시 태우면 라이브와 같은 계측이 나온다 — 규칙이
|
|
114
|
+
test('★ 저널을 다시 태우면 라이브와 같은 계측이 나온다 — 규칙이 하나이므로', () => {
|
|
115
115
|
const T0 = Date.parse('2026-08-05T00:00:00.000Z')
|
|
116
116
|
const ev = (ms: number, id: string, status: string) => ({
|
|
117
117
|
eventId: `e${ms}`,
|
|
@@ -164,3 +164,22 @@ test('★ 계측 시점을 벽시계로 재지 않는다 — 과거 화면의
|
|
|
164
164
|
const atMuchLater = acc.oee('fk-1', T0 + 10_000_000)
|
|
165
165
|
assert.notDeepEqual(atThen, atMuchLater, '시점을 넘기면 값이 달라진다 — 그래서 그 화면의 시각을 넘겨야 한다')
|
|
166
166
|
})
|
|
167
|
+
|
|
168
|
+
/*
|
|
169
|
+
* ★ 못 본 설비의 계측 창 — **없으면 없다고 한다.**
|
|
170
|
+
*
|
|
171
|
+
* 예전에는 `metricsSinceMs: 0` 을 돌려줬다. 시각이 epoch 밀리초라서 그 창은 곧 "1970년부터" 가 되고,
|
|
172
|
+
* 공식의 분모가 지금 시각 전체가 된다 — 화면에 `idleMs 1767226603000`(약 56,000년) 유휴가 섰다.
|
|
173
|
+
* 그 값은 availability·overall 의 분모이기도 해서 **성과 숫자 전체가 조용히 거짓**이 됐다.
|
|
174
|
+
*/
|
|
175
|
+
test('이벤트를 못 본 설비는 계측 창을 만들지 않는다 — epoch 를 창으로 삼지 않는다', () => {
|
|
176
|
+
const acc = new OeeAccumulator()
|
|
177
|
+
const c = acc.counters('never-seen', Date.parse('2026-01-01T00:00:00.000Z'))
|
|
178
|
+
assert.equal(c.metricsSinceMs, undefined, '모르는 창을 0 으로 메우면 1970년부터 쟀다는 말이 된다')
|
|
179
|
+
|
|
180
|
+
/*
|
|
181
|
+
* 그다음(창이 없을 때 유휴는 0)은 **공식의 책임**이고 공식은 커널에 있다 — 커널의
|
|
182
|
+
* `test/oee-window` 가 못 박는다. 여기서 그 결과까지 단정하면 이 검사는 커널 발행 시점에
|
|
183
|
+
* 묶이고, 남의 책임을 자기 검사에 넣은 것이 된다.
|
|
184
|
+
*/
|
|
185
|
+
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 공정 목록 — **무엇을 돌리고, 그 시간이 어디서 오나.**
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 이 목록이 필요한가 (2026-08-18) ─────────────────────────────────────
|
|
5
|
+
* 「측정이 없을 때의 표준 시간」을 현장이 선언하려면 먼저 무엇에 선언할지 보여야 한다. 그런데 실측으로
|
|
6
|
+
* 운영 중인 트윈 35개 중 **창고·야드는 공정 명세 행이 0개**였다 — 모델만 보면 「공정이 없다」로 읽힌다.
|
|
7
|
+
* 실제로는 `putaway`·`spot` 을 돌리고 있고, 그 사실은 **커널만 안다**(`specCoverage`).
|
|
8
|
+
*
|
|
9
|
+
* 그래서 이 모듈이 지키는 것:
|
|
10
|
+
* ① 목록을 지어내지 않는다 — 원천 행·커널 보고·우리 선언, 세 곳에서만 모은다.
|
|
11
|
+
* ② 돌지 않은 트윈에서 「공정이 없다」고 말하지 않는다 — `basis` 가 비는 것으로 모름을 드러낸다.
|
|
12
|
+
* ③ 선언해 둔 공정이 목록에서 사라지지 않는다(그러면 철회할 자리가 없어진다).
|
|
13
|
+
*/
|
|
14
|
+
import { test } from 'node:test'
|
|
15
|
+
import assert from 'node:assert/strict'
|
|
16
|
+
|
|
17
|
+
import { readFileSync } from 'node:fs'
|
|
18
|
+
import { fileURLToPath } from 'node:url'
|
|
19
|
+
|
|
20
|
+
import { operationBasis, knownOperationKinds } from '../server/engine/operation-basis.ts'
|
|
21
|
+
|
|
22
|
+
/** 커널 흉내 — 자기보고만 낸다(이 모듈이 커널에서 읽는 것은 그것뿐이다). */
|
|
23
|
+
const kernelReporting = (ops: { kind: string; duration: 'measured' | 'declared' | 'default' }[]) => ({
|
|
24
|
+
specCoverage: () => ({
|
|
25
|
+
operations: ops.map(o => ({ ...o, parameters: [] })),
|
|
26
|
+
measuredDurations: ops.filter(o => o.duration === 'measured').length,
|
|
27
|
+
declaredDurations: ops.filter(o => o.duration === 'declared').length,
|
|
28
|
+
defaultDurations: ops.filter(o => o.duration === 'default').length
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('행이 없는 트윈에서도 돌린 공정이 나온다 — 창고·야드가 바로 그 경우다', () => {
|
|
33
|
+
const rows = operationBasis({ locations: [], equipment: [] }, kernelReporting([{ kind: 'putaway', duration: 'default' }]))
|
|
34
|
+
assert.deepEqual(rows, [{ kind: 'putaway', basis: 'default' }])
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('원천 명세 행은 이름과 시간을 함께 든다 — 우리 선언이 그것을 이긴다', () => {
|
|
38
|
+
const model = { operations: [{ key: 'weld', label: 'Welding', intent: 'process', duration: 'PT90S' }], localDurations: { weld: 'PT60S' } }
|
|
39
|
+
const rows = operationBasis(model, kernelReporting([{ kind: 'weld', duration: 'declared' }]))
|
|
40
|
+
assert.deepEqual(rows, [{ kind: 'weld', label: 'Welding', referenceDuration: 'PT90S', basis: 'declared', localDuration: 'PT60S' }])
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('정지 트윈은 basis 가 빈다 — 「공정이 없다」가 아니라 「모른다」다', () => {
|
|
44
|
+
const rows = operationBasis({ operations: [{ key: 'pick', label: 'Pick' }] }, undefined)
|
|
45
|
+
assert.deepEqual(rows, [{ kind: 'pick', label: 'Pick' }])
|
|
46
|
+
assert.equal(rows[0].basis, undefined)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('선언해 둔 공정은 커널이 아직 돌리지 않아도 목록에 남는다 — 철회할 자리가 사라지지 않게', () => {
|
|
50
|
+
const rows = operationBasis({ localDurations: { spot: 'PT300S' } }, kernelReporting([]))
|
|
51
|
+
assert.deepEqual(rows, [{ kind: 'spot', localDuration: 'PT300S' }])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('아무 근거도 없으면 목록을 지어내지 않는다 — 빈 배열이다', () => {
|
|
55
|
+
assert.deepEqual(operationBasis({}, undefined), [])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('확인 목록은 하나도 없을 때 undefined 다 — 빈 배열은 「아무것도 안 된다」로 읽힌다', () => {
|
|
59
|
+
assert.equal(knownOperationKinds({}, undefined), undefined)
|
|
60
|
+
assert.deepEqual(knownOperationKinds({}, kernelReporting([{ kind: 'putaway', duration: 'default' }])), ['putaway'])
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('커널이 보고하지 못해도 터지지 않는다 — 옛 커널에서도 목록은 나온다', () => {
|
|
64
|
+
const rows = operationBasis({ operations: [{ key: 'pick', label: 'Pick' }] }, { tick: () => {} })
|
|
65
|
+
assert.deepEqual(rows, [{ kind: 'pick', label: 'Pick' }])
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
/*
|
|
69
|
+
* ── 재기동 직후의 빈칸 (2026-08-18 실측) ────────────────────────────────────
|
|
70
|
+
* 커널의 자기보고는 **작업이 새로 생길 때** 채워진다. 그래서 이력이 풍부한 창고 트윈(pick·putaway 를
|
|
71
|
+
* 이미 배운 트윈)도 재기동 직후에는 목록이 비었다 — 값을 채울 곳을 보여야 하는 화면이 그때 아무 말도
|
|
72
|
+
* 못 한다. 이력에서 배운 종류를 넷째 근거로 넣어 그 빈칸을 메운다.
|
|
73
|
+
*/
|
|
74
|
+
test('이력에서 배운 종류가 목록에 나온다 — 재기동 직후에도 빈칸이 아니다', () => {
|
|
75
|
+
const rows = operationBasis({}, kernelReporting([]), ['pick', 'putaway'])
|
|
76
|
+
assert.deepEqual(rows, [{ kind: 'pick', basis: 'measured' }, { kind: 'putaway', basis: 'measured' }])
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('커널이 실제로 돌린 사실이 이력 목록을 덮지 않는다 — 더 강한 사실이 이긴다', () => {
|
|
80
|
+
const rows = operationBasis({}, kernelReporting([{ kind: 'putaway', duration: 'default' }]), ['putaway'])
|
|
81
|
+
assert.deepEqual(rows, [{ kind: 'putaway', basis: 'default' }], '지금 상수로 돌고 있으면 그렇게 말한다')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('이력 목록은 선언 대조에도 쓰인다 — 오타를 그때부터 잡는다', () => {
|
|
85
|
+
assert.deepEqual(knownOperationKinds({}, undefined, ['pick']), ['pick'])
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
/*
|
|
89
|
+
* ── 도는 시뮬레이션은 「저장했다」가 아니다 (2026-08-18 실측) ────────────────
|
|
90
|
+
* 시뮬 기동 경로가 `mode` 를 비워 두어(「미지정=sim」 규약에 기대) `modeOf()` 가 도는 트윈에 undefined 를
|
|
91
|
+
* 답했다. 그래서 선언·철회의 답이 「저장했다」로 나갔다 — 실제로는 도는 커널이 옛 수를 쓰고 재기동 때
|
|
92
|
+
* 받는데, 사용자는 지금 화면의 수가 새 값으로 계산된 줄로 읽는다. 사실을 적는 쪽으로 고쳤다.
|
|
93
|
+
*/
|
|
94
|
+
test('시뮬 기동이 모드를 적는다 — 「돌고 있나」를 묻는 쪽이 답을 받아야 한다', () => {
|
|
95
|
+
const src = readFileSync(fileURLToPath(new URL('../server/engine/twin-engine.ts', import.meta.url)), 'utf-8')
|
|
96
|
+
assert.match(src, /id, domainId, mode: 'sim', runtime/, '시뮬 런타임도 모드를 든다')
|
|
97
|
+
/* 답이 갈리는 자리 — 도는 트윈이면 「재기동 때 받는다」로 말해야 한다. */
|
|
98
|
+
const mut = readFileSync(fileURLToPath(new URL('../server/service/twin-model/twin-model-mutation.ts', import.meta.url)), 'utf-8')
|
|
99
|
+
assert.match(mut, /running \? 'on-restart' : 'stored'/)
|
|
100
|
+
})
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ 생산 능력 질의 — **트윈 하나를 두고 "지금 더 받을 수 있나" 를 물을 길.**
|
|
3
|
+
*
|
|
4
|
+
* 커널이 계산과 표준 등식을 지킨다(kernel/test/operations-capability). 이 검사가 지키는 것은 그 앞의
|
|
5
|
+
* 배선이다 — 그리고 여기서 지켜야 할 규율은 `twinCapacity` 와 같다:
|
|
6
|
+
*
|
|
7
|
+
* · **대상은 등록부가 정한다** — 떠 있는 런타임만 훑으면 "안 떠 있어서 안 보이는 것" 과 "없는 것" 이
|
|
8
|
+
* 구별되지 않는다. 화면은 그 둘을 다르게 말해야 한다.
|
|
9
|
+
* · **공정을 선언하지 않은 트윈은 판정 대상이 아니다** — 0 으로 채우면 "야드가 병목" 이라는 없는
|
|
10
|
+
* 사실이 생긴다.
|
|
11
|
+
* · **구간은 부르는 쪽이 말한다** — 기본값을 두면 화면마다 다른 구간을 보면서 같은 값으로 읽는다.
|
|
12
|
+
*/
|
|
13
|
+
import { test, before, after } from 'node:test'
|
|
14
|
+
import assert from 'node:assert/strict'
|
|
15
|
+
import { createRequire } from 'node:module'
|
|
16
|
+
import type { DataSource } from 'typeorm'
|
|
17
|
+
|
|
18
|
+
const req = createRequire(import.meta.url)
|
|
19
|
+
const { NamingStrategy, addDataSource, entities: shellEntities, Domain } = req('@things-factory/shell')
|
|
20
|
+
const { entities: authEntities } = req('@things-factory/auth-base')
|
|
21
|
+
const { entities: twinEntities } = req('../dist-server/service/index.js')
|
|
22
|
+
const { TwinEngine } = req('../dist-server/engine/index.js')
|
|
23
|
+
const { TwinJournalQuery } = req('../dist-server/service/twin-journal/twin-journal-query.js')
|
|
24
|
+
const { DataSource: TypeOrmDataSource } = req('typeorm')
|
|
25
|
+
|
|
26
|
+
let ds: DataSource
|
|
27
|
+
let domainId: string
|
|
28
|
+
let q: any
|
|
29
|
+
let ctx: any
|
|
30
|
+
|
|
31
|
+
const MES = 'cap-mes'
|
|
32
|
+
const WMS = 'cap-wms'
|
|
33
|
+
const WINDOW = { fromTime: '2026-01-05T00:00:00.000Z', toTime: '2026-01-06T00:00:00.000Z' }
|
|
34
|
+
|
|
35
|
+
/** 절단 하나 — 설비 2대·사람 2명, 사이클 1시간. 24시간 창이면 상한 48대. */
|
|
36
|
+
const mesMaster = (): any => ({
|
|
37
|
+
source: MES,
|
|
38
|
+
system: 'mes',
|
|
39
|
+
siteName: '능력 공장',
|
|
40
|
+
space: { spaceId: 'cap-site', width: 60, depth: 40, unit: 'm' },
|
|
41
|
+
locations: [
|
|
42
|
+
{ id: 'raw-store', type: 'raw-store', role: 'location', name: '자재 보관', capacity: 100 },
|
|
43
|
+
{ id: 'cut-station', type: 'cut-station', role: 'location', name: '절단', capacity: 10 },
|
|
44
|
+
{ id: 'fg-store', type: 'fg-store', role: 'location', name: '완제품', capacity: 100 }
|
|
45
|
+
],
|
|
46
|
+
equipment: [
|
|
47
|
+
{ id: 'cutter-1', kind: 'cutter', homeLocationId: 'cut-station' },
|
|
48
|
+
{ id: 'cutter-2', kind: 'cutter', homeLocationId: 'cut-station' }
|
|
49
|
+
],
|
|
50
|
+
personnelClasses: [{ id: 'cutter-op', description: '절단 작업자' }],
|
|
51
|
+
persons: [
|
|
52
|
+
{ id: 'c-1', personnelClassIds: ['cutter-op'], homeLocationId: 'cut-station' },
|
|
53
|
+
{ id: 'c-2', personnelClassIds: ['cutter-op'], homeLocationId: 'cut-station' }
|
|
54
|
+
],
|
|
55
|
+
operations: [
|
|
56
|
+
{
|
|
57
|
+
key: 'cut', label: '절단', intent: 'process', locationType: 'cut-station', resourceType: 'cutter',
|
|
58
|
+
duration: 'PT1H',
|
|
59
|
+
equipmentSpecification: [{ equipmentClass: 'cutter', quantity: 1 }],
|
|
60
|
+
personnelSpecification: [{ personnelClass: 'cutter-op', quantity: 1 }]
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
productionSpec: {
|
|
64
|
+
/* 레시피 생산은 자재 키를 GS1 로 풀 수 있어야 한다 — 커널이 없으면 기동을 거절한다(정확한 거절이다). */
|
|
65
|
+
companyPrefix: '0614141',
|
|
66
|
+
binding: { steel: '077101', frame: '078101' },
|
|
67
|
+
definition: {
|
|
68
|
+
materials: [{ id: 'steel' }, { id: 'frame' }],
|
|
69
|
+
routes: [{ key: 'r', steps: ['cut'] }],
|
|
70
|
+
recipes: [{ key: 'rc', label: '프레임', route: 'r', inputs: [{ material: 'steel', qty: 1 }], outputs: [{ material: 'frame', qty: 1 }] }]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
/** 공정을 선언하지 않는 창고 — 판정 대상이 아님을 확인하는 대조군. */
|
|
76
|
+
const wmsMaster = (): any => ({
|
|
77
|
+
source: WMS,
|
|
78
|
+
system: 'wms',
|
|
79
|
+
siteName: '능력 창고',
|
|
80
|
+
space: { spaceId: 'cap-wh', width: 40, depth: 30, unit: 'm' },
|
|
81
|
+
locations: [
|
|
82
|
+
{ id: 'dock', type: 'dock', role: 'location', name: '도크', capacity: 50 },
|
|
83
|
+
{ id: 'bin-1', type: 'storage', role: 'location', name: '빈1', capacity: 20 }
|
|
84
|
+
],
|
|
85
|
+
equipment: [{ id: 'fk-1', kind: 'forklift', homeLocationId: 'dock' }]
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
before(async () => {
|
|
89
|
+
ds = new TypeOrmDataSource({
|
|
90
|
+
type: 'sqlite',
|
|
91
|
+
database: ':memory:',
|
|
92
|
+
synchronize: true,
|
|
93
|
+
dropSchema: true,
|
|
94
|
+
namingStrategy: new NamingStrategy(),
|
|
95
|
+
entities: [...shellEntities, ...authEntities, ...twinEntities],
|
|
96
|
+
logging: false
|
|
97
|
+
})
|
|
98
|
+
await ds.initialize()
|
|
99
|
+
addDataSource('default', ds)
|
|
100
|
+
const d = await ds.getRepository(Domain).save({ name: 'cap-test', subdomain: 'cap-test' } as any)
|
|
101
|
+
domainId = d.id
|
|
102
|
+
q = new TwinJournalQuery()
|
|
103
|
+
ctx = { state: { domain: { id: domainId } } }
|
|
104
|
+
|
|
105
|
+
await TwinEngine.ingestMaster(domainId, mesMaster())
|
|
106
|
+
await TwinEngine.ingestMaster(domainId, wmsMaster())
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
after(async () => {
|
|
110
|
+
for (const id of [MES, WMS]) await TwinEngine.stop(domainId, id).catch(() => {})
|
|
111
|
+
await ds?.destroy()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const ask = (instanceId: string) =>
|
|
115
|
+
q.twinOperationsCapability(instanceId, undefined, 24, WINDOW.fromTime, WINDOW.toTime, WINDOW.fromTime, ctx)
|
|
116
|
+
|
|
117
|
+
test('실행 중이 아니면 그 사실을 알린다 — "없는 것" 과 구별된다', async () => {
|
|
118
|
+
const rows = await ask(MES)
|
|
119
|
+
assert.equal(rows.length, 1, '등록부에 있으면 목록에 선다(떠 있지 않아도)')
|
|
120
|
+
assert.equal(rows[0].reason, 'not-running')
|
|
121
|
+
assert.equal(rows[0].report, undefined, '값을 지어내지 않는다')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('돌고 있으면 구간의 양을 종류로 갈라 낸다 — 표준 등식이 성립한다', async () => {
|
|
125
|
+
await TwinEngine.startFromRegistry(domainId, MES, 'sim-experiment')
|
|
126
|
+
const rows = await ask(MES)
|
|
127
|
+
assert.equal(rows[0].reason, undefined)
|
|
128
|
+
|
|
129
|
+
const report = rows[0].report
|
|
130
|
+
assert.equal(report.operationsType, 'Production', '표준 열거값을 그대로 낸다')
|
|
131
|
+
assert.equal(report.window.fromTime, WINDOW.fromTime)
|
|
132
|
+
assert.equal(report.window.workingHours, 24, '캘린더 선언이 없으면 종일 가동으로 본다')
|
|
133
|
+
|
|
134
|
+
const cut = report.perOperation.find((o: any) => o.operation === 'cut')
|
|
135
|
+
assert.ok(cut, '선언한 공정이 보고서에 있어야 한다')
|
|
136
|
+
assert.equal(cut.total, 48, '설비 2대·사람 2명·사이클 1시간 → 하루 48대')
|
|
137
|
+
assert.equal(cut.total, cut.committed + cut.available + cut.unattainable, 'Total = Committed + Available + Unattainable')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('공정을 선언하지 않은 트윈은 판정 대상이 아니다 — 0 으로 채우면 없는 병목이 생긴다', async () => {
|
|
141
|
+
await TwinEngine.startFromRegistry(domainId, WMS, 'sim-experiment')
|
|
142
|
+
const rows = await q.twinOperationsCapability(WMS, undefined, 24, WINDOW.fromTime, WINDOW.toTime, WINDOW.fromTime, ctx)
|
|
143
|
+
assert.equal(rows[0].reason, 'no-operations')
|
|
144
|
+
assert.equal(rows[0].report, undefined)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('구간은 호출하는 쪽이 지정한다 — 뒤집힌 구간·잘못된 시각은 거절한다', async () => {
|
|
148
|
+
await assert.rejects(
|
|
149
|
+
() => q.twinOperationsCapability(MES, undefined, 24, WINDOW.toTime, WINDOW.fromTime, undefined, ctx),
|
|
150
|
+
/inverted/
|
|
151
|
+
)
|
|
152
|
+
await assert.rejects(
|
|
153
|
+
() => q.twinOperationsCapability(MES, undefined, 24, 'not-a-time', WINDOW.toTime, undefined, ctx),
|
|
154
|
+
/valid ISO time/
|
|
155
|
+
)
|
|
156
|
+
await assert.rejects(() => q.twinOperationsCapability(MES, undefined, 0, WINDOW.fromTime, WINDOW.toTime, undefined, ctx))
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('남의 테넌트 트윈은 열리지 않는다 — 능력은 현장의 사실이다', async () => {
|
|
160
|
+
const other = { state: { domain: { id: 'no-such-domain' } } }
|
|
161
|
+
await assert.rejects(
|
|
162
|
+
() => q.twinOperationsCapability(MES, undefined, 24, WINDOW.fromTime, WINDOW.toTime, undefined, other),
|
|
163
|
+
/not found in this tenant/
|
|
164
|
+
)
|
|
165
|
+
})
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 구조 투영 — `TwinModelDef`(커널 입력 문서) → **표준 엔티티 행**(ADR-0032).
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 DB 검사인가 ─────────────────────────────────────────────────────────
|
|
5
|
+
* 이 코드의 값어치는 **DB 가 대신 지켜 주는 것**에 있다 — 끊어진 참조를 FK 가 막고, 원본 없는 행을
|
|
6
|
+
* NOT NULL 이 막는다. 순수 함수 검사로는 그게 하나도 확인되지 않는다. 그래서 실제 DataSource 를
|
|
7
|
+
* 세운다(멀티DB 규약상 sqlite 로 도는 것이 5개 드라이버 공통의 최소선이기도 하다).
|
|
8
|
+
*
|
|
9
|
+
* ── 이 검사가 지키는 것 ────────────────────────────────────────────────────
|
|
10
|
+
* · 문서의 세 축이 행으로 선다
|
|
11
|
+
* · 설비→자리가 **진짜 FK** 로 이어진다(문자열 대조가 아니라)
|
|
12
|
+
* · 끊어진 참조는 **이름을 대고 보고**되고, 행 자체는 남는다(막지 않되 침묵하지 않는다)
|
|
13
|
+
* · **전량 교체** — 두 번 돌려도 늘지 않고, 원본에서 사라진 것은 행에서도 사라진다
|
|
14
|
+
* · 원본 표식(`sourceRef`·`ingestedAt`) 없는 행은 만들 수 없다(캐시라는 사실이 구조로 지켜진다)
|
|
15
|
+
* · 소요시간은 ISO 8601 **문자열 그대로** 남는다(분으로 접으면 초가 조용히 사라진다)
|
|
16
|
+
*/
|
|
17
|
+
import { test, before, after, beforeEach } from 'node:test'
|
|
18
|
+
import assert from 'node:assert/strict'
|
|
19
|
+
import { createRequire } from 'node:module'
|
|
20
|
+
import type { DataSource } from 'typeorm'
|
|
21
|
+
|
|
22
|
+
const req = createRequire(import.meta.url)
|
|
23
|
+
const { NamingStrategy, addDataSource, entities: shellEntities, Domain } = req('@things-factory/shell')
|
|
24
|
+
const { entities: authEntities } = req('@things-factory/auth-base')
|
|
25
|
+
const { entities: twinEntities } = req('../dist-server/service/index.js')
|
|
26
|
+
const { projectStructure } = req('../dist-server/service/twin-model/project-structure.js')
|
|
27
|
+
const { TwinInstance } = req('../dist-server/service/twin-instance/twin-instance.js')
|
|
28
|
+
const { TwinSpace } = req('../dist-server/service/twin-space/twin-space.js')
|
|
29
|
+
const { TwinArea } = req('../dist-server/service/twin-space/twin-area.js')
|
|
30
|
+
const { TwinLocation } = req('../dist-server/service/twin-model/twin-location.js')
|
|
31
|
+
const { TwinEquipment } = req('../dist-server/service/twin-model/twin-equipment.js')
|
|
32
|
+
const { TwinOperation } = req('../dist-server/service/twin-model/twin-operation.js')
|
|
33
|
+
const { DataSource: TypeOrmDataSource } = req('typeorm')
|
|
34
|
+
|
|
35
|
+
let ds: DataSource
|
|
36
|
+
let domainId: string
|
|
37
|
+
|
|
38
|
+
const INSTANCE = 'projection-probe'
|
|
39
|
+
const SOURCE = 'projection-probe'
|
|
40
|
+
|
|
41
|
+
/** 실제 트레일러 트윈과 같은 모양의 문서(축소판). */
|
|
42
|
+
const model = () => ({
|
|
43
|
+
locations: [
|
|
44
|
+
{ id: 'cut-station', type: 'cut-station', name: '프레임 절단', capacity: 20, parentId: 'a-floor' },
|
|
45
|
+
{ id: 'weld-station', type: 'weld-station', capacity: 20, parentId: 'a-floor' },
|
|
46
|
+
{ id: 'orphan-bin', type: 'storage', capacity: 5, parentId: 'no-such-area' }
|
|
47
|
+
],
|
|
48
|
+
equipment: [
|
|
49
|
+
{ id: 'cutter-1', kind: 'cutter', name: '절단기 1호', homeLocation: 'cut-station', mtbfMs: 3600000,
|
|
50
|
+
properties: [{ id: 'speed', value: '1.2', dataType: 'xs:double', uom: 'MTS' }] },
|
|
51
|
+
{ id: 'welder-1', kind: 'welder', homeLocation: 'weld-station' },
|
|
52
|
+
{ id: 'ghost-1', kind: 'welder', homeLocation: 'no-such-location' }
|
|
53
|
+
],
|
|
54
|
+
operations: [
|
|
55
|
+
{ key: 'cut', label: 'twin.tpl.x.cut', intent: 'process', locationType: 'cut-station', resourceType: 'cutter',
|
|
56
|
+
duration: 'PT1H10M30S', variability: { distribution: 'triangular', min: 'PT1H', max: 'PT1H30M' },
|
|
57
|
+
parameters: [{ id: 'yield', value: '0.995' }],
|
|
58
|
+
/* 네 자원 명세 — 설비는 `resourceType`, 나머지 셋은 아래. */
|
|
59
|
+
personnelSpecification: [{ personnelClass: 'cutter-op', quantity: 1 }],
|
|
60
|
+
physicalAssetSpecification: [{ assetClass: 'jig', quantity: 1 }],
|
|
61
|
+
materialSpecification: [{ materialDefinition: 'steel', use: 'consumed', quantity: 2 }] }
|
|
62
|
+
]
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
before(async () => {
|
|
66
|
+
ds = new TypeOrmDataSource({
|
|
67
|
+
type: 'sqlite',
|
|
68
|
+
database: ':memory:',
|
|
69
|
+
synchronize: true,
|
|
70
|
+
dropSchema: true,
|
|
71
|
+
namingStrategy: new NamingStrategy(),
|
|
72
|
+
entities: [...shellEntities, ...authEntities, ...twinEntities],
|
|
73
|
+
logging: false
|
|
74
|
+
})
|
|
75
|
+
await ds.initialize()
|
|
76
|
+
addDataSource('default', ds)
|
|
77
|
+
const d = await ds.getRepository(Domain).save({ name: 'projection-test', subdomain: 'projection-test' } as any)
|
|
78
|
+
domainId = d.id
|
|
79
|
+
|
|
80
|
+
await ds.getRepository(TwinInstance).save({
|
|
81
|
+
domain: { id: domainId },
|
|
82
|
+
instanceId: INSTANCE,
|
|
83
|
+
kind: 'mes',
|
|
84
|
+
realityMode: 'sim-experiment',
|
|
85
|
+
purpose: 'operational',
|
|
86
|
+
status: 'stopped',
|
|
87
|
+
model: {}
|
|
88
|
+
} as any)
|
|
89
|
+
|
|
90
|
+
/* 구역은 model 밖(호스트 개념) — 자리가 FK 로 이걸 가리킨다. */
|
|
91
|
+
const space = await ds.getRepository(TwinSpace).save({ domain: { id: domainId }, spaceId: 'sp-1', name: '공장' } as any)
|
|
92
|
+
await ds.getRepository(TwinArea).save({ domain: { id: domainId }, space: { id: space.id }, areaId: 'a-floor', name: '생산 플로어' } as any)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
after(async () => {
|
|
96
|
+
await ds?.destroy()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
beforeEach(async () => {
|
|
100
|
+
await projectStructure(domainId, INSTANCE, model(), SOURCE)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('문서의 세 축이 행으로 선다', async () => {
|
|
104
|
+
assert.equal(await ds.getRepository(TwinLocation).count(), 3)
|
|
105
|
+
assert.equal(await ds.getRepository(TwinEquipment).count(), 3)
|
|
106
|
+
assert.equal(await ds.getRepository(TwinOperation).count(), 1)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('설비→자리가 진짜 FK 로 이어진다 — 문자열 대조가 아니다', async () => {
|
|
110
|
+
const eq = await ds.getRepository(TwinEquipment).findOne({ where: { equipmentId: 'cutter-1' }, relations: ['homeLocation'] })
|
|
111
|
+
assert.equal(eq?.homeLocation?.locationId, 'cut-station', 'FK 로 자리를 따라갈 수 있어야 한다')
|
|
112
|
+
/* 자리 쪽에서도 같은 행이다 — 문자열이었으면 같은 값이지만 같은 행은 아니다. */
|
|
113
|
+
const loc = await ds.getRepository(TwinLocation).findOne({ where: { locationId: 'cut-station' } })
|
|
114
|
+
assert.equal(eq?.homeLocation?.id, loc?.id)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('자리→구역이 FK 로 이어진다 — 커널이 "밖" 이라 부른 참조가 호스트에서는 평범한 FK 다', async () => {
|
|
118
|
+
const loc = await ds.getRepository(TwinLocation).findOne({ where: { locationId: 'cut-station' }, relations: ['area'] })
|
|
119
|
+
assert.equal(loc?.area?.areaId, 'a-floor')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('끊어진 참조는 이름을 대고 보고된다 — 막지 않되 침묵하지 않는다', async () => {
|
|
123
|
+
const report = await projectStructure(domainId, INSTANCE, model(), SOURCE)
|
|
124
|
+
assert.deepEqual(report.unresolvedHome, ['ghost-1→no-such-location'])
|
|
125
|
+
assert.deepEqual(report.unresolvedArea, ['orphan-bin→no-such-area'])
|
|
126
|
+
|
|
127
|
+
/* 행 자체는 남는다 — 참조 하나 때문에 설비가 통째로 사라지면 그게 더 나쁜 거짓말이다. */
|
|
128
|
+
const ghost = await ds.getRepository(TwinEquipment).findOne({ where: { equipmentId: 'ghost-1' }, relations: ['homeLocation'] })
|
|
129
|
+
assert.ok(ghost, '끊어진 참조를 가진 설비도 행으로 남는다')
|
|
130
|
+
assert.equal(ghost?.homeLocation, null, '이을 수 없는 참조는 비운다(엉뚱한 자리에 붙이지 않는다)')
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('전량 교체 — 두 번 돌려도 늘지 않는다', async () => {
|
|
134
|
+
await projectStructure(domainId, INSTANCE, model(), SOURCE)
|
|
135
|
+
await projectStructure(domainId, INSTANCE, model(), SOURCE)
|
|
136
|
+
assert.equal(await ds.getRepository(TwinLocation).count(), 3)
|
|
137
|
+
assert.equal(await ds.getRepository(TwinEquipment).count(), 3)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('원본에서 사라진 것은 행에서도 사라진다 — 캐시는 원본을 따라간다', async () => {
|
|
141
|
+
const shrunk = model()
|
|
142
|
+
shrunk.locations = shrunk.locations.filter(l => l.id !== 'weld-station')
|
|
143
|
+
shrunk.equipment = shrunk.equipment.filter(m => m.id !== 'welder-1')
|
|
144
|
+
await projectStructure(domainId, INSTANCE, shrunk, SOURCE)
|
|
145
|
+
assert.equal(await ds.getRepository(TwinLocation).count(), 2)
|
|
146
|
+
assert.equal(await ds.getRepository(TwinEquipment).count(), 2)
|
|
147
|
+
assert.equal(await ds.getRepository(TwinLocation).countBy({ locationId: 'weld-station' } as any), 0)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('원본 표식 없는 행은 만들 수 없다 — 캐시라는 사실이 구조로 지켜진다', async () => {
|
|
151
|
+
await assert.rejects(
|
|
152
|
+
() =>
|
|
153
|
+
ds.getRepository(TwinLocation).save({
|
|
154
|
+
domain: { id: domainId },
|
|
155
|
+
locationId: 'no-origin',
|
|
156
|
+
type: 'storage'
|
|
157
|
+
/* sourceRef · ingestedAt 없음 */
|
|
158
|
+
} as any),
|
|
159
|
+
/NOT NULL|constraint/i,
|
|
160
|
+
'출처 없는 행이 들어가면 "원본에서 온 캐시" 라는 규율이 구조로는 안 지켜지는 것이다'
|
|
161
|
+
)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('소요시간은 ISO 8601 문자열 그대로 — 분으로 접으면 초가 조용히 사라진다', async () => {
|
|
165
|
+
const op = await ds.getRepository(TwinOperation).findOne({ where: { operationKey: 'cut' } })
|
|
166
|
+
assert.equal(op?.duration, 'PT1H10M30S')
|
|
167
|
+
/* 변동성·파라미터는 도메인마다 형이 달라 원형 그대로 싣는다. */
|
|
168
|
+
assert.equal(op?.variability?.distribution, 'triangular')
|
|
169
|
+
assert.deepEqual(op?.parameters, [{ id: 'yield', value: '0.995' }])
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('등록되지 않은 트윈에는 투영하지 않는다 — 주인 없는 행을 만들지 않는다', async () => {
|
|
173
|
+
await assert.rejects(() => projectStructure(domainId, 'not-registered', model(), SOURCE), /not registered/)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test('네 자원 명세가 함께 옮겨진다 — 선언한 것만 제약이 되므로 빠지면 화면이 거짓이 된다', async () => {
|
|
177
|
+
/*
|
|
178
|
+
* ISA-95 는 공정이 무엇을 필요로 하는지를 넷으로 말한다: 설비·인원·자산·자재. 셋이 투영에서
|
|
179
|
+
* 떨어져 있어서, 조회 화면이 "원본이 말한 값" 이라 적고도 넷 중 하나만 보였다 — 사람이 필요한
|
|
180
|
+
* 공정을 보고 **사람이 필요 없다고 결론 내게** 되어 있었다.
|
|
181
|
+
*/
|
|
182
|
+
const op = await ds.getRepository(TwinOperation).findOne({ where: { operationKey: 'cut' } })
|
|
183
|
+
assert.deepEqual(op?.personnelSpecification, [{ personnelClass: 'cutter-op', quantity: 1 }], '인원 명세가 행에 남아야 한다')
|
|
184
|
+
assert.deepEqual(op?.physicalAssetSpecification, [{ assetClass: 'jig', quantity: 1 }], '자산 명세도 남아야 한다')
|
|
185
|
+
assert.deepEqual(op?.materialSpecification, [{ materialDefinition: 'steel', use: 'consumed', quantity: 2 }], '자재 명세도 남아야 한다')
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('이름이 행까지 온다 — 이름이 없으면 화면에 id 만 남는다', async () => {
|
|
189
|
+
/*
|
|
190
|
+
* 투영이 마스터의 `name` 을 버리고 있었다. 행에 이름이 없으면 어떤 화면도 이름을 만들 수 없어서
|
|
191
|
+
* 목록·표·관계 그림이 모두 `cut-station` 을 찍었다 — 표면마다 따로 보이지만 결함은 여기 하나다.
|
|
192
|
+
*/
|
|
193
|
+
const loc = await ds.getRepository(TwinLocation).findOne({ where: { domain: { id: domainId } as any, locationId: 'cut-station' } as any })
|
|
194
|
+
assert.equal(loc!.name, '프레임 절단', '자리의 이름이 행에 실려야 한다')
|
|
195
|
+
|
|
196
|
+
const eq = await ds.getRepository(TwinEquipment).findOne({ where: { domain: { id: domainId } as any, equipmentId: 'cutter-1' } as any })
|
|
197
|
+
assert.equal(eq!.name, '절단기 1호', '설비의 이름이 행에 실려야 한다')
|
|
198
|
+
|
|
199
|
+
/* 공정은 마스터가 `label` 로 말한다 — 행에서는 규약의 이름으로 둔다(어휘를 하나로). */
|
|
200
|
+
const op = await ds.getRepository(TwinOperation).findOne({ where: { domain: { id: domainId } as any, operationKey: 'cut' } as any })
|
|
201
|
+
assert.equal(op!.name, 'twin.tpl.x.cut', '공정의 이름이 행에 실려야 한다')
|
|
202
|
+
|
|
203
|
+
/* **이름 없는 원본은 이름 없이 남는다** — id 를 이름으로 베끼면 "이름이 있다" 는 거짓이 되고,
|
|
204
|
+
화면은 이름 없음을 알아볼 수 없게 된다. */
|
|
205
|
+
const noname = await ds.getRepository(TwinLocation).findOne({ where: { domain: { id: domainId } as any, locationId: 'weld-station' } as any })
|
|
206
|
+
assert.equal(noname!.name ?? null, null, '원본이 이름을 말하지 않았으면 비어 있어야 한다')
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
test('자원 속성이 행까지 온다 — 소비처가 실제로 읽는 사실이다(주행 속도)', async () => {
|
|
210
|
+
/*
|
|
211
|
+
* 모델에는 있는데 행에 자리가 없어 투영이 흘리고 있었다. 그러면 조회 화면은 "원본이 말한 값" 이라
|
|
212
|
+
* 적고도 속성을 못 보인다 — 이름이 같은 식으로 사라졌던 것과 같은 부류다.
|
|
213
|
+
*
|
|
214
|
+
* 단위를 값과 함께 담는지도 본다: 단위 없는 "1.2" 는 초속인지 시속인지 말하지 않는다.
|
|
215
|
+
*/
|
|
216
|
+
const eq = await ds.getRepository(TwinEquipment).findOne({ where: { domain: { id: domainId } as any, equipmentId: 'cutter-1' } as any })
|
|
217
|
+
const props = (eq!.properties ?? []) as any[]
|
|
218
|
+
assert.equal(props.length, 1, '선언한 속성이 행에 실려야 한다')
|
|
219
|
+
assert.equal(props[0].id, 'speed')
|
|
220
|
+
assert.equal(props[0].value, '1.2')
|
|
221
|
+
assert.equal(props[0].uom, 'MTS', '단위가 값과 함께 남아야 한다')
|
|
222
|
+
|
|
223
|
+
/* 선언하지 않은 자원은 **비어 있다** — 빈 배열로 "선언했다" 고 말하지 않는다. */
|
|
224
|
+
const noneProps = await ds.getRepository(TwinEquipment).findOne({ where: { domain: { id: domainId } as any, equipmentId: 'welder-1' } as any })
|
|
225
|
+
assert.equal(noneProps!.properties ?? null, null)
|
|
226
|
+
})
|