@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,122 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 뮤테이션 게이트 가드 — **트윈을 바꾸는 문은 전부 잠겨 있다.**
|
|
3
|
+
*
|
|
4
|
+
* ── 계기 (2026-08-06) ──────────────────────────────────────────────────────
|
|
5
|
+
* 권한 모델을 손대며 세어 보니 트윈 뮤테이션 24개 중 **22개에 게이트가 없었다.** 인스턴스 삭제와
|
|
6
|
+
* 레퍼런스 자격정보 저장까지 로그인만 하면 누구나 부를 수 있었다. 발견은 사람 눈이었다 — 즉
|
|
7
|
+
* 25번째 뮤테이션이 게이트 없이 들어오면 아무것도 잡지 못한다. 그래서 기계가 센다.
|
|
8
|
+
*
|
|
9
|
+
* ── 왜 문자열을 그대로 비교하나 ─────────────────────────────────────────────
|
|
10
|
+
* "게이트가 있나" 만 보면 `domainOwnerGranted: false` 로 약해지거나 category 가 다른 값으로 갈라져도
|
|
11
|
+
* 통과한다. 게이트는 **한 문장이어야** 서버·클라·관리 화면이 같은 권한을 말한다. 그래서 정본 문장과
|
|
12
|
+
* 글자까지 같은지 본다.
|
|
13
|
+
*
|
|
14
|
+
* ── 조회는 왜 검사하지 않나 (ADR-0027) ──────────────────────────────────────
|
|
15
|
+
* 조회에는 게이트를 두지 않기로 했다 — `twinKpi` 가 같은 판정을 게이트 없이 이미 내주므로, 일부
|
|
16
|
+
* 조회만 잠그면 보안이 아니라 화면 고장이 된다. 이 결정이 바뀌면 아래 "정본 문장" 검사가 먼저
|
|
17
|
+
* 깨진다. 그때는 테스트를 고치지 말고 **ADR 을 먼저 고친다.**
|
|
18
|
+
*/
|
|
19
|
+
import { test } from 'node:test'
|
|
20
|
+
import assert from 'node:assert/strict'
|
|
21
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
|
22
|
+
import { join } from 'node:path'
|
|
23
|
+
|
|
24
|
+
/** 정본 게이트 — 트윈을 **바꾸는** 모든 작업이 요구하는 단 하나의 권한. */
|
|
25
|
+
const GATE =
|
|
26
|
+
`@Directive('@privilege(category: "twin", privilege: "mutation", ` + `domainOwnerGranted: true, superUserGranted: true)')`
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 두 번째 게이트 — **테넌트를 넘어 읽는 운영 계기판** (ADR-0027 예외, 2026-08-07).
|
|
30
|
+
*
|
|
31
|
+
* ADR-0027 의 전제는 "읽기의 경계는 테넌트 소속" 이라 조회에는 게이트를 붙이지 않는다. 그런데 프로세스
|
|
32
|
+
* 부하 계기판(`twinLoad`)은 이벤트 루프가 프로세스 공유이므로 **일부러 전 테넌트를 합산한다** — 소속이
|
|
33
|
+
* 경계가 되지 못한다. 그래서 시스템 소유만 본다. **권한 이름을 만들지 않았다**(category·privilege 없음).
|
|
34
|
+
*
|
|
35
|
+
* 늘리지 말 것. 조건은 ADR-0027 의 예외 절에 있고, 셋 중 하나라도 아니면 조회 무게이트가 그대로다.
|
|
36
|
+
*/
|
|
37
|
+
const OPS_GATE = `@Directive('@privilege(superUserGranted: true)')`
|
|
38
|
+
|
|
39
|
+
const ALLOWED = [GATE, OPS_GATE]
|
|
40
|
+
|
|
41
|
+
const SERVER = new URL('../server', import.meta.url).pathname
|
|
42
|
+
|
|
43
|
+
function walk(dir: string, out: string[] = []): string[] {
|
|
44
|
+
let entries: string[]
|
|
45
|
+
try {
|
|
46
|
+
entries = readdirSync(dir)
|
|
47
|
+
} catch {
|
|
48
|
+
return out
|
|
49
|
+
}
|
|
50
|
+
for (const e of entries) {
|
|
51
|
+
const p = join(dir, e)
|
|
52
|
+
if (statSync(p).isDirectory()) walk(p, out)
|
|
53
|
+
else if (p.endsWith('.ts') && !p.endsWith('.d.ts')) out.push(p)
|
|
54
|
+
}
|
|
55
|
+
return out
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* `@Mutation` 위에 붙은 데코레이터·주석 덩이를 거꾸로 모은다.
|
|
60
|
+
*
|
|
61
|
+
* 데코레이터가 바로 붙어 있다고 가정하지 않는다 — 나중에 다른 데코레이터가 끼어들어도 게이트를
|
|
62
|
+
* 찾아야 한다. 덩이의 끝은 코드(다른 메서드의 `}` 등)를 만나는 자리다.
|
|
63
|
+
*/
|
|
64
|
+
function decoratorBlockAbove(lines: string[], at: number): string[] {
|
|
65
|
+
const block: string[] = []
|
|
66
|
+
for (let i = at - 1; i >= 0; i--) {
|
|
67
|
+
const t = lines[i].trim()
|
|
68
|
+
if (!t) continue /* 주석과 데코레이터 사이의 빈 줄은 덩이를 끊지 않는다 */
|
|
69
|
+
const isBlockPart = t.startsWith('@') || t.startsWith('*') || t.startsWith('/*') || t.startsWith('//')
|
|
70
|
+
if (!isBlockPart) break
|
|
71
|
+
block.push(t)
|
|
72
|
+
}
|
|
73
|
+
return block
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const files = walk(SERVER)
|
|
77
|
+
|
|
78
|
+
test('트윈을 바꾸는 뮤테이션은 전부 정본 게이트를 달고 있다', () => {
|
|
79
|
+
const ungated: string[] = []
|
|
80
|
+
let mutations = 0
|
|
81
|
+
|
|
82
|
+
for (const f of files) {
|
|
83
|
+
const lines = readFileSync(f, 'utf-8').split('\n')
|
|
84
|
+
lines.forEach((line, i) => {
|
|
85
|
+
if (!line.trim().startsWith('@Mutation')) return
|
|
86
|
+
mutations++
|
|
87
|
+
if (!decoratorBlockAbove(lines, i).some(l => l === GATE)) {
|
|
88
|
+
/* 어느 뮤테이션인지 이름으로 말한다 — 줄번호만 주면 고치는 사람이 다시 찾아야 한다. */
|
|
89
|
+
const name = (lines.slice(i, i + 6).join(' ').match(/async\s+(\w+)/) ?? [, '?'])[1]
|
|
90
|
+
ungated.push(`${f.split('/').slice(-2).join('/')}:${i + 1} ${name}`)
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* 훑기가 망가져 0건이 되면 "위반 없음" 으로 보인다 — 그 거짓 통과를 막는다. */
|
|
96
|
+
assert.ok(mutations >= 20, `뮤테이션을 ${mutations}개만 찾았다 — 소스 훑기가 망가졌다`)
|
|
97
|
+
assert.deepEqual(
|
|
98
|
+
ungated,
|
|
99
|
+
[],
|
|
100
|
+
`게이트 없는 뮤테이션 ${ungated.length}건 — 로그인만 하면 누구나 부를 수 있다:\n${ungated.join('\n')}`
|
|
101
|
+
)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('권한 문장이 하나로 통일되어 있다 — 약해지거나 서로 어긋나지 않았다', () => {
|
|
105
|
+
const odd: string[] = []
|
|
106
|
+
for (const f of files) {
|
|
107
|
+
readFileSync(f, 'utf-8')
|
|
108
|
+
.split('\n')
|
|
109
|
+
.forEach((line, i) => {
|
|
110
|
+
const t = line.trim()
|
|
111
|
+
if (!t.startsWith("@Directive('@privilege")) return
|
|
112
|
+
if (!ALLOWED.includes(t)) odd.push(`${f.split('/').slice(-2).join('/')}:${i + 1} ${t}`)
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
assert.deepEqual(
|
|
116
|
+
odd,
|
|
117
|
+
[],
|
|
118
|
+
`정본과 다른 권한 문장 ${odd.length}건. 쓰기는 GATE 하나, 테넌트를 넘어 읽는 운영 계기판만 ` +
|
|
119
|
+
`OPS_GATE. 그 밖으로 늘리는 것이 의도라면 ADR-0027 을 먼저 고치고 이 검사의 정본을 함께 옮긴다:` +
|
|
120
|
+
`\n${odd.join('\n')}`
|
|
121
|
+
)
|
|
122
|
+
})
|
|
@@ -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
|
|
|
@@ -94,9 +94,92 @@ test('★ withLiveOee: projector 미러(oee 없음) + 누적기 → live 설비
|
|
|
94
94
|
|
|
95
95
|
// withLiveOee 로 보강 → buildEntityDeltas payload 에 oee 포함(sim 과 동일 형상 = 보드 OEE 컴포넌트 live 렌더).
|
|
96
96
|
const enriched = withLiveOee(rawLive, acc)
|
|
97
|
-
const fk = buildEntityDeltas(enriched).find(d => d.tag === 'fk-1')!.data as any
|
|
97
|
+
const fk = buildEntityDeltas(enriched, 't1').find(d => d.tag === 'fk-1')!.data as any
|
|
98
98
|
assert.ok(fk.oee, 'withLiveOee 후 설비 data(tag) payload 에 oee 포함 — live OEE 갭 종료')
|
|
99
99
|
// buildEntityDeltas shapeOee = 정수 퍼센트(0-100), raw ms 제외.
|
|
100
100
|
assert.ok(fk.oee.overall >= 0 && fk.oee.overall <= 100, 'oee.overall ∈ [0,100] 정수 퍼센트')
|
|
101
101
|
assert.ok(fk.oee.availability < 100, 'availability < 100 (고장 시간 반영 — 누적기 downMs)')
|
|
102
102
|
})
|
|
103
|
+
|
|
104
|
+
/*
|
|
105
|
+
* ── 되짚은 화면에도 가동 이력이 있어야 한다 (2026-08-05) ────────────────────
|
|
106
|
+
* OEE 는 원 시스템이 누적을 보내 주지 않아 호스트가 상태 전이를 적분해 만든다. 그런데 그 누적기는
|
|
107
|
+
* **라이브에서만** 돌았고, 저널을 접는 경로는 그 계산을 하지 않았다 — 과거를 되짚으면 모든 설비가
|
|
108
|
+
* "가동 이력이 전혀 없음" 으로 보였다(실 저널: `rosarito-mes` 18대, `hatiolab-wms` 22대가 그랬다).
|
|
109
|
+
*
|
|
110
|
+
* 없는 것은 데이터가 아니라 계산이다: 입력은 저널에 다 있다. 그래서 같은 누적기에 같은 이벤트를
|
|
111
|
+
* 태운다 — 이 테스트는 그 재계산이 라이브와 **같은 값**을 내는지, 그리고 시간여행에서 **그 시점까지만**
|
|
112
|
+
* 세는지를 못 박는다.
|
|
113
|
+
*/
|
|
114
|
+
test('★ 저널을 다시 태우면 라이브와 같은 계측이 나온다 — 규칙이 하나이므로', () => {
|
|
115
|
+
const T0 = Date.parse('2026-08-05T00:00:00.000Z')
|
|
116
|
+
const ev = (ms: number, id: string, status: string) => ({
|
|
117
|
+
eventId: `e${ms}`,
|
|
118
|
+
eventType: 'equipment.status',
|
|
119
|
+
eventTime: new Date(T0 + ms).toISOString(),
|
|
120
|
+
tenantId: 't',
|
|
121
|
+
data: { equipmentId: id, moverId: id, status }
|
|
122
|
+
})
|
|
123
|
+
const stream = [ev(0, 'fk-1', 'idle'), ev(10_000, 'fk-1', 'busy'), ev(40_000, 'fk-1', 'down'), ev(70_000, 'fk-1', 'idle')]
|
|
124
|
+
|
|
125
|
+
/* 라이브 — 이벤트가 도착하는 대로 누적. */
|
|
126
|
+
const live = new OeeAccumulator()
|
|
127
|
+
for (const e of stream) live.apply(e as any)
|
|
128
|
+
|
|
129
|
+
/* 되짚기 — 저널에서 같은 순서로 다시 태운다. */
|
|
130
|
+
const replayed = new OeeAccumulator()
|
|
131
|
+
for (const e of stream) replayed.apply(e as any)
|
|
132
|
+
|
|
133
|
+
const at = T0 + 70_000
|
|
134
|
+
assert.deepEqual(replayed.oee('fk-1', at), live.oee('fk-1', at), '같은 이벤트·같은 시점이면 같은 값이어야 한다')
|
|
135
|
+
assert.equal(replayed.oee('fk-1', at).downMs, 30_000, '고장 30초가 잡혀야 한다')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('★ 시간여행은 그 시점까지만 센다 — 뒤에 일어난 고장이 과거 화면에 섞이지 않게', () => {
|
|
139
|
+
const T0 = Date.parse('2026-08-05T00:00:00.000Z')
|
|
140
|
+
const ev = (ms: number, status: string) => ({
|
|
141
|
+
eventId: `e${ms}`,
|
|
142
|
+
eventType: 'equipment.status',
|
|
143
|
+
eventTime: new Date(T0 + ms).toISOString(),
|
|
144
|
+
tenantId: 't',
|
|
145
|
+
data: { equipmentId: 'fk-1', moverId: 'fk-1', status }
|
|
146
|
+
})
|
|
147
|
+
const all = [ev(0, 'busy'), ev(30_000, 'down'), ev(60_000, 'idle')]
|
|
148
|
+
|
|
149
|
+
/* 30초 시점까지만 태운다(호스트가 잘라 낸 이벤트만 태우는 것과 같다). */
|
|
150
|
+
const past = new OeeAccumulator()
|
|
151
|
+
for (const e of all.slice(0, 2)) past.apply(e as any)
|
|
152
|
+
assert.equal(past.oee('fk-1', T0 + 30_000).downMs, 0, '아직 고장이 시작된 시점 — 고장 시간은 0 이다')
|
|
153
|
+
|
|
154
|
+
const now = new OeeAccumulator()
|
|
155
|
+
for (const e of all) now.apply(e as any)
|
|
156
|
+
assert.equal(now.oee('fk-1', T0 + 60_000).downMs, 30_000, '지금 화면에서는 고장 30초가 잡힌다')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('★ 계측 시점을 벽시계로 재지 않는다 — 과거 화면의 가용률이 시간이 갈수록 떨어지지 않게', () => {
|
|
160
|
+
const T0 = Date.parse('2026-08-05T00:00:00.000Z')
|
|
161
|
+
const acc = new OeeAccumulator()
|
|
162
|
+
acc.apply({ eventId: 'e1', eventType: 'equipment.status', eventTime: new Date(T0).toISOString(), tenantId: 't', data: { equipmentId: 'fk-1', moverId: 'fk-1', status: 'busy' } } as any)
|
|
163
|
+
const atThen = acc.oee('fk-1', T0 + 10_000)
|
|
164
|
+
const atMuchLater = acc.oee('fk-1', T0 + 10_000_000)
|
|
165
|
+
assert.notDeepEqual(atThen, atMuchLater, '시점을 넘기면 값이 달라진다 — 그래서 그 화면의 시각을 넘겨야 한다')
|
|
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
|
+
})
|