@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,275 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ 개념 안의 목록과 항목 하나 — **막다른 길을 만들지 않는다.**
|
|
3
|
+
*
|
|
4
|
+
* 개념 지도가 숫자만 내면 그 숫자가 막다른 길이다. 인스펙션의 원칙은 "화면에 보이는 모든 식별자는
|
|
5
|
+
* 그 자신의 상세로 가는 문" 이므로, 셈 다음에 **목록**과 **항목**이 있어야 그 원칙이 성립한다.
|
|
6
|
+
*
|
|
7
|
+
* 여기서 지키는 것:
|
|
8
|
+
* · 어떻게 셌는지 밝힌다 — 행이면 DB 가 페이징, 문서면 서버가 통째로 읽어 잘랐다(`basis`)
|
|
9
|
+
* · 조용히 자르지 않는다 — 총 건수가 항상 함께 오고, 훑기가 상한에 닿으면 그렇다고 말한다
|
|
10
|
+
* · 모르는 축은 **거절한다** — 빈 목록으로 답하면 오타 하나가 "우리 트윈엔 그게 없다" 로 굳는다
|
|
11
|
+
* · 투영이 낡아도 항목은 열린다 — 행에 없으면 문서에서 찾는다
|
|
12
|
+
* · 이어진 것은 **양방향**이고, 선언된 관계만 따른다(선언에 없는 길은 그리지 않는다)
|
|
13
|
+
* · 관측이 없는 것과 견주지 못한 것을 구별한다(`observed: null`)
|
|
14
|
+
*/
|
|
15
|
+
import { test, before, after } from 'node:test'
|
|
16
|
+
import assert from 'node:assert/strict'
|
|
17
|
+
import { createRequire } from 'node:module'
|
|
18
|
+
import type { DataSource } from 'typeorm'
|
|
19
|
+
|
|
20
|
+
const req = createRequire(import.meta.url)
|
|
21
|
+
const { NamingStrategy, addDataSource, entities: shellEntities, Domain } = req('@things-factory/shell')
|
|
22
|
+
const { entities: authEntities } = req('@things-factory/auth-base')
|
|
23
|
+
const { entities: twinEntities } = req('../dist-server/service/index.js')
|
|
24
|
+
const { TwinEngine } = req('../dist-server/engine/index.js')
|
|
25
|
+
const { TwinModelItemQuery } = req('../dist-server/service/twin-model/twin-model-item-query.js')
|
|
26
|
+
const { TwinModelQuery } = req('../dist-server/service/twin-model/twin-model-query.js')
|
|
27
|
+
const { TwinSpace } = req('../dist-server/service/twin-space/twin-space.js')
|
|
28
|
+
const { TwinArea } = req('../dist-server/service/twin-space/twin-area.js')
|
|
29
|
+
const { DataSource: TypeOrmDataSource } = req('typeorm')
|
|
30
|
+
|
|
31
|
+
let ds: DataSource
|
|
32
|
+
let domainId: string
|
|
33
|
+
let q: any
|
|
34
|
+
let ctx: any
|
|
35
|
+
|
|
36
|
+
const INSTANCE = 'item-probe'
|
|
37
|
+
|
|
38
|
+
/** MES 마스터 — 자리·설비·공정에 더해 **문서에만 사는 축**(레시피·라우트)까지 있는 모양. */
|
|
39
|
+
const master = (): any => ({
|
|
40
|
+
source: INSTANCE,
|
|
41
|
+
system: 'mes',
|
|
42
|
+
siteName: '항목 공장',
|
|
43
|
+
space: { width: 80, depth: 40, unit: 'm' },
|
|
44
|
+
locations: [
|
|
45
|
+
{ id: 'a-floor', type: 'production-line', role: 'area', name: '생산 플로어' },
|
|
46
|
+
{ id: 'cut-station', type: 'cut-station', role: 'location', name: '절단', parentId: 'a-floor', capacity: 20 },
|
|
47
|
+
{ id: 'weld-station', type: 'weld-station', role: 'location', name: '용접', parentId: 'a-floor', capacity: 20 }
|
|
48
|
+
],
|
|
49
|
+
equipment: [
|
|
50
|
+
{ id: 'cutter-1', kind: 'cutter', homeLocationId: 'cut-station', mtbfMs: 3600000 },
|
|
51
|
+
{ id: 'welder-1', kind: 'welder', homeLocationId: 'weld-station' },
|
|
52
|
+
{ id: 'welder-2', kind: 'welder', homeLocationId: 'weld-station' }
|
|
53
|
+
],
|
|
54
|
+
operations: [
|
|
55
|
+
{ key: 'cut', label: '절단', intent: 'process', locationType: 'cut-station', resourceType: 'cutter', duration: 'PT10M' },
|
|
56
|
+
{ key: 'weld', label: '용접', intent: 'process', locationType: 'weld-station', resourceType: 'welder', duration: 'PT20M' }
|
|
57
|
+
],
|
|
58
|
+
productionSpec: {
|
|
59
|
+
definition: {
|
|
60
|
+
materials: [{ id: 'steel' }, { id: 'frame' }],
|
|
61
|
+
routes: [{ key: 'frame-route', steps: ['cut', 'weld'] }],
|
|
62
|
+
recipes: [{ key: 'frame-recipe', route: 'frame-route', inputs: [{ material: 'steel' }], outputs: [{ material: 'frame' }] }],
|
|
63
|
+
operations: [
|
|
64
|
+
{ key: 'cut', label: '절단', intent: 'process', locationType: 'cut-station', resourceType: 'cutter', duration: 'PT10M' },
|
|
65
|
+
{ key: 'weld', label: '용접', intent: 'process', locationType: 'weld-station', resourceType: 'welder', duration: 'PT20M' }
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
before(async () => {
|
|
72
|
+
ds = new TypeOrmDataSource({
|
|
73
|
+
type: 'sqlite',
|
|
74
|
+
database: ':memory:',
|
|
75
|
+
synchronize: true,
|
|
76
|
+
dropSchema: true,
|
|
77
|
+
namingStrategy: new NamingStrategy(),
|
|
78
|
+
entities: [...shellEntities, ...authEntities, ...twinEntities],
|
|
79
|
+
logging: false
|
|
80
|
+
})
|
|
81
|
+
await ds.initialize()
|
|
82
|
+
addDataSource('default', ds)
|
|
83
|
+
const d = await ds.getRepository(Domain).save({ name: 'item-test', subdomain: 'item-test' } as any)
|
|
84
|
+
domainId = d.id
|
|
85
|
+
q = new TwinModelItemQuery()
|
|
86
|
+
ctx = { state: { domain: { id: domainId } } }
|
|
87
|
+
|
|
88
|
+
await TwinEngine.ingestMaster(domainId, master())
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
after(async () => {
|
|
92
|
+
await TwinEngine.stop(domainId, INSTANCE).catch(() => {})
|
|
93
|
+
await ds?.destroy()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('행으로 옮긴 축은 DB 가 페이징하고 그 사실을 밝힌다', async () => {
|
|
97
|
+
const page1 = await q.twinModelEntities(INSTANCE, 'equipment', ctx, undefined, 0, 2)
|
|
98
|
+
assert.equal(page1.basis, 'rows', 'DB 가 셌다는 사실이 응답에 있어야 한다')
|
|
99
|
+
assert.equal(page1.total, 3, '총 건수는 페이지 크기와 무관하게 사실이다')
|
|
100
|
+
assert.equal(page1.items.length, 2)
|
|
101
|
+
|
|
102
|
+
const page2 = await q.twinModelEntities(INSTANCE, 'equipment', ctx, undefined, 2, 2)
|
|
103
|
+
assert.equal(page2.items.length, 1, '이어 읽을 수 있다')
|
|
104
|
+
const seen = [...page1.items, ...page2.items].map((i: any) => i.id).sort()
|
|
105
|
+
assert.deepEqual(seen, ['cutter-1', 'welder-1', 'welder-2'], '두 페이지가 전건을 덮는다')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('아직 문서에 사는 축도 열린다 — 이주 전이라고 안 보이면 안 된다', async () => {
|
|
109
|
+
const r = await q.twinModelEntities(INSTANCE, 'recipes', ctx)
|
|
110
|
+
assert.equal(r.basis, 'document', '문서를 읽어 잘랐다는 사실을 밝힌다(규모 한계가 드러나야 한다)')
|
|
111
|
+
assert.equal(r.total, 1)
|
|
112
|
+
assert.deepEqual(r.items.map((i: any) => i.id), ['frame-recipe'])
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('검색이 전수 조회라는 사실을 드러낸다 — 감추면 한계를 아무도 모른다', async () => {
|
|
116
|
+
const r = await q.twinModelEntities(INSTANCE, 'equipment', ctx, 'weld')
|
|
117
|
+
assert.equal(r.basis, 'rows-scanned', '아직 DB 로 내리지 않았다는 사실이 응답에 있다')
|
|
118
|
+
assert.equal(r.total, 2)
|
|
119
|
+
assert.equal(r.truncated, false, '상한에 닿지 않았으면 잘리지 않았다고 말한다')
|
|
120
|
+
assert.deepEqual(r.items.map((i: any) => i.id).sort(), ['welder-1', 'welder-2'])
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('모르는 축은 거절한다 — 빈 목록은 "없다" 로 읽힌다', async () => {
|
|
124
|
+
await assert.rejects(() => q.twinModelEntities(INSTANCE, 'nosuch', ctx), /unknown concept axis/)
|
|
125
|
+
await assert.rejects(() => q.twinModelItem(INSTANCE, 'nosuch', 'x', ctx), /unknown concept axis/)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('항목 하나 — 정체는 카탈로그에서, 선언은 원본에서, 출처가 함께 온다', async () => {
|
|
129
|
+
const it = await q.twinModelItem(INSTANCE, 'equipment', 'cutter-1', ctx)
|
|
130
|
+
assert.ok(it, '있는 항목은 열려야 한다')
|
|
131
|
+
assert.equal(it.identity.typeRole, 'equipment', '표준 역할은 커널 카탈로그가 말한다')
|
|
132
|
+
assert.equal(it.declared.kind, 'cutter')
|
|
133
|
+
assert.equal(it.declared.mtbfMs, 3600000)
|
|
134
|
+
assert.equal(it.provenance.basis, 'rows')
|
|
135
|
+
assert.equal(it.provenance.sourceRef, INSTANCE, '어느 원본에서 왔는지가 항목에 붙어 있다')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('비교하지 못했으면 관측은 null 이고, 답했으면 **근거를 알린다**', async () => {
|
|
139
|
+
const it = await q.twinModelItem(INSTANCE, 'equipment', 'cutter-1', ctx)
|
|
140
|
+
/*
|
|
141
|
+
* 이 픽스처에는 저널이 없다(행만 있다) — 되짚을 것이 없으므로 관측은 없다. **0 이나 빈 객체로
|
|
142
|
+
* 메우면 "관측했는데 아무것도 없었다" 로 읽힌다.**
|
|
143
|
+
*
|
|
144
|
+
* 예전 이 검사는 "트윈이 안 돌면 관측은 null" 이었다. 지금은 멈춘 트윈도 저널을 접어 답할 수 있고,
|
|
145
|
+
* 그 답이 **자기 근거를 달고 온다**(`observedBasis`). 그래서 규율을 그 자리에 맞춰 다시 적는다:
|
|
146
|
+
* 답이 없으면 null, 답이 있으면 그것이 라이브인지 되짚은 것인지 말한다. 근거 없는 답은 없다.
|
|
147
|
+
*/
|
|
148
|
+
assert.equal(it.observed, null, '되짚을 저널이 없으면 관측은 없다')
|
|
149
|
+
assert.equal(it.observedBasis, undefined, '답이 없으면 근거도 말하지 않는다(빈 답에 근거를 붙이면 견준 것처럼 보인다)')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('이어진 것은 양방향이고 선언된 관계만 따른다', async () => {
|
|
153
|
+
const loc = await q.twinModelItem(INSTANCE, 'locations', 'weld-station', ctx)
|
|
154
|
+
|
|
155
|
+
/* 들어오는 쪽 — 이 자리를 기준으로 삼는 설비 둘. 선언(`equipment.homeLocation`)이 알려 준다. */
|
|
156
|
+
const home = loc.links.incoming.find((l: any) => l.via === 'twin.rel.home' && l.from === 'equipment')
|
|
157
|
+
assert.ok(home, '나를 가리키는 관계가 보여야 한 걸음씩 걸을 수 있다')
|
|
158
|
+
assert.deepEqual(home.ids.sort(), ['welder-1', 'welder-2'])
|
|
159
|
+
assert.equal(home.total, 2)
|
|
160
|
+
|
|
161
|
+
/* 나가는 쪽 — 자리는 구역을 가리킨다. 대상이 **축 밖**(공간의 구역)이라는 사실을 그대로 낸다. */
|
|
162
|
+
const area = loc.links.out.find((l: any) => l.via === 'twin.rel.area')
|
|
163
|
+
assert.ok(area, '커널이 "밖" 이라 부른 참조도 이어진 것으로 낸다')
|
|
164
|
+
assert.equal(area.target.kind, 'external', '갈 곳이 다르므로 화면이 링크를 만들지 말지 알 수 있어야 한다')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
test('문서에만 사는 항목의 관계도 선언대로 이어진다', async () => {
|
|
168
|
+
const recipe = await q.twinModelItem(INSTANCE, 'recipes', 'frame-recipe', ctx)
|
|
169
|
+
assert.equal(recipe.provenance.basis, 'document')
|
|
170
|
+
|
|
171
|
+
const route = recipe.links.out.find((l: any) => l.via === 'twin.rel.route')
|
|
172
|
+
assert.deepEqual(route.ids, ['frame-route'])
|
|
173
|
+
|
|
174
|
+
/* `inputs[].material` — 배열을 지나는 경로가 선언의 문법이고, 그것을 따르는 것이 선언을 존중하는 것이다. */
|
|
175
|
+
const input = recipe.links.out.find((l: any) => l.via === 'twin.rel.input')
|
|
176
|
+
assert.deepEqual(input.ids, ['steel'])
|
|
177
|
+
const output = recipe.links.out.find((l: any) => l.via === 'twin.rel.output')
|
|
178
|
+
assert.deepEqual(output.ids, ['frame'])
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('없는 항목은 null — 빈 껍데기를 내주지 않는다', async () => {
|
|
182
|
+
assert.equal(await q.twinModelItem(INSTANCE, 'equipment', 'no-such-machine', ctx), null)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('내부 배관은 새어 나가지 않는다 — 사용자가 볼 것은 현장의 사실이지 우리 스키마가 아니다', async () => {
|
|
186
|
+
/*
|
|
187
|
+
* 실제로 새었다: `domainId`·`instanceRowId` 같은 내부 UUID 가 속성 패널까지 갔다.
|
|
188
|
+
* 손으로 제외 목록을 두면 컬럼이 늘 때마다 잊는다 — 관계와 조인 컬럼은 엔티티 메타데이터가 안다.
|
|
189
|
+
*/
|
|
190
|
+
const INTERNAL = /^(id|domainId?|instanceRowId|areaRowId|homeLocationRowId|createdAt|updatedAt|creator|updater|creatorId|updaterId)$/
|
|
191
|
+
|
|
192
|
+
const list = await q.twinModelEntities(INSTANCE, 'locations', ctx)
|
|
193
|
+
for (const it of list.items)
|
|
194
|
+
for (const k of Object.keys(it.fields))
|
|
195
|
+
assert.ok(!INTERNAL.test(k), `목록에 내부 컬럼이 새었다: ${k}`)
|
|
196
|
+
|
|
197
|
+
const item = await q.twinModelItem(INSTANCE, 'locations', 'weld-station', ctx)
|
|
198
|
+
for (const k of Object.keys(item.declared))
|
|
199
|
+
assert.ok(!INTERNAL.test(k), `속성 패널에 내부 컬럼이 새었다: ${k}`)
|
|
200
|
+
|
|
201
|
+
/* 식별자도 두 번 보이지 않는다 — 이미 itemId 로 나간다. */
|
|
202
|
+
assert.ok(!('locationId' in item.declared), '항목 자신의 id 를 속성으로 또 내지 않는다')
|
|
203
|
+
/* 출처는 속성이 아니라 근거다. */
|
|
204
|
+
assert.ok(!('sourceRef' in item.declared) && !('ingestedAt' in item.declared))
|
|
205
|
+
assert.equal(item.provenance.sourceRef, INSTANCE)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test('지도의 선이 진단이 된다 — 이어진 것뿐 아니라 **끊긴 것**을 센다', async () => {
|
|
209
|
+
/*
|
|
210
|
+
* 선이 "선언되었고 양끝이 존재한다" 만 말하면 지도는 그림일 뿐이다. 관계에서 가장 중요한 정보는
|
|
211
|
+
* 대개 부재다 — 기준 자리가 비어 있는 설비, 없는 자리를 가리키는 설비.
|
|
212
|
+
*/
|
|
213
|
+
const bad = master()
|
|
214
|
+
bad.equipment.push({ id: 'ghost-1', kind: 'welder', homeLocationId: 'no-such-station' }) // 끊어진 참조
|
|
215
|
+
bad.equipment.push({ id: 'floater-1', kind: 'welder' }) // 기준 자리 없음
|
|
216
|
+
await TwinEngine.ingestMaster(domainId, bad)
|
|
217
|
+
|
|
218
|
+
const sum = await new TwinModelQuery().twinModelSummary(INSTANCE, ctx)
|
|
219
|
+
const home = sum.edges.find((e: any) => e.from === 'equipment' && e.to === 'locations' && e.via === 'twin.rel.home')
|
|
220
|
+
assert.ok(home, '설비→자리 선이 있어야 한다')
|
|
221
|
+
assert.equal(home.linked, 3, '제대로 이어진 설비 셋')
|
|
222
|
+
assert.equal(home.dangling, 1, '없는 자리를 가리키는 설비 하나 — 가장 나쁜 부류이므로 따로 센다')
|
|
223
|
+
assert.equal(home.missing, 1, '기준 자리를 말하지 않은 설비 하나')
|
|
224
|
+
|
|
225
|
+
/* 선택 관계인지도 함께 온다 — `missing` 이 결손인지 정상인지는 그것으로 갈린다. */
|
|
226
|
+
assert.equal(home.optional, true)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
/*
|
|
230
|
+
* ★ 관측 열이 **자기 근거를 말한다** — 라이브인지 되짚은 것인지.
|
|
231
|
+
*
|
|
232
|
+
* 둘은 같은 모양의 값이라, 근거가 없으면 되짚은 값을 "지금 이렇다" 로 읽는다. 그래서 근거를 실을
|
|
233
|
+
* 자리가 없던 동안에는 멈춘 트윈의 저널을 아예 접지 않았다(관측 열이 통째로 `—`). 자리를 만든 뒤의
|
|
234
|
+
* 규율은 하나다: **답하면 근거를 말한다.**
|
|
235
|
+
*/
|
|
236
|
+
test('멈춘 트윈도 저널을 접어 답하고, 그 답은 다시 계산한 값이라고 알린다', async () => {
|
|
237
|
+
const { WmsKernel, EventJournal } = req('@operato/twin-kernel')
|
|
238
|
+
const model = {
|
|
239
|
+
spaceId: 'journal-space',
|
|
240
|
+
utcOffsetMinutes: 0,
|
|
241
|
+
locations: [
|
|
242
|
+
{ id: 'dock-recv', type: 'dock', capacity: 100 },
|
|
243
|
+
{ id: 'bin-1', type: 'storage', capacity: 20 }
|
|
244
|
+
],
|
|
245
|
+
equipment: [{ id: 'fk-1', kind: 'forklift', homeLocation: 'dock-recv' }]
|
|
246
|
+
}
|
|
247
|
+
/* 커널을 실제로 굴려 봉투를 얻는다 — 손으로 지어낸 모양은 검증이 아니다. */
|
|
248
|
+
const journal = new EventJournal()
|
|
249
|
+
const k = new WmsKernel('journal-probe')
|
|
250
|
+
k.onEvent((e: any) => journal.append(e))
|
|
251
|
+
k.loadTwinModel(model)
|
|
252
|
+
k.scenario.load({
|
|
253
|
+
seed: 7,
|
|
254
|
+
generators: [
|
|
255
|
+
{
|
|
256
|
+
kind: 'inbound-arrival',
|
|
257
|
+
rate: { distribution: 'constant', meanPerHour: 240 },
|
|
258
|
+
content: { skuMix: [{ gtin: 'urn:epc:idpat:sgtin:0614141.012345.*', weight: 1 }], qtyPerLine: { min: 1, max: 1 } }
|
|
259
|
+
}
|
|
260
|
+
]
|
|
261
|
+
})
|
|
262
|
+
k.scenario.start()
|
|
263
|
+
for (let i = 0; i < 200; i++) k.tick(1000)
|
|
264
|
+
|
|
265
|
+
const INST = 'journal-probe'
|
|
266
|
+
await TwinEngine.provision(domainId, INST, 'wms', model)
|
|
267
|
+
await TwinEngine.persistBatch(domainId, INST, journal.all(), 0)
|
|
268
|
+
await TwinEngine.stop(domainId, INST).catch(() => {})
|
|
269
|
+
delete (TwinEngine as any).recovered[`${domainId}:${INST}`]
|
|
270
|
+
|
|
271
|
+
const it = await q.twinModelItem(INST, 'equipment', 'fk-1', ctx)
|
|
272
|
+
assert.ok(it?.observed, '저널이 있으면 멈춘 트윈도 마지막으로 알던 사실을 답할 수 있다')
|
|
273
|
+
assert.equal(it.observedBasis, 'journal', '라이브가 아니라 되짚은 것이라고 말해야 "지금 이렇다" 로 읽히지 않는다')
|
|
274
|
+
assert.ok(it.observedRevision > 0, '몇 번째 리비전까지 접었는지 함께 말한다 — 규모의 한계도 그 수가 말한다')
|
|
275
|
+
})
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ 다단 전개(BOM) — **자재가 자재를 품는 트리가 아니다.**
|
|
3
|
+
*
|
|
4
|
+
* 표준(ISA-95)이 택한 모양은 자재와 레시피가 번갈아 나오는 그래프다:
|
|
5
|
+
* 완제품 ←산출— 레시피 —투입→ 부품, 그리고 그 부품이 또 어느 레시피의 산출이면 거기서 다시 내려간다.
|
|
6
|
+
* 우리 선언(`twin.rel.input`·`twin.rel.output`)이 이미 그 두 홉이므로 **새 선언 없이** 재귀가 된다.
|
|
7
|
+
*
|
|
8
|
+
* 여기서 지키는 것:
|
|
9
|
+
* · 패턴을 코드가 알지 않는다 — 선언 그래프에서 자기 축으로 돌아오는 순환을 찾는다
|
|
10
|
+
* · **조용히 고르지 않는다** — 순환이 여럿이면 후보를 내고 멈춘다(방향이 뒤집힌 전개는
|
|
11
|
+
* 틀린 답이 아니라 **반대의 답**이라 화면만 보고는 구별되지 않는다)
|
|
12
|
+
* · 수량은 곱해 내려가되, **모르면 곱하지 않는다**(1 로 메우면 윗단 합계가 통째로 거짓이 된다)
|
|
13
|
+
* · 사이클을 만나면 다시 파지 않고 말한다
|
|
14
|
+
* · 상한에서 끊기면 끊겼다고 말한다
|
|
15
|
+
*/
|
|
16
|
+
import { test, before, after } from 'node:test'
|
|
17
|
+
import assert from 'node:assert/strict'
|
|
18
|
+
import { createRequire } from 'node:module'
|
|
19
|
+
import type { DataSource } from 'typeorm'
|
|
20
|
+
|
|
21
|
+
const req = createRequire(import.meta.url)
|
|
22
|
+
const { NamingStrategy, addDataSource, entities: shellEntities, Domain } = req('@things-factory/shell')
|
|
23
|
+
const { entities: authEntities } = req('@things-factory/auth-base')
|
|
24
|
+
const { entities: twinEntities } = req('../dist-server/service/index.js')
|
|
25
|
+
const { TwinEngine } = req('../dist-server/engine/index.js')
|
|
26
|
+
const { TwinModelTreeQuery } = req('../dist-server/service/twin-model/twin-model-tree-query.js')
|
|
27
|
+
const { DataSource: TypeOrmDataSource } = req('typeorm')
|
|
28
|
+
|
|
29
|
+
let ds: DataSource
|
|
30
|
+
let domainId: string
|
|
31
|
+
let q: any
|
|
32
|
+
let ctx: any
|
|
33
|
+
|
|
34
|
+
const INSTANCE = 'bom-probe'
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 3단 BOM — 완제품 → 하위조립 → 원자재.
|
|
38
|
+
* trailer ← trailer-line ← { frame ×1, axle ×2 }
|
|
39
|
+
* frame ← frame-line ← { steel ×4 }
|
|
40
|
+
*/
|
|
41
|
+
const master = (extra: any = {}): any => ({
|
|
42
|
+
source: INSTANCE,
|
|
43
|
+
system: 'mes',
|
|
44
|
+
siteName: 'BOM 공장',
|
|
45
|
+
space: { width: 60, depth: 30, unit: 'm' },
|
|
46
|
+
locations: [{ id: 'line', type: 'weld-station', role: 'location', capacity: 10 }],
|
|
47
|
+
equipment: [{ id: 'welder-1', kind: 'welder', homeLocationId: 'line' }],
|
|
48
|
+
productionSpec: {
|
|
49
|
+
definition: {
|
|
50
|
+
materials: [{ id: 'trailer' }, { id: 'frame' }, { id: 'axle' }, { id: 'steel' }],
|
|
51
|
+
recipes: [
|
|
52
|
+
{ key: 'trailer-line', label: '트레일러', inputs: [{ material: 'frame', qty: 1 }, { material: 'axle', qty: 2 }], outputs: [{ material: 'trailer', qty: 1 }] },
|
|
53
|
+
{ key: 'frame-line', label: '프레임', inputs: [{ material: 'steel', qty: 4 }], outputs: [{ material: 'frame', qty: 1 }] }
|
|
54
|
+
],
|
|
55
|
+
...extra
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const explode = (itemId: string, opts: any = {}) =>
|
|
61
|
+
q.twinModelTree(INSTANCE, 'materials', itemId, ctx, 'twin.rel.output', 'twin.rel.input', opts.maxDepth, opts.maxItems)
|
|
62
|
+
|
|
63
|
+
before(async () => {
|
|
64
|
+
ds = new TypeOrmDataSource({
|
|
65
|
+
type: 'sqlite',
|
|
66
|
+
database: ':memory:',
|
|
67
|
+
synchronize: true,
|
|
68
|
+
dropSchema: true,
|
|
69
|
+
namingStrategy: new NamingStrategy(),
|
|
70
|
+
entities: [...shellEntities, ...authEntities, ...twinEntities],
|
|
71
|
+
logging: false
|
|
72
|
+
})
|
|
73
|
+
await ds.initialize()
|
|
74
|
+
addDataSource('default', ds)
|
|
75
|
+
const d = await ds.getRepository(Domain).save({ name: 'bom-test', subdomain: 'bom-test' } as any)
|
|
76
|
+
domainId = d.id
|
|
77
|
+
q = new TwinModelTreeQuery()
|
|
78
|
+
ctx = { state: { domain: { id: domainId } } }
|
|
79
|
+
await TwinEngine.ingestMaster(domainId, master())
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
after(async () => {
|
|
83
|
+
await TwinEngine.stop(domainId, INSTANCE).catch(() => {})
|
|
84
|
+
await ds?.destroy()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('다단으로 내려간다 — 자재가 자재를 품지 않아도 전개된다', async () => {
|
|
88
|
+
const t = await explode('trailer')
|
|
89
|
+
const byDepth = (n: number) => t.items.filter((x: any) => x.depth === n).map((x: any) => x.id).sort()
|
|
90
|
+
|
|
91
|
+
assert.deepEqual(byDepth(0), ['trailer'])
|
|
92
|
+
assert.deepEqual(byDepth(1), ['axle', 'frame'], '레시피를 거쳐 1단이 나온다')
|
|
93
|
+
assert.deepEqual(byDepth(2), ['steel'], '프레임이 또 어느 레시피의 산출이므로 거기서 더 내려간다')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('수량을 곱해 내려간다 — 그리고 모르면 곱하지 않는다', async () => {
|
|
97
|
+
const t = await explode('trailer')
|
|
98
|
+
const qty = (id: string) => t.items.find((n: any) => n.id === id)?.qty
|
|
99
|
+
assert.equal(qty('frame'), 1)
|
|
100
|
+
assert.equal(qty('axle'), 2)
|
|
101
|
+
assert.equal(qty('steel'), 4, '프레임 1개당 강재 4 → 트레일러 1대당 4')
|
|
102
|
+
assert.equal(t.qtyIncomplete, false)
|
|
103
|
+
|
|
104
|
+
/* 수량이 빠진 선언이 섞이면 그 아래 합계는 성립하지 않는다 — 1 로 메우지 않고 그렇게 말한다. */
|
|
105
|
+
const noQty = master({
|
|
106
|
+
recipes: [
|
|
107
|
+
{ key: 'trailer-line', label: 't', inputs: [{ material: 'frame' }], outputs: [{ material: 'trailer', qty: 1 }] },
|
|
108
|
+
{ key: 'frame-line', label: 'f', inputs: [{ material: 'steel', qty: 4 }], outputs: [{ material: 'frame', qty: 1 }] }
|
|
109
|
+
]
|
|
110
|
+
})
|
|
111
|
+
await TwinEngine.ingestMaster(domainId, noQty)
|
|
112
|
+
const t2 = await explode('trailer')
|
|
113
|
+
assert.equal(t2.items.find((n: any) => n.id === 'frame').qty, null, '모르는 수량을 1 로 메우지 않는다')
|
|
114
|
+
assert.equal(t2.items.find((n: any) => n.id === 'steel').qty, null, '모르는 값 아래는 전부 모르는 값이다')
|
|
115
|
+
assert.equal(t2.qtyIncomplete, true, '합계가 성립하지 않는다는 사실을 낸다')
|
|
116
|
+
|
|
117
|
+
await TwinEngine.ingestMaster(domainId, master())
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('역전개도 같은 선언으로 — 방향만 뒤집는다', async () => {
|
|
121
|
+
/* "이 강재를 쓰는 것은 무엇인가" — 투입으로 들어와 산출로 나간다. */
|
|
122
|
+
const up = await q.twinModelTree(INSTANCE, 'materials', 'steel', ctx, 'twin.rel.input', 'twin.rel.output')
|
|
123
|
+
assert.deepEqual(up.items.filter((n: any) => n.depth === 1).map((n: any) => n.id), ['frame'])
|
|
124
|
+
assert.deepEqual(up.items.filter((n: any) => n.depth === 2).map((n: any) => n.id), ['trailer'])
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('방향을 안 고르면 아무것도 펼치지 않는다 — 뒤집힌 전개는 틀린 답이 아니라 반대의 답이다', async () => {
|
|
128
|
+
const t = await q.twinModelTree(INSTANCE, 'materials', 'trailer', ctx)
|
|
129
|
+
assert.equal(t.reason, 'ambiguous-pattern')
|
|
130
|
+
assert.equal(t.items.length, 0, '한쪽을 집으면 아무도 모른 채 반대 답을 본다')
|
|
131
|
+
assert.ok(t.patterns.length >= 2, '고를 수 있게 후보를 낸다')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('어떤 순환을 따라 펼쳤는지 밝힌다 — 화면만 보고는 방향을 구별할 수 없다', async () => {
|
|
135
|
+
const t = await explode('trailer')
|
|
136
|
+
assert.equal(t.pattern.inVia, 'twin.rel.output')
|
|
137
|
+
assert.equal(t.pattern.outVia, 'twin.rel.input')
|
|
138
|
+
assert.equal(t.pattern.through, 'recipes')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test('사이클은 다시 탐색하지 않고 알린다', async () => {
|
|
142
|
+
/* 강재가 트레일러에서 나온다고 선언하면(재활용) 그래프가 순환한다. 파고 들면 돌아 나오지 못한다. */
|
|
143
|
+
const loop = master({
|
|
144
|
+
recipes: [
|
|
145
|
+
{ key: 'trailer-line', label: 't', inputs: [{ material: 'frame', qty: 1 }], outputs: [{ material: 'trailer', qty: 1 }] },
|
|
146
|
+
{ key: 'frame-line', label: 'f', inputs: [{ material: 'trailer', qty: 1 }], outputs: [{ material: 'frame', qty: 1 }] }
|
|
147
|
+
]
|
|
148
|
+
})
|
|
149
|
+
await TwinEngine.ingestMaster(domainId, loop)
|
|
150
|
+
const t = await explode('trailer')
|
|
151
|
+
assert.ok(t.items.length < 10, '무한히 자라지 않는다')
|
|
152
|
+
assert.ok(t.cycles.length > 0, '사이클을 만났다는 사실을 말한다')
|
|
153
|
+
await TwinEngine.ingestMaster(domainId, master())
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('상한에서 끊기면 끊겼다고 알린다 — 조용히 자르지 않는다', async () => {
|
|
157
|
+
const shallow = await explode('trailer', { maxDepth: 1 })
|
|
158
|
+
assert.equal(shallow.truncated.depth, true)
|
|
159
|
+
assert.deepEqual(shallow.items.filter((n: any) => n.depth === 2), [], '깊이 상한 아래는 안 판다')
|
|
160
|
+
/* 너비 우선이라 **얕은 층은 온전하다** — 깊이 우선이면 한 가지만 깊게 파고 끊긴다. */
|
|
161
|
+
assert.deepEqual(shallow.items.filter((n: any) => n.depth === 1).map((n: any) => n.id).sort(), ['axle', 'frame'])
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('선언에 없는 자재를 참조하면 그 사실이 드러난다', async () => {
|
|
165
|
+
const ghost = master({
|
|
166
|
+
recipes: [
|
|
167
|
+
{ key: 'trailer-line', label: 't', inputs: [{ material: 'no-such-part', qty: 1 }], outputs: [{ material: 'trailer', qty: 1 }] }
|
|
168
|
+
]
|
|
169
|
+
})
|
|
170
|
+
await TwinEngine.ingestMaster(domainId, ghost)
|
|
171
|
+
const t = await explode('trailer')
|
|
172
|
+
const child = t.items.find((n: any) => n.id === 'no-such-part')
|
|
173
|
+
assert.ok(child, '가리켜진 것은 그리되')
|
|
174
|
+
assert.equal(child.known, false, '선언에 없다는 사실을 함께 낸다(있는 척하지 않는다)')
|
|
175
|
+
await TwinEngine.ingestMaster(domainId, master())
|
|
176
|
+
})
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ 트윈은 자기 원본을 기억한다 — **기억하지 않으면 다시 읽을 수 없다.**
|
|
3
|
+
*
|
|
4
|
+
* 트윈은 원본을 비출 뿐이므로(ADR-0028) 원본이 달라지면 다시 읽어야 한다. 그런데 트윈은
|
|
5
|
+
* "내가 무엇에서 나왔나" 를 남기지 않았다 — 어댑터로 들어온 트윈에는 `TwinReference` 행이
|
|
6
|
+
* 있었지만 템플릿으로 세운 트윈에는 아무 표식도 없었다. 그 트윈은 **첫날의 모습에 영원히
|
|
7
|
+
* 머문다.** 실제로 이 앱의 트윈 둘이 그 상태였다(`twinReferences` 도 비었고 `source` 도 null).
|
|
8
|
+
*
|
|
9
|
+
* 여기서 지키는 것 셋:
|
|
10
|
+
* · 인제스트가 원천을 각인한다 — 원천이 무엇이든 한 자리에
|
|
11
|
+
* · 원천을 모르는 재프로비전이 **이미 아는 것을 지우지 않는다** — 한 번 지우면 다시 못 읽는다
|
|
12
|
+
* · 못 읽는 트윈은 **못 읽는다고 말한다** — 짐작으로 아무 원본이나 집지 않는다
|
|
13
|
+
*/
|
|
14
|
+
import { test, before, after } from 'node:test'
|
|
15
|
+
import assert from 'node:assert/strict'
|
|
16
|
+
import { createRequire } from 'node:module'
|
|
17
|
+
import type { DataSource } from 'typeorm'
|
|
18
|
+
|
|
19
|
+
const req = createRequire(import.meta.url)
|
|
20
|
+
const { NamingStrategy, addDataSource, entities: shellEntities, Domain } = req('@things-factory/shell')
|
|
21
|
+
const { entities: authEntities } = req('@things-factory/auth-base')
|
|
22
|
+
const { entities: twinEntities } = req('../dist-server/service/index.js')
|
|
23
|
+
const { TwinEngine } = req('../dist-server/engine/index.js')
|
|
24
|
+
const { TwinInstance } = req('../dist-server/service/twin-instance/twin-instance.js')
|
|
25
|
+
const { DataSource: TypeOrmDataSource } = req('typeorm')
|
|
26
|
+
|
|
27
|
+
let ds: DataSource
|
|
28
|
+
let domainId: string
|
|
29
|
+
|
|
30
|
+
const BORN = 'origin-probe'
|
|
31
|
+
const ORPHAN = 'orphan-probe'
|
|
32
|
+
|
|
33
|
+
const master = (source: string, bins: number, origin?: any): any => ({
|
|
34
|
+
source,
|
|
35
|
+
system: 'wms',
|
|
36
|
+
siteName: '원천 창고',
|
|
37
|
+
space: { width: 40, depth: 20, unit: 'm' },
|
|
38
|
+
locations: [
|
|
39
|
+
{ id: 'dock-recv', type: 'dock', role: 'location', name: '입고 도크', capacity: 100 },
|
|
40
|
+
...Array.from({ length: bins }, (_, i) => ({ id: `bin-${i + 1}`, type: 'storage', role: 'location', name: `빈${i + 1}`, capacity: 20 }))
|
|
41
|
+
],
|
|
42
|
+
equipment: [{ id: 'fk-1', kind: 'forklift', homeLocationId: 'dock-recv' }],
|
|
43
|
+
...(origin ? { origin } : {})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const originOf = async (instanceId: string) =>
|
|
47
|
+
(await ds.getRepository(TwinInstance).findOne({ where: { instanceId } }))?.origin
|
|
48
|
+
|
|
49
|
+
before(async () => {
|
|
50
|
+
ds = new TypeOrmDataSource({
|
|
51
|
+
type: 'sqlite',
|
|
52
|
+
database: ':memory:',
|
|
53
|
+
synchronize: true,
|
|
54
|
+
dropSchema: true,
|
|
55
|
+
namingStrategy: new NamingStrategy(),
|
|
56
|
+
entities: [...shellEntities, ...authEntities, ...twinEntities],
|
|
57
|
+
logging: false
|
|
58
|
+
})
|
|
59
|
+
await ds.initialize()
|
|
60
|
+
addDataSource('default', ds)
|
|
61
|
+
const d = await ds.getRepository(Domain).save({ name: 'origin-test', subdomain: 'origin-test' } as any)
|
|
62
|
+
domainId = d.id
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
after(async () => {
|
|
66
|
+
for (const id of [BORN, ORPHAN]) await TwinEngine.stop(domainId, id).catch(() => {})
|
|
67
|
+
await ds?.destroy()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('인제스트가 원천을 각인한다 — 원천이 무엇이든 한 자리에', async () => {
|
|
71
|
+
await TwinEngine.ingestMaster(domainId, master(BORN, 3, { kind: 'template', templateId: 'wh-unit-load', params: { bins: 3 } }))
|
|
72
|
+
assert.deepEqual(await originOf(BORN), { kind: 'template', templateId: 'wh-unit-load', params: { bins: 3 } })
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('원천을 모르는 재프로비전이 이미 아는 것을 지우지 않는다 — 한 번 지우면 다시 못 읽는다', async () => {
|
|
76
|
+
/*
|
|
77
|
+
* 이 경로는 실제로 있다 — 원천을 모르는 호출자(스크립트·복구·옛 코드)가 같은 트윈을 다시
|
|
78
|
+
* 인제스트한다. 그때 `origin` 을 덮어 비우면 그 트윈은 **조용히 다시 읽을 수 없게 된다.**
|
|
79
|
+
* 그런 종류의 손상은 다음번에 다시 읽으려 할 때까지 아무도 모른다.
|
|
80
|
+
*/
|
|
81
|
+
await TwinEngine.ingestMaster(domainId, master(BORN, 5))
|
|
82
|
+
assert.deepEqual(
|
|
83
|
+
await originOf(BORN),
|
|
84
|
+
{ kind: 'template', templateId: 'wh-unit-load', params: { bins: 3 } },
|
|
85
|
+
'원천을 말하지 않은 인제스트는 기존 원천을 보존한다'
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const inst = await ds.getRepository(TwinInstance).findOne({ where: { instanceId: BORN } })
|
|
89
|
+
assert.equal(inst.model.locations.length, 6, '구조 자체는 새 마스터로 갱신된다(도크 1 + 빈 5)')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('새 원천을 말하면 갱신된다 — 보존은 침묵에만 적용된다', async () => {
|
|
93
|
+
await TwinEngine.ingestMaster(domainId, master(BORN, 5, { kind: 'reference', source: 'oracle-wms' }))
|
|
94
|
+
assert.deepEqual(await originOf(BORN), { kind: 'reference', source: 'oracle-wms' })
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('원천 없이 태어난 트윈은 원천이 없다 — 짐작해 채우지 않는다', async () => {
|
|
98
|
+
await TwinEngine.ingestMaster(domainId, master(ORPHAN, 2))
|
|
99
|
+
assert.equal(await originOf(ORPHAN), null, '모르는 것을 아는 척하면 엉뚱한 원본으로 현장을 덮는다')
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('조회가 다시 읽을 수 있는지를 먼저 답한다 — 못 하는 것을 못 한다고 알린다', async () => {
|
|
103
|
+
const { TwinModelQuery } = req('../dist-server/service/twin-model/twin-model-query.js')
|
|
104
|
+
const q = new TwinModelQuery()
|
|
105
|
+
const ctx = { state: { domain: { id: domainId } } } as any
|
|
106
|
+
|
|
107
|
+
const born = await q.twinModelSummary(BORN, ctx)
|
|
108
|
+
assert.equal(born.canResync, true)
|
|
109
|
+
assert.deepEqual(born.origin, { kind: 'reference', source: 'oracle-wms' })
|
|
110
|
+
|
|
111
|
+
const orphan = await q.twinModelSummary(ORPHAN, ctx)
|
|
112
|
+
assert.equal(orphan.canResync, false, '버튼을 그대로 내면 누를 때마다 실패한다')
|
|
113
|
+
assert.equal(orphan.origin, null)
|
|
114
|
+
})
|