@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,226 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 구조 투영 — `TwinModelDef`(커널 입력 문서) → **표준 엔티티 행**(ADR-0032).
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 DB 검사인가 ─────────────────────────────────────────────────────────
|
|
5
|
+
* 이 코드의 값어치는 **DB 가 대신 지켜 주는 것**에 있다 — 끊어진 참조를 FK 가 막고, 원본 없는 행을
|
|
6
|
+
* NOT NULL 이 막는다. 순수 함수 검사로는 그게 하나도 확인되지 않는다. 그래서 실제 DataSource 를
|
|
7
|
+
* 세운다(멀티DB 규약상 sqlite 로 도는 것이 5개 드라이버 공통의 최소선이기도 하다).
|
|
8
|
+
*
|
|
9
|
+
* ── 이 검사가 지키는 것 ────────────────────────────────────────────────────
|
|
10
|
+
* · 문서의 세 축이 행으로 선다
|
|
11
|
+
* · 설비→자리가 **진짜 FK** 로 이어진다(문자열 대조가 아니라)
|
|
12
|
+
* · 끊어진 참조는 **이름을 대고 보고**되고, 행 자체는 남는다(막지 않되 침묵하지 않는다)
|
|
13
|
+
* · **전량 교체** — 두 번 돌려도 늘지 않고, 원본에서 사라진 것은 행에서도 사라진다
|
|
14
|
+
* · 원본 표식(`sourceRef`·`ingestedAt`) 없는 행은 만들 수 없다(캐시라는 사실이 구조로 지켜진다)
|
|
15
|
+
* · 소요시간은 ISO 8601 **문자열 그대로** 남는다(분으로 접으면 초가 조용히 사라진다)
|
|
16
|
+
*/
|
|
17
|
+
import { test, before, after, beforeEach } from 'node:test'
|
|
18
|
+
import assert from 'node:assert/strict'
|
|
19
|
+
import { createRequire } from 'node:module'
|
|
20
|
+
import type { DataSource } from 'typeorm'
|
|
21
|
+
|
|
22
|
+
const req = createRequire(import.meta.url)
|
|
23
|
+
const { NamingStrategy, addDataSource, entities: shellEntities, Domain } = req('@things-factory/shell')
|
|
24
|
+
const { entities: authEntities } = req('@things-factory/auth-base')
|
|
25
|
+
const { entities: twinEntities } = req('../dist-server/service/index.js')
|
|
26
|
+
const { projectStructure } = req('../dist-server/service/twin-model/project-structure.js')
|
|
27
|
+
const { TwinInstance } = req('../dist-server/service/twin-instance/twin-instance.js')
|
|
28
|
+
const { TwinSpace } = req('../dist-server/service/twin-space/twin-space.js')
|
|
29
|
+
const { TwinArea } = req('../dist-server/service/twin-space/twin-area.js')
|
|
30
|
+
const { TwinLocation } = req('../dist-server/service/twin-model/twin-location.js')
|
|
31
|
+
const { TwinEquipment } = req('../dist-server/service/twin-model/twin-equipment.js')
|
|
32
|
+
const { TwinOperation } = req('../dist-server/service/twin-model/twin-operation.js')
|
|
33
|
+
const { DataSource: TypeOrmDataSource } = req('typeorm')
|
|
34
|
+
|
|
35
|
+
let ds: DataSource
|
|
36
|
+
let domainId: string
|
|
37
|
+
|
|
38
|
+
const INSTANCE = 'projection-probe'
|
|
39
|
+
const SOURCE = 'projection-probe'
|
|
40
|
+
|
|
41
|
+
/** 실제 트레일러 트윈과 같은 모양의 문서(축소판). */
|
|
42
|
+
const model = () => ({
|
|
43
|
+
locations: [
|
|
44
|
+
{ id: 'cut-station', type: 'cut-station', name: '프레임 절단', capacity: 20, parentId: 'a-floor' },
|
|
45
|
+
{ id: 'weld-station', type: 'weld-station', capacity: 20, parentId: 'a-floor' },
|
|
46
|
+
{ id: 'orphan-bin', type: 'storage', capacity: 5, parentId: 'no-such-area' }
|
|
47
|
+
],
|
|
48
|
+
equipment: [
|
|
49
|
+
{ id: 'cutter-1', kind: 'cutter', name: '절단기 1호', homeLocation: 'cut-station', mtbfMs: 3600000,
|
|
50
|
+
properties: [{ id: 'speed', value: '1.2', dataType: 'xs:double', uom: 'MTS' }] },
|
|
51
|
+
{ id: 'welder-1', kind: 'welder', homeLocation: 'weld-station' },
|
|
52
|
+
{ id: 'ghost-1', kind: 'welder', homeLocation: 'no-such-location' }
|
|
53
|
+
],
|
|
54
|
+
operations: [
|
|
55
|
+
{ key: 'cut', label: 'twin.tpl.x.cut', intent: 'process', locationType: 'cut-station', resourceType: 'cutter',
|
|
56
|
+
duration: 'PT1H10M30S', variability: { distribution: 'triangular', min: 'PT1H', max: 'PT1H30M' },
|
|
57
|
+
parameters: [{ id: 'yield', value: '0.995' }],
|
|
58
|
+
/* 네 자원 명세 — 설비는 `resourceType`, 나머지 셋은 아래. */
|
|
59
|
+
personnelSpecification: [{ personnelClass: 'cutter-op', quantity: 1 }],
|
|
60
|
+
physicalAssetSpecification: [{ assetClass: 'jig', quantity: 1 }],
|
|
61
|
+
materialSpecification: [{ materialDefinition: 'steel', use: 'consumed', quantity: 2 }] }
|
|
62
|
+
]
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
before(async () => {
|
|
66
|
+
ds = new TypeOrmDataSource({
|
|
67
|
+
type: 'sqlite',
|
|
68
|
+
database: ':memory:',
|
|
69
|
+
synchronize: true,
|
|
70
|
+
dropSchema: true,
|
|
71
|
+
namingStrategy: new NamingStrategy(),
|
|
72
|
+
entities: [...shellEntities, ...authEntities, ...twinEntities],
|
|
73
|
+
logging: false
|
|
74
|
+
})
|
|
75
|
+
await ds.initialize()
|
|
76
|
+
addDataSource('default', ds)
|
|
77
|
+
const d = await ds.getRepository(Domain).save({ name: 'projection-test', subdomain: 'projection-test' } as any)
|
|
78
|
+
domainId = d.id
|
|
79
|
+
|
|
80
|
+
await ds.getRepository(TwinInstance).save({
|
|
81
|
+
domain: { id: domainId },
|
|
82
|
+
instanceId: INSTANCE,
|
|
83
|
+
kind: 'mes',
|
|
84
|
+
realityMode: 'sim-experiment',
|
|
85
|
+
purpose: 'operational',
|
|
86
|
+
status: 'stopped',
|
|
87
|
+
model: {}
|
|
88
|
+
} as any)
|
|
89
|
+
|
|
90
|
+
/* 구역은 model 밖(호스트 개념) — 자리가 FK 로 이걸 가리킨다. */
|
|
91
|
+
const space = await ds.getRepository(TwinSpace).save({ domain: { id: domainId }, spaceId: 'sp-1', name: '공장' } as any)
|
|
92
|
+
await ds.getRepository(TwinArea).save({ domain: { id: domainId }, space: { id: space.id }, areaId: 'a-floor', name: '생산 플로어' } as any)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
after(async () => {
|
|
96
|
+
await ds?.destroy()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
beforeEach(async () => {
|
|
100
|
+
await projectStructure(domainId, INSTANCE, model(), SOURCE)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('문서의 세 축이 행으로 선다', async () => {
|
|
104
|
+
assert.equal(await ds.getRepository(TwinLocation).count(), 3)
|
|
105
|
+
assert.equal(await ds.getRepository(TwinEquipment).count(), 3)
|
|
106
|
+
assert.equal(await ds.getRepository(TwinOperation).count(), 1)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('설비→자리가 진짜 FK 로 이어진다 — 문자열 대조가 아니다', async () => {
|
|
110
|
+
const eq = await ds.getRepository(TwinEquipment).findOne({ where: { equipmentId: 'cutter-1' }, relations: ['homeLocation'] })
|
|
111
|
+
assert.equal(eq?.homeLocation?.locationId, 'cut-station', 'FK 로 자리를 따라갈 수 있어야 한다')
|
|
112
|
+
/* 자리 쪽에서도 같은 행이다 — 문자열이었으면 같은 값이지만 같은 행은 아니다. */
|
|
113
|
+
const loc = await ds.getRepository(TwinLocation).findOne({ where: { locationId: 'cut-station' } })
|
|
114
|
+
assert.equal(eq?.homeLocation?.id, loc?.id)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('자리→구역이 FK 로 이어진다 — 커널이 "밖" 이라 부른 참조가 호스트에서는 평범한 FK 다', async () => {
|
|
118
|
+
const loc = await ds.getRepository(TwinLocation).findOne({ where: { locationId: 'cut-station' }, relations: ['area'] })
|
|
119
|
+
assert.equal(loc?.area?.areaId, 'a-floor')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('끊어진 참조는 이름을 대고 보고된다 — 막지 않되 침묵하지 않는다', async () => {
|
|
123
|
+
const report = await projectStructure(domainId, INSTANCE, model(), SOURCE)
|
|
124
|
+
assert.deepEqual(report.unresolvedHome, ['ghost-1→no-such-location'])
|
|
125
|
+
assert.deepEqual(report.unresolvedArea, ['orphan-bin→no-such-area'])
|
|
126
|
+
|
|
127
|
+
/* 행 자체는 남는다 — 참조 하나 때문에 설비가 통째로 사라지면 그게 더 나쁜 거짓말이다. */
|
|
128
|
+
const ghost = await ds.getRepository(TwinEquipment).findOne({ where: { equipmentId: 'ghost-1' }, relations: ['homeLocation'] })
|
|
129
|
+
assert.ok(ghost, '끊어진 참조를 가진 설비도 행으로 남는다')
|
|
130
|
+
assert.equal(ghost?.homeLocation, null, '이을 수 없는 참조는 비운다(엉뚱한 자리에 붙이지 않는다)')
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('전량 교체 — 두 번 돌려도 늘지 않는다', async () => {
|
|
134
|
+
await projectStructure(domainId, INSTANCE, model(), SOURCE)
|
|
135
|
+
await projectStructure(domainId, INSTANCE, model(), SOURCE)
|
|
136
|
+
assert.equal(await ds.getRepository(TwinLocation).count(), 3)
|
|
137
|
+
assert.equal(await ds.getRepository(TwinEquipment).count(), 3)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('원본에서 사라진 것은 행에서도 사라진다 — 캐시는 원본을 따라간다', async () => {
|
|
141
|
+
const shrunk = model()
|
|
142
|
+
shrunk.locations = shrunk.locations.filter(l => l.id !== 'weld-station')
|
|
143
|
+
shrunk.equipment = shrunk.equipment.filter(m => m.id !== 'welder-1')
|
|
144
|
+
await projectStructure(domainId, INSTANCE, shrunk, SOURCE)
|
|
145
|
+
assert.equal(await ds.getRepository(TwinLocation).count(), 2)
|
|
146
|
+
assert.equal(await ds.getRepository(TwinEquipment).count(), 2)
|
|
147
|
+
assert.equal(await ds.getRepository(TwinLocation).countBy({ locationId: 'weld-station' } as any), 0)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('원본 표식 없는 행은 만들 수 없다 — 캐시라는 사실이 구조로 지켜진다', async () => {
|
|
151
|
+
await assert.rejects(
|
|
152
|
+
() =>
|
|
153
|
+
ds.getRepository(TwinLocation).save({
|
|
154
|
+
domain: { id: domainId },
|
|
155
|
+
locationId: 'no-origin',
|
|
156
|
+
type: 'storage'
|
|
157
|
+
/* sourceRef · ingestedAt 없음 */
|
|
158
|
+
} as any),
|
|
159
|
+
/NOT NULL|constraint/i,
|
|
160
|
+
'출처 없는 행이 들어가면 "원본에서 온 캐시" 라는 규율이 구조로는 안 지켜지는 것이다'
|
|
161
|
+
)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('소요시간은 ISO 8601 문자열 그대로 — 분으로 접으면 초가 조용히 사라진다', async () => {
|
|
165
|
+
const op = await ds.getRepository(TwinOperation).findOne({ where: { operationKey: 'cut' } })
|
|
166
|
+
assert.equal(op?.duration, 'PT1H10M30S')
|
|
167
|
+
/* 변동성·파라미터는 도메인마다 형이 달라 원형 그대로 싣는다. */
|
|
168
|
+
assert.equal(op?.variability?.distribution, 'triangular')
|
|
169
|
+
assert.deepEqual(op?.parameters, [{ id: 'yield', value: '0.995' }])
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('등록되지 않은 트윈에는 투영하지 않는다 — 주인 없는 행을 만들지 않는다', async () => {
|
|
173
|
+
await assert.rejects(() => projectStructure(domainId, 'not-registered', model(), SOURCE), /not registered/)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test('네 자원 명세가 함께 옮겨진다 — 선언한 것만 제약이 되므로 빠지면 화면이 거짓이 된다', async () => {
|
|
177
|
+
/*
|
|
178
|
+
* ISA-95 는 공정이 무엇을 필요로 하는지를 넷으로 말한다: 설비·인원·자산·자재. 셋이 투영에서
|
|
179
|
+
* 떨어져 있어서, 조회 화면이 "원본이 말한 값" 이라 적고도 넷 중 하나만 보였다 — 사람이 필요한
|
|
180
|
+
* 공정을 보고 **사람이 필요 없다고 결론 내게** 되어 있었다.
|
|
181
|
+
*/
|
|
182
|
+
const op = await ds.getRepository(TwinOperation).findOne({ where: { operationKey: 'cut' } })
|
|
183
|
+
assert.deepEqual(op?.personnelSpecification, [{ personnelClass: 'cutter-op', quantity: 1 }], '인원 명세가 행에 남아야 한다')
|
|
184
|
+
assert.deepEqual(op?.physicalAssetSpecification, [{ assetClass: 'jig', quantity: 1 }], '자산 명세도 남아야 한다')
|
|
185
|
+
assert.deepEqual(op?.materialSpecification, [{ materialDefinition: 'steel', use: 'consumed', quantity: 2 }], '자재 명세도 남아야 한다')
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('이름이 행까지 온다 — 이름이 없으면 화면에 id 만 남는다', async () => {
|
|
189
|
+
/*
|
|
190
|
+
* 투영이 마스터의 `name` 을 버리고 있었다. 행에 이름이 없으면 어떤 화면도 이름을 만들 수 없어서
|
|
191
|
+
* 목록·표·관계 그림이 모두 `cut-station` 을 찍었다 — 표면마다 따로 보이지만 결함은 여기 하나다.
|
|
192
|
+
*/
|
|
193
|
+
const loc = await ds.getRepository(TwinLocation).findOne({ where: { domain: { id: domainId } as any, locationId: 'cut-station' } as any })
|
|
194
|
+
assert.equal(loc!.name, '프레임 절단', '자리의 이름이 행에 실려야 한다')
|
|
195
|
+
|
|
196
|
+
const eq = await ds.getRepository(TwinEquipment).findOne({ where: { domain: { id: domainId } as any, equipmentId: 'cutter-1' } as any })
|
|
197
|
+
assert.equal(eq!.name, '절단기 1호', '설비의 이름이 행에 실려야 한다')
|
|
198
|
+
|
|
199
|
+
/* 공정은 마스터가 `label` 로 말한다 — 행에서는 규약의 이름으로 둔다(어휘를 하나로). */
|
|
200
|
+
const op = await ds.getRepository(TwinOperation).findOne({ where: { domain: { id: domainId } as any, operationKey: 'cut' } as any })
|
|
201
|
+
assert.equal(op!.name, 'twin.tpl.x.cut', '공정의 이름이 행에 실려야 한다')
|
|
202
|
+
|
|
203
|
+
/* **이름 없는 원본은 이름 없이 남는다** — id 를 이름으로 베끼면 "이름이 있다" 는 거짓이 되고,
|
|
204
|
+
화면은 이름 없음을 알아볼 수 없게 된다. */
|
|
205
|
+
const noname = await ds.getRepository(TwinLocation).findOne({ where: { domain: { id: domainId } as any, locationId: 'weld-station' } as any })
|
|
206
|
+
assert.equal(noname!.name ?? null, null, '원본이 이름을 말하지 않았으면 비어 있어야 한다')
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
test('자원 속성이 행까지 온다 — 소비처가 실제로 읽는 사실이다(주행 속도)', async () => {
|
|
210
|
+
/*
|
|
211
|
+
* 모델에는 있는데 행에 자리가 없어 투영이 흘리고 있었다. 그러면 조회 화면은 "원본이 말한 값" 이라
|
|
212
|
+
* 적고도 속성을 못 보인다 — 이름이 같은 식으로 사라졌던 것과 같은 부류다.
|
|
213
|
+
*
|
|
214
|
+
* 단위를 값과 함께 담는지도 본다: 단위 없는 "1.2" 는 초속인지 시속인지 말하지 않는다.
|
|
215
|
+
*/
|
|
216
|
+
const eq = await ds.getRepository(TwinEquipment).findOne({ where: { domain: { id: domainId } as any, equipmentId: 'cutter-1' } as any })
|
|
217
|
+
const props = (eq!.properties ?? []) as any[]
|
|
218
|
+
assert.equal(props.length, 1, '선언한 속성이 행에 실려야 한다')
|
|
219
|
+
assert.equal(props[0].id, 'speed')
|
|
220
|
+
assert.equal(props[0].value, '1.2')
|
|
221
|
+
assert.equal(props[0].uom, 'MTS', '단위가 값과 함께 남아야 한다')
|
|
222
|
+
|
|
223
|
+
/* 선언하지 않은 자원은 **비어 있다** — 빈 배열로 "선언했다" 고 말하지 않는다. */
|
|
224
|
+
const noneProps = await ds.getRepository(TwinEquipment).findOne({ where: { domain: { id: domainId } as any, equipmentId: 'welder-1' } as any })
|
|
225
|
+
assert.equal(noneProps!.properties ?? null, null)
|
|
226
|
+
})
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 선언 적합성(투영 편) — **모델이 말한 사실은 행에 남고, 행에 남은 것은 화면까지 간다.**
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 필요한가 ─────────────────────────────────────────────────────────────
|
|
5
|
+
* 같은 이름의 하네스가 두 벌 있다. 커널 편은 *선언 ⊆ 상태* 를 지키고(kernel/declaration-in-state),
|
|
6
|
+
* 경계 편은 *원본 → 모델* 을 지킨다(declaration-reaches-model). 그런데 사실이 새던 구간은 **셋**이었고,
|
|
7
|
+
* 남은 하나가 이것이다: **모델 → DB 행 → 조회 응답.**
|
|
8
|
+
*
|
|
9
|
+
* 여기서 잃은 것들: 자리·설비의 이름(화면이 `cut-station` 을 찍었다) · GS1 참조 · 자원 속성 ·
|
|
10
|
+
* 시험 명세 참조 · 시험 결과. 전부 **투영 함수의 한 줄이 빠진** 것이었고, 그때는 사람이 하나씩 찾았다.
|
|
11
|
+
*
|
|
12
|
+
* 타입 검사는 못 잡는다: 컬럼이 거의 다 `nullable` 이라 **안 채우는 것이 합법**이다.
|
|
13
|
+
*
|
|
14
|
+
* ── 무엇이 기준인가 ─────────────────────────────────────────────────────────
|
|
15
|
+
* **엔티티의 컬럼**이다(TypeORM 메타데이터에서 실행 중에 읽는다 — 손으로 적지 않는다). 컬럼이 있다는
|
|
16
|
+
* 것은 "이 사실이 여기 앉는다" 는 선언이고, 앉을 자리를 만들어 두고 안 채우면 그 컬럼은 영원히 빈다.
|
|
17
|
+
*
|
|
18
|
+
* 그래서 컬럼마다 셋을 요구한다:
|
|
19
|
+
* ① 픽스처가 모델에 그 값을 싣는다(값을 줄 방법을 모르면 **검사가 실패한다** — 조용히 건너뛰면
|
|
20
|
+
* 그 컬럼은 아무도 안 보는 컬럼이 된다)
|
|
21
|
+
* ② 투영을 지나 행에 남는다
|
|
22
|
+
* ③ 조회 응답에 나온다(화면이 읽는 자리)
|
|
23
|
+
*
|
|
24
|
+
* 기반 컬럼(id·관계·감사·출처)은 모델이 말하는 사실이 아니므로 이유와 함께 뺀다.
|
|
25
|
+
*/
|
|
26
|
+
import { test, before, after } from 'node:test'
|
|
27
|
+
import assert from 'node:assert/strict'
|
|
28
|
+
import { createRequire } from 'node:module'
|
|
29
|
+
import type { DataSource } from 'typeorm'
|
|
30
|
+
|
|
31
|
+
const req = createRequire(import.meta.url)
|
|
32
|
+
const { NamingStrategy, addDataSource, entities: shellEntities, Domain, getRepository } = req('@things-factory/shell')
|
|
33
|
+
const { entities: authEntities } = req('@things-factory/auth-base')
|
|
34
|
+
const { entities: twinEntities } = req('../dist-server/service/index.js')
|
|
35
|
+
const { TwinEngine } = req('../dist-server/engine/index.js')
|
|
36
|
+
const { TwinModelItemQuery } = req('../dist-server/service/twin-model/twin-model-item-query.js')
|
|
37
|
+
const { TwinLocation } = req('../dist-server/service/twin-model/twin-location.js')
|
|
38
|
+
const { TwinEquipment } = req('../dist-server/service/twin-model/twin-equipment.js')
|
|
39
|
+
const { TwinOperation } = req('../dist-server/service/twin-model/twin-operation.js')
|
|
40
|
+
const { DataSource: TypeOrmDataSource } = req('typeorm')
|
|
41
|
+
|
|
42
|
+
let ds: DataSource
|
|
43
|
+
let domainId: string
|
|
44
|
+
let q: any
|
|
45
|
+
let ctx: any
|
|
46
|
+
|
|
47
|
+
const INSTANCE = 'projection-probe'
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 모델이 말하는 사실이 **아닌** 컬럼 — 이유와 함께.
|
|
51
|
+
*
|
|
52
|
+
* 비어 있으면 안 된다: 여기 적지 않으면 검사가 그 컬럼에 값을 요구하고, 그것은 틀린 요구다.
|
|
53
|
+
* 반대로 여기에 슬쩍 올리면 감시가 사라지므로 **이유를 함께 적는다**(리뷰가 볼 자리다).
|
|
54
|
+
*/
|
|
55
|
+
const NOT_FROM_MODEL: Record<string, string> = {
|
|
56
|
+
id: '행의 열쇠 — 모델이 정하지 않는다(DB 가 발급한다)',
|
|
57
|
+
locationId: '개체 식별자 — 대조의 열쇠라 값 비교에서 뺀다(존재는 조회로 이미 확인한다)',
|
|
58
|
+
equipmentId: '같음',
|
|
59
|
+
operationKey: '같음',
|
|
60
|
+
sourceRef: '어디서 왔나 — 투영이 남기는 근거다(모델의 사실이 아니라 옮긴 행위의 기록)',
|
|
61
|
+
ingestedAt: '언제 옮겼나 — 같은 이유',
|
|
62
|
+
seq: '문서의 차례 — 투영이 넣는 내부 컬럼이다(원본이 말한 값이 아니다)'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 컬럼별 값 — 이름으로 준다. 여기 없는 컬럼이 검사 대상에 들면 하네스가 실패한다. */
|
|
66
|
+
const VALUE: Record<string, unknown> = {
|
|
67
|
+
name: '절단 스테이션',
|
|
68
|
+
description: '강판을 자르는 자리',
|
|
69
|
+
type: 'cut-station',
|
|
70
|
+
kind: 'cutter',
|
|
71
|
+
capacity: 40,
|
|
72
|
+
parallelism: 2,
|
|
73
|
+
level: 'WorkCenter',
|
|
74
|
+
gs1Id: 'urn:epc:id:sgln:0614141.00001.0',
|
|
75
|
+
homeLocation: 'cut-station',
|
|
76
|
+
mtbfMs: 600000,
|
|
77
|
+
mttrMs: 60000,
|
|
78
|
+
properties: [{ id: 'speed', value: '1.2', dataType: 'xs:double', uom: 'MTS' }],
|
|
79
|
+
testSpecificationIds: ['cut-cert'],
|
|
80
|
+
testResults: [{ specId: 'cut-cert', result: 'pass', expiresAt: '2099-01-01T00:00:00.000Z' }],
|
|
81
|
+
intent: 'process',
|
|
82
|
+
locationType: 'cut-station',
|
|
83
|
+
resourceType: 'cutter',
|
|
84
|
+
duration: 'PT25M',
|
|
85
|
+
variability: { distribution: 'triangular', min: 'PT20M', max: 'PT35M', mode: 'PT24M' },
|
|
86
|
+
parameters: [{ id: 'yield', value: '0.98' }],
|
|
87
|
+
personnelSpecification: [{ personnelClass: 'cutter-op', quantity: 1 }],
|
|
88
|
+
equipmentSpecification: [{ equipmentClass: 'cutter', quantity: 1 }],
|
|
89
|
+
physicalAssetSpecification: [{ assetClass: 'euro-pallet', quantity: 1 }],
|
|
90
|
+
materialSpecification: [{ material: 'steel', quantity: 1 }],
|
|
91
|
+
label: '프레임 절단'
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface Case {
|
|
95
|
+
axis: 'locations' | 'equipment' | 'operations'
|
|
96
|
+
entity: any
|
|
97
|
+
/** 행에서 그 개체를 찾는 컬럼 — 조회가 쓰는 것과 같은 이름이어야 한다(§ROW_BACKED). */
|
|
98
|
+
idColumn: string
|
|
99
|
+
id: string
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const CASES: Case[] = [
|
|
103
|
+
{ axis: 'locations', entity: TwinLocation, idColumn: 'locationId', id: 'cut-station' },
|
|
104
|
+
{ axis: 'equipment', entity: TwinEquipment, idColumn: 'equipmentId', id: 'cutter-1' },
|
|
105
|
+
{ axis: 'operations', entity: TwinOperation, idColumn: 'operationKey', id: 'cut' }
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 그 엔티티에서 **모델이 채워야 하는 컬럼** — TypeORM 메타데이터에서 읽는다(손으로 적지 않는다).
|
|
110
|
+
* 관계·관계 id·감사 컬럼은 기반이므로 뺀다(그 판정은 조회가 쓰는 규칙과 같은 근거를 쓴다).
|
|
111
|
+
*/
|
|
112
|
+
function watchedColumns(entity: any): string[] {
|
|
113
|
+
const meta = getRepository(entity).metadata
|
|
114
|
+
const infra = new Set<string>(['createdAt', 'updatedAt', 'creator', 'updater', 'creatorId', 'updaterId'])
|
|
115
|
+
for (const rel of meta.relations) {
|
|
116
|
+
infra.add(rel.propertyName)
|
|
117
|
+
for (const jc of rel.joinColumns) infra.add(jc.propertyName)
|
|
118
|
+
}
|
|
119
|
+
for (const rid of meta.relationIds) infra.add(rid.propertyName)
|
|
120
|
+
return meta.columns
|
|
121
|
+
.map((c: any) => c.propertyName as string)
|
|
122
|
+
.filter((n: string) => !infra.has(n) && !(n in NOT_FROM_MODEL))
|
|
123
|
+
.sort()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** 모든 감시 컬럼에 값을 실은 마스터. */
|
|
127
|
+
function master(): any {
|
|
128
|
+
const fill = (c: Case): Record<string, unknown> => {
|
|
129
|
+
const o: Record<string, unknown> = {}
|
|
130
|
+
for (const col of watchedColumns(c.entity)) {
|
|
131
|
+
assert.ok(
|
|
132
|
+
col in VALUE,
|
|
133
|
+
`픽스처가 "${c.axis}.${col}" 에 실을 값을 모른다 — VALUE 에 하나 적어라(모델의 사실이 아니면 NOT_FROM_MODEL 에 이유와 함께).\n` +
|
|
134
|
+
'조용히 건너뛰면 그 컬럼은 아무도 안 보는 컬럼이 되고, 그것이 이 하네스가 막으려는 실패다.'
|
|
135
|
+
)
|
|
136
|
+
o[col] = VALUE[col]
|
|
137
|
+
}
|
|
138
|
+
return o
|
|
139
|
+
}
|
|
140
|
+
const loc = fill(CASES[0])
|
|
141
|
+
const eq = fill(CASES[1])
|
|
142
|
+
const op = fill(CASES[2])
|
|
143
|
+
return {
|
|
144
|
+
source: INSTANCE,
|
|
145
|
+
system: 'mes',
|
|
146
|
+
siteName: '투영 공장',
|
|
147
|
+
space: { spaceId: 'projection-site', width: 60, depth: 40, unit: 'm' },
|
|
148
|
+
locations: [
|
|
149
|
+
{ id: CASES[0].id, role: 'location', ...loc },
|
|
150
|
+
{ id: 'fg-store', type: 'fg-store', role: 'location', name: '완제품', capacity: 100 }
|
|
151
|
+
],
|
|
152
|
+
/* 경계에서 이름이 바뀌는 것은 하나다(`homeLocationId → homeLocation`) — 경계 편 하네스가 지킨다. */
|
|
153
|
+
equipment: [{ id: CASES[1].id, ...eq, homeLocationId: CASES[0].id }],
|
|
154
|
+
testSpecifications: [{ id: 'cut-cert', name: '절단 자격' }],
|
|
155
|
+
personnelClasses: [{ id: 'cutter-op', description: 'twin.tpl.test.class.cutter-op' }],
|
|
156
|
+
assetClasses: [{ id: 'euro-pallet' }],
|
|
157
|
+
operations: [{ key: CASES[2].id, ...op }],
|
|
158
|
+
productionSpec: {
|
|
159
|
+
companyPrefix: '0614141',
|
|
160
|
+
binding: { steel: '077101' },
|
|
161
|
+
definition: {
|
|
162
|
+
materials: [{ id: 'steel' }],
|
|
163
|
+
routes: [{ key: 'r', steps: [CASES[2].id] }],
|
|
164
|
+
recipes: [{ key: 'rc', label: '프레임', route: 'r', inputs: [{ material: 'steel', qty: 1 }], outputs: [{ material: 'steel', qty: 1 }] }]
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
before(async () => {
|
|
171
|
+
ds = new TypeOrmDataSource({
|
|
172
|
+
type: 'sqlite',
|
|
173
|
+
database: ':memory:',
|
|
174
|
+
synchronize: true,
|
|
175
|
+
dropSchema: true,
|
|
176
|
+
namingStrategy: new NamingStrategy(),
|
|
177
|
+
entities: [...shellEntities, ...authEntities, ...twinEntities],
|
|
178
|
+
logging: false
|
|
179
|
+
})
|
|
180
|
+
await ds.initialize()
|
|
181
|
+
addDataSource('default', ds)
|
|
182
|
+
const d = await ds.getRepository(Domain).save({ name: 'projection-test', subdomain: 'projection-test' } as any)
|
|
183
|
+
domainId = d.id
|
|
184
|
+
q = new TwinModelItemQuery()
|
|
185
|
+
ctx = { state: { domain: { id: domainId } } }
|
|
186
|
+
|
|
187
|
+
await TwinEngine.ingestMaster(domainId, master())
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
after(async () => {
|
|
191
|
+
await TwinEngine.stop(domainId, INSTANCE).catch(() => {})
|
|
192
|
+
await ds?.destroy()
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
test('감시할 컬럼이 비어 있지 않다 — 비면 이 하네스는 아무것도 지키지 않는다', () => {
|
|
196
|
+
for (const c of CASES) {
|
|
197
|
+
const cols = watchedColumns(c.entity)
|
|
198
|
+
assert.ok(cols.length > 2, `${c.axis}: 감시 컬럼을 못 읽었다(${cols.length}개) — 메타데이터 읽기가 낡았다`)
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
for (const c of CASES) {
|
|
203
|
+
test(`${c.axis}: 모델이 말한 사실이 행에 남는다 — 투영이 버리지 않는다`, async () => {
|
|
204
|
+
const row: any = await getRepository(c.entity).findOne({
|
|
205
|
+
where: { domain: { id: domainId } as any, [c.idColumn]: c.id } as any
|
|
206
|
+
})
|
|
207
|
+
assert.ok(row, `${c.axis} "${c.id}" 행이 없다 — 사실 하나가 아니라 개체가 사라졌다`)
|
|
208
|
+
|
|
209
|
+
const lost = watchedColumns(c.entity).filter(col => row[col] === null || row[col] === undefined)
|
|
210
|
+
assert.deepEqual(
|
|
211
|
+
lost,
|
|
212
|
+
[],
|
|
213
|
+
`${c.axis}: 모델은 말했는데 행에 없는 사실 [${lost}] — 투영이 버렸다.\n` +
|
|
214
|
+
'컬럼이 있다는 것은 "이 사실이 여기 앉는다" 는 선언이다 — 앉을 자리를 두고 안 채우면 영원히 빈다.'
|
|
215
|
+
)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
test(`${c.axis}: 행에 남은 사실이 화면까지 간다 — 조회가 감추지 않는다`, async () => {
|
|
219
|
+
const it = await q.twinModelItem(INSTANCE, c.axis, c.id, ctx)
|
|
220
|
+
assert.ok(it?.declared, `${c.axis} "${c.id}" 항목이 열리지 않았다`)
|
|
221
|
+
assert.equal(it.provenance?.basis, 'rows', '행에서 답해야 이 검사가 그 구간을 지킨다')
|
|
222
|
+
|
|
223
|
+
const hidden = watchedColumns(c.entity).filter(col => it.declared[col] === undefined)
|
|
224
|
+
assert.deepEqual(
|
|
225
|
+
hidden,
|
|
226
|
+
[],
|
|
227
|
+
`${c.axis}: 행에는 있는데 조회 응답에 없는 사실 [${hidden}] — 화면은 그것을 물을 방법이 없다.`
|
|
228
|
+
)
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/*
|
|
233
|
+
* ★ 어떤 속성이 **효과가 있나** — 열린 것과 닫힌 것을 화면이 구별할 수 있게.
|
|
234
|
+
*
|
|
235
|
+
* 속성은 열려 있다(아무 사실이나 실어 보낼 수 있다). 그런데 값이 계산을 바꾸려면 그 규칙을 코드가
|
|
236
|
+
* 알아야 하고, 그것은 닫혀 있다. 그 구분이 화면에 없으면 사용자는 속성을 넣고 나서 **아무 일도 안
|
|
237
|
+
* 일어나는 이유**를 알 수 없다. 그래서 서버가 효과 등록부를 함께 낸다.
|
|
238
|
+
*
|
|
239
|
+
* 이 검사가 지키는 규율: **읽는 코드가 있는 것만 등록한다.** 지어낸 효과를 올리면 화면이 거짓을 말한다.
|
|
240
|
+
*/
|
|
241
|
+
test('설비 항목이 효과 등록부를 함께 낸다 — 속도는 효과가 있고 나머지는 표시뿐이다', async () => {
|
|
242
|
+
const it = await q.twinModelItem(INSTANCE, 'equipment', 'cutter-1', ctx)
|
|
243
|
+
assert.ok(it?.propertyEffects, '등록부가 없으면 화면은 효과 있는 속성을 구별할 수 없다')
|
|
244
|
+
assert.equal(it.propertyEffects.speed, 'twin.propeffect.speed', '속도는 주행 시간 계산이 읽는다')
|
|
245
|
+
assert.equal('maxLoadKg' in it.propertyEffects, false, '읽는 코드가 없는 속성을 등록하면 화면이 거짓을 말한다')
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
test('등록부의 모든 항목이 읽는 자리를 밝힌다 — 지어낸 효과를 막는다', () => {
|
|
249
|
+
const { PROPERTY_EFFECTS } = req('../dist-server/engine/property-effects.js')
|
|
250
|
+
assert.ok(PROPERTY_EFFECTS.length > 0, '등록부가 비면 아무것도 지키지 않는다')
|
|
251
|
+
for (const e of PROPERTY_EFFECTS) {
|
|
252
|
+
assert.ok(e.id && e.axes?.length, `${e.id}: 어느 축의 속성인지 밝혀야 한다`)
|
|
253
|
+
assert.match(e.effect, /^twin\.propeffect\./, `${e.id}: 효과 문구는 i18n 키다(서버는 사람 말을 만들지 않는다)`)
|
|
254
|
+
assert.ok(e.readBy && e.readBy.length > 10, `${e.id}: 실제로 읽는 자리를 적어야 한다 — 없으면 지어낸 효과다`)
|
|
255
|
+
}
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
test('등록되지 않은 속성은 등록부에 없다 — 화면이 "표시만 됩니다" 를 말할 근거가 된다', () => {
|
|
259
|
+
const { propertyEffectsOf } = req('../dist-server/engine/property-effects.js')
|
|
260
|
+
const eq = propertyEffectsOf('equipment')
|
|
261
|
+
/*
|
|
262
|
+
* 지금 트레일러 템플릿에는 효과 없는 속성이 선언돼 있지 않다(속도 하나뿐이다). 그래서 화면에서
|
|
263
|
+
* "표시만 됩니다" 를 눈으로 볼 수 없다 — 그 갈래를 여기서 지킨다: 등록부에 없으면 화면이 그렇게 말한다.
|
|
264
|
+
*/
|
|
265
|
+
assert.equal(eq.speed, 'twin.propeffect.speed')
|
|
266
|
+
for (const unknown of ['maxLoadKg', 'skill', 'nominalSpeed']) {
|
|
267
|
+
assert.equal(unknown in eq, false, `읽는 코드가 없는 "${unknown}" 이 등록되면 화면이 거짓을 말한다`)
|
|
268
|
+
}
|
|
269
|
+
/* 축이 다르면 효과도 다르다 — 품목 정의의 환산 계수를 설비에 말하지 않는다. */
|
|
270
|
+
assert.equal('perBaseUnit' in eq, false)
|
|
271
|
+
assert.equal(propertyEffectsOf('materialDefinitions').perBaseUnit, 'twin.propeffect.perBaseUnit')
|
|
272
|
+
})
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 속성 효과 등록부 — **지어낸 효과를 등록하지 않고, 있는 효과를 숨기지 않는다.**
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 이 시험이 있나 (2026-08-18) ──────────────────────────────────────────
|
|
5
|
+
* 화면은 속성마다 「무엇을 바꾸나」를 말한다. 등록되지 않은 속성에는 「표시만 됩니다」라고 한다 — 그것이
|
|
6
|
+
* 정직한 기본값이다. 그런데 에너지 값들(계약전력·정격·요금)은 **오래전부터 읽히고 있었는데** 등록부에
|
|
7
|
+
* 없었다: 사용자는 계약전력을 넣고도 「이 값은 아무것도 바꾸지 않는다」는 문장을 읽었다. 조용한 거짓말이다.
|
|
8
|
+
*
|
|
9
|
+
* 반대 방향의 위험도 있다: 효과를 등록해 두고 **읽는 코드가 없으면** 화면이 「쓰입니다」라고 말하는데
|
|
10
|
+
* 아무 일도 일어나지 않는다. 그래서 등록부의 각 줄이 가리키는 자리를 이 시험이 확인한다.
|
|
11
|
+
*/
|
|
12
|
+
import { test } from 'node:test'
|
|
13
|
+
import assert from 'node:assert/strict'
|
|
14
|
+
import { readFileSync } from 'node:fs'
|
|
15
|
+
import { fileURLToPath } from 'node:url'
|
|
16
|
+
import { createRequire } from 'node:module'
|
|
17
|
+
|
|
18
|
+
import { PROPERTY_EFFECTS, propertyEffectsOf, declarationTargetsOf } from '../server/engine/property-effects.ts'
|
|
19
|
+
|
|
20
|
+
const req = createRequire(import.meta.url)
|
|
21
|
+
/*
|
|
22
|
+
* 커널은 **한 벌로 묶인 산출물**을 내보낸다(`dist-cjs/index.cjs`) — 서브패스는 exports 로 막혀 있다.
|
|
23
|
+
* 그래서 모듈마다 읽지 않고 그 묶음 하나를 읽는다(안에 커널의 모든 읽기 코드가 들어 있다).
|
|
24
|
+
*/
|
|
25
|
+
const kernelBundle = readFileSync(req.resolve('@operato/twin-kernel'), 'utf-8')
|
|
26
|
+
const hostSrc = (file: string): string => readFileSync(fileURLToPath(new URL(`../server/${file}`, import.meta.url)), 'utf-8')
|
|
27
|
+
|
|
28
|
+
test('등록부의 모든 줄이 자기 자리를 가리킨다 — 빈 항목이 없다', () => {
|
|
29
|
+
for (const e of PROPERTY_EFFECTS) {
|
|
30
|
+
assert.ok(e.id, '속성 id 가 있어야 한다')
|
|
31
|
+
assert.ok(e.axes.length, `${e.id}: 어느 축의 속성인지 말해야 한다`)
|
|
32
|
+
assert.match(e.effect, /^twin\.propeffect\./, `${e.id}: 효과 문장은 i18n 키다(서버는 사람 말을 만들지 않는다)`)
|
|
33
|
+
assert.ok(e.readBy && e.readBy.length > 10, `${e.id}: 실제로 읽는 자리를 적어야 한다`)
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('등록한 속성 id 를 읽는 코드가 실제로 있다 — 「쓰입니다」가 거짓이 되지 않게', () => {
|
|
38
|
+
/* 읽는 쪽 소스 — 커널(에너지·환산·주행)과 호스트(요금·주행 추정). */
|
|
39
|
+
const corpus = [kernelBundle, hostSrc('engine/kpi-query.ts'), hostSrc('engine/travel-estimator.ts')].join('\n')
|
|
40
|
+
|
|
41
|
+
const missing: string[] = []
|
|
42
|
+
for (const e of PROPERTY_EFFECTS) {
|
|
43
|
+
/* id 그대로 나오거나(문자열 리터럴), 커널 어휘 상수의 마지막 마디로 나온다(EMS_PROPERTY.contractKW). */
|
|
44
|
+
const tail = e.id.includes('.') ? e.id.split('.').pop()! : e.id
|
|
45
|
+
if (!corpus.includes(`'${e.id}'`) && !corpus.includes(`"${e.id}"`) && !new RegExp(`\\b${tail}\\b`).test(corpus)) missing.push(e.id)
|
|
46
|
+
}
|
|
47
|
+
assert.deepEqual(missing, [], `읽는 코드를 찾지 못한 등록 항목:\n${missing.join('\n')}`)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('에너지 값들이 등록돼 있다 — 예전에는 「표시만 됩니다」라고 말했다', () => {
|
|
51
|
+
const ids = PROPERTY_EFFECTS.map(e => e.id)
|
|
52
|
+
for (const id of ['contract.kW', 'power.ratedKW', 'storage.capacityKWh', 'tariff.demandChargePerKW', 'tariff.currency'])
|
|
53
|
+
assert.ok(ids.includes(id), `${id} 가 등록부에 없다`)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('축으로 갈라 낸다 — 그 축의 항목에만 효과를 말한다', () => {
|
|
57
|
+
const locations = propertyEffectsOf('locations')
|
|
58
|
+
const equipment = propertyEffectsOf('equipment')
|
|
59
|
+
assert.ok(locations['contract.kW'], '계약전력은 자리의 속성이다')
|
|
60
|
+
assert.equal(locations['power.ratedKW'], undefined, '설비 속성을 자리에 말하지 않는다')
|
|
61
|
+
assert.ok(equipment['speed'], '속도는 설비의 속성이다')
|
|
62
|
+
assert.equal(equipment['tariff.currency'], undefined, '요금은 자리(수전)의 속성이다')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('요금은 커널이 아니라 호스트가 읽는다 — 그 사실을 readBy 가 말한다', () => {
|
|
66
|
+
const tariff = PROPERTY_EFFECTS.filter(e => e.id.startsWith('tariff.'))
|
|
67
|
+
assert.equal(tariff.length, 3)
|
|
68
|
+
for (const e of tariff) assert.match(e.readBy, /kpi-query/, '금액은 저널을 접는 쪽이 만든다')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
/*
|
|
72
|
+
* ── 어느 대상에 얹히나 (2026-08-18) ────────────────────────────────────────
|
|
73
|
+
* 지게차에 계약전력을 선언할 수 있었다 — 문은 받고 화면은 「넣었습니다」라고 말하는데, 읽는 코드는
|
|
74
|
+
* 자리(수전)만 본다. 그 제한은 이미 등록부의 축이 알고 있으므로, 검사에 그 지식을 쓴다(두 곳에 적지
|
|
75
|
+
* 않는다). 등록부에 없는 속성은 **제한을 모르는 것**이므로 받아 준다 — 속성은 열려 있고 효과만 닫혀 있다.
|
|
76
|
+
*/
|
|
77
|
+
test('축이 곧 얹을 수 있는 대상이다 — 목록을 따로 적지 않는다', () => {
|
|
78
|
+
assert.deepEqual(declarationTargetsOf('contract.kW'), ['location'])
|
|
79
|
+
assert.deepEqual(declarationTargetsOf('speed'), ['equipment'])
|
|
80
|
+
assert.deepEqual(declarationTargetsOf('operation.duration'), ['operation'])
|
|
81
|
+
/* 선언 대상이 아닌 축(자재 정의)은 제한을 말하지 않는다 — 이 문의 대상이 아니다. */
|
|
82
|
+
assert.equal(declarationTargetsOf('perBaseUnit'), undefined)
|
|
83
|
+
assert.equal(declarationTargetsOf('made.up.property'), undefined, '모르는 속성은 제한도 모른다')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('공정 시간이 등록돼 있고, 그 값을 실어 보내는 자리를 가리킨다', () => {
|
|
87
|
+
const e = PROPERTY_EFFECTS.find(x => x.id === 'operation.duration')!
|
|
88
|
+
assert.ok(e, '공정 시간이 등록부에 있어야 한다')
|
|
89
|
+
assert.deepEqual(e.axes, ['operations'])
|
|
90
|
+
assert.match(e.readBy, /declareDurations/, '커널의 어느 창구로 가는지 말한다')
|
|
91
|
+
/* 실어 보내는 코드가 실제로 있다 — 등록만 해 두고 아무도 나르지 않으면 화면이 거짓을 말한다. */
|
|
92
|
+
const layer = hostSrc('engine/local-declarations.ts')
|
|
93
|
+
assert.match(layer, /OPERATION_PROPERTY\.duration/, '선언 겹이 그 속성을 안다')
|
|
94
|
+
assert.match(layer, /isoDurationOf/, '커널이 읽는 표기로 바꾼다')
|
|
95
|
+
assert.match(hostSrc('engine/twin-engine.ts'), /declareDurations\(declared\)/, '기동·구조전환 때 커널에 싣는다')
|
|
96
|
+
})
|