@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,81 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 커맨드 감사 기록 만들기 (순수) — **누가 무엇을 허락했고, 그래서 어떻게 됐나.**
|
|
3
|
+
*
|
|
4
|
+
* 승인 게이트가 앞을 막고, 이 기록이 뒤를 설명한다. 둘 다 있어야 "사람 손을 거쳤다" 를 증명할 수 있다.
|
|
5
|
+
*
|
|
6
|
+
* 무엇을 믿는가가 이 기록의 전부다:
|
|
7
|
+
* · **신원·시각은 서버가 준다.** 클라이언트가 말한 신원을 그대로 적으면 위조가 자유로워 아무것도
|
|
8
|
+
* 증명하지 못한다. 세션이 아는 사용자와 서버 시각만 쓴다.
|
|
9
|
+
* · **의도는 클라이언트가 준다** — 출처(origin)·근거(reason). 서버가 알 수 없는 정보이고,
|
|
10
|
+
* 위조되더라도 신원 증명을 훼손하지 않는다.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type CommandOrigin = 'user' | 'ai' | 'automation' | 'unknown'
|
|
14
|
+
|
|
15
|
+
export type CommandAuditInput = {
|
|
16
|
+
/** 클라이언트가 실어 보낸 커맨드(신뢰하지 않는 부분을 포함한 원본). */
|
|
17
|
+
command: any
|
|
18
|
+
/** 커널 판정. */
|
|
19
|
+
ack: { accepted?: boolean; errorCode?: string; error?: string }
|
|
20
|
+
/** 세션이 아는 사용자 — **서버만 채운다.** */
|
|
21
|
+
actor?: { id?: string; email?: string; name?: string }
|
|
22
|
+
/** 서버 시각. 호출부가 주입해 테스트가 시간을 고정할 수 있게 한다. */
|
|
23
|
+
at: Date
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** `TwinAuditEvent` 에 그대로 넣을 수 있는 모양(엔티티를 몰라도 되게 순수하게 둔다). */
|
|
27
|
+
export type CommandAuditRow = {
|
|
28
|
+
eventTime: Date
|
|
29
|
+
commandId?: string
|
|
30
|
+
commandType?: string
|
|
31
|
+
origin: CommandOrigin
|
|
32
|
+
reason?: string
|
|
33
|
+
actorId?: string
|
|
34
|
+
actorEmail?: string
|
|
35
|
+
actorName?: string
|
|
36
|
+
accepted: boolean
|
|
37
|
+
errorCode?: string
|
|
38
|
+
args?: any
|
|
39
|
+
argsTruncated: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 신뢰할 수 있는 출처 표기로 정규화 — 모르는 값은 지어내지 않고 'unknown' 으로 둔다. */
|
|
43
|
+
export function normalizeOrigin(v: unknown): CommandOrigin {
|
|
44
|
+
return v === 'user' || v === 'ai' || v === 'automation' ? v : 'unknown'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** args 직렬화 상한(문자). 넘으면 자르되 잘랐다고 표시한다. */
|
|
48
|
+
const ARGS_MAX = 4000
|
|
49
|
+
|
|
50
|
+
export function buildCommandAudit(input: CommandAuditInput): CommandAuditRow {
|
|
51
|
+
const { command, ack, actor, at } = input
|
|
52
|
+
const argsText = safeJson(command?.args)
|
|
53
|
+
const truncated = argsText !== null && argsText.length > ARGS_MAX
|
|
54
|
+
return {
|
|
55
|
+
eventTime: at,
|
|
56
|
+
commandId: str(command?.commandId),
|
|
57
|
+
commandType: str(command?.type),
|
|
58
|
+
origin: normalizeOrigin(command?.origin),
|
|
59
|
+
reason: str(command?.reason),
|
|
60
|
+
/* 신원 — 세션 값만. 커맨드에 실려 온 신원 주장은 쳐다보지 않는다. */
|
|
61
|
+
actorId: str(actor?.id),
|
|
62
|
+
actorEmail: str(actor?.email),
|
|
63
|
+
actorName: str(actor?.name),
|
|
64
|
+
/* 결과 — 승인했는데 커널이 거절한 것도 감사 대상이다(무엇을 시도했는지가 남아야 한다). */
|
|
65
|
+
accepted: !!ack?.accepted,
|
|
66
|
+
errorCode: str(ack?.errorCode),
|
|
67
|
+
/* 인자는 원형 그대로. 너무 크면 잘린 문자열로 두되 잘랐다고 말한다. */
|
|
68
|
+
args: truncated ? argsText!.slice(0, ARGS_MAX) : command?.args ?? undefined,
|
|
69
|
+
argsTruncated: truncated
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const str = (v: unknown): string | undefined => (typeof v === 'string' && v ? v : undefined)
|
|
74
|
+
|
|
75
|
+
function safeJson(v: unknown): string | null {
|
|
76
|
+
try {
|
|
77
|
+
return v === undefined ? null : JSON.stringify(v)
|
|
78
|
+
} catch {
|
|
79
|
+
return null /* 순환 참조 등 — 못 재면 자르지 않는다(원본을 그대로 둔다) */
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { CreateDateColumn, Entity, Index, Column, RelationId, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'
|
|
2
|
+
import { ObjectType, Field, ID } from 'type-graphql'
|
|
3
|
+
|
|
4
|
+
import { Domain, ScalarObject } from '@things-factory/shell'
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* TwinAuditEvent — **사람이 무엇을 허락했는가.**
|
|
8
|
+
*
|
|
9
|
+
* ── 왜 저널(TwinEvent)이 아닌가 ───────────────────────────────────────────────
|
|
10
|
+
* 처음에는 저널에 함께 넣었다. "감사는 원장에서 하니까" 라는 이유였는데, 그건 **화면의 편의**였고
|
|
11
|
+
* 저장 구조를 정할 근거가 못 됐다. 넣자마자 예외가 넷 생겼다 — 리플레이에서 빼고, 백필에서 빼고,
|
|
12
|
+
* 원장 거래 분류에서 빼고, 원장 렌더에서 갈랐다. **예외가 넷이면 설계가 "여기 아니다" 라고 말하는 것이다.**
|
|
13
|
+
*
|
|
14
|
+
* 갈라 두면 예외가 0 이고, 오히려 얻는다:
|
|
15
|
+
* · 행위자가 **컬럼**이 된다 — "누가 승인한 것 전부" 는 감사의 기본 질문인데 payload 안에서는 못 찾았다
|
|
16
|
+
* · revision 이 없다. 저널에서는 그 칸을 비워 "상태가 아님" 을 뜻하게 썼는데, 없음을 종류 표시로
|
|
17
|
+
* 쓰는 것이 여러 번 문제를 냈다(백필의 `bizStep IS NULL = 옛 행` 전제가 이것 때문에 깨졌다)
|
|
18
|
+
* · bizStep 오염이 없다 — 저널의 키 추출기는 값이 없으면 이벤트 타입으로 대체하므로,
|
|
19
|
+
* 감사 행을 태우면 CBV 표준 어휘 컬럼에 `governance.command` 가 박힌다
|
|
20
|
+
*
|
|
21
|
+
* ── 무엇을 믿는가 ────────────────────────────────────────────────────────────
|
|
22
|
+
* **신원·시각은 서버가 각인한다.** 클라이언트가 말한 신원을 그대로 적으면 위조가 자유로워 그 기록은
|
|
23
|
+
* 아무것도 증명하지 못한다. 세션이 아는 사용자와 서버 시각만 신뢰한다.
|
|
24
|
+
* 클라이언트에서 받는 것은 **의도**뿐이다 — 어디서 시작됐나(origin), 왜 하나(reason).
|
|
25
|
+
*/
|
|
26
|
+
@Entity()
|
|
27
|
+
/* 목록 기본 정렬 — 최근 것부터. 감사는 "요즘 무슨 일이 있었나" 로 시작한다. */
|
|
28
|
+
@Index('ix_twin_audit_0', (e: TwinAuditEvent) => [e.domain, e.instanceId, e.eventTime], { unique: false })
|
|
29
|
+
/* 행위자로 찾기 — 저널에 있을 때는 불가능했던 질문("김현장이 승인한 것 전부"). */
|
|
30
|
+
@Index('ix_twin_audit_1', (e: TwinAuditEvent) => [e.domain, e.actorEmail, e.eventTime], { unique: false })
|
|
31
|
+
@ObjectType({ description: 'Audit record of a twin command: who approved what, and what the kernel answered.' })
|
|
32
|
+
export class TwinAuditEvent {
|
|
33
|
+
@PrimaryGeneratedColumn('uuid')
|
|
34
|
+
@Field(type => ID, { description: 'Unique identifier of the audit record.' })
|
|
35
|
+
readonly id: string
|
|
36
|
+
|
|
37
|
+
@ManyToOne(type => Domain)
|
|
38
|
+
@Field(type => Domain, { nullable: true, description: 'Owning tenant domain.' })
|
|
39
|
+
domain?: Domain
|
|
40
|
+
|
|
41
|
+
@RelationId((e: TwinAuditEvent) => e.domain)
|
|
42
|
+
domainId?: string
|
|
43
|
+
|
|
44
|
+
@Column()
|
|
45
|
+
@Field({ description: 'Twin runtime instance the command was aimed at.' })
|
|
46
|
+
instanceId: string
|
|
47
|
+
|
|
48
|
+
/*
|
|
49
|
+
* 서버 시각. 클라이언트 시계를 믿지 않는다 — 기록의 시간축이 단말 설정에 좌우되면 안 된다.
|
|
50
|
+
*
|
|
51
|
+
* `datetime` 을 쓴다. `timestamp` 는 **sqlite 가 지원하지 않아** 5개 드라이버 공통이 아니다
|
|
52
|
+
* (실제로 그렇게 썼다가 DB 를 쓰는 검사 33개가 한꺼번에 터졌다).
|
|
53
|
+
*/
|
|
54
|
+
@Column({ type: 'datetime' })
|
|
55
|
+
@Field({ description: 'Server time when the command was dispatched.' })
|
|
56
|
+
eventTime: Date
|
|
57
|
+
|
|
58
|
+
@Column({ nullable: true })
|
|
59
|
+
@Field({ nullable: true, description: 'Client-supplied command id, for correlating with kernel logs.' })
|
|
60
|
+
commandId?: string
|
|
61
|
+
|
|
62
|
+
@Column({ nullable: true })
|
|
63
|
+
@Field({ nullable: true, description: 'Command type (e.g. order.hold, equipment.repair).' })
|
|
64
|
+
commandType?: string
|
|
65
|
+
|
|
66
|
+
/* 'user' | 'ai' | 'automation' | 'unknown'. 모르면 unknown 이지 사람이 한 것으로 치지 않는다. */
|
|
67
|
+
@Column({ nullable: true })
|
|
68
|
+
@Field({ nullable: true, description: "Where the command originated: 'user' | 'ai' | 'automation' | 'unknown'." })
|
|
69
|
+
origin?: string
|
|
70
|
+
|
|
71
|
+
@Column({ type: 'text', nullable: true })
|
|
72
|
+
@Field({ nullable: true, description: 'Why this action — the rationale shown on the approval card.' })
|
|
73
|
+
reason?: string
|
|
74
|
+
|
|
75
|
+
/* ── 신원: 세션에서만 온다 ── */
|
|
76
|
+
@Column({ nullable: true })
|
|
77
|
+
@Field({ nullable: true, description: 'Approving user id, stamped by the server from the session.' })
|
|
78
|
+
actorId?: string
|
|
79
|
+
|
|
80
|
+
@Column({ nullable: true })
|
|
81
|
+
@Field({ nullable: true, description: 'Approving user email, stamped by the server from the session.' })
|
|
82
|
+
actorEmail?: string
|
|
83
|
+
|
|
84
|
+
@Column({ nullable: true })
|
|
85
|
+
@Field({ nullable: true, description: 'Approving user name, stamped by the server from the session.' })
|
|
86
|
+
actorName?: string
|
|
87
|
+
|
|
88
|
+
/* ── 결과: 거절도 남긴다 ── */
|
|
89
|
+
@Column({ default: false })
|
|
90
|
+
@Field({ description: 'Whether the kernel accepted the command.' })
|
|
91
|
+
accepted: boolean
|
|
92
|
+
|
|
93
|
+
@Column({ nullable: true })
|
|
94
|
+
@Field({ nullable: true, description: 'Language-neutral rejection code when the kernel refused.' })
|
|
95
|
+
errorCode?: string
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 명령 인자 — **원형 그대로.** 무엇에 대한 명령이었는지가 감사의 핵심이라 요약하지 않는다.
|
|
99
|
+
* 지나치게 크면 자르되 `argsTruncated` 로 잘랐다는 사실을 남긴다(조용히 버리지 않는다).
|
|
100
|
+
*/
|
|
101
|
+
@Column({ type: 'simple-json', nullable: true })
|
|
102
|
+
@Field(type => ScalarObject, { nullable: true, description: 'Command arguments as given.' })
|
|
103
|
+
args?: any
|
|
104
|
+
|
|
105
|
+
@Column({ default: false })
|
|
106
|
+
@Field({ description: 'True when args were too large and stored truncated.' })
|
|
107
|
+
argsTruncated: boolean
|
|
108
|
+
|
|
109
|
+
@CreateDateColumn()
|
|
110
|
+
@Field({ nullable: true, description: 'Timestamp when the audit row was written.' })
|
|
111
|
+
createdAt?: Date
|
|
112
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Args, Ctx, Query, Resolver } from 'type-graphql'
|
|
2
|
+
|
|
3
|
+
import { ListParam, getQueryBuilderFromListParams, getRepository, ScalarObject } from '@things-factory/shell'
|
|
4
|
+
|
|
5
|
+
import { TwinAuditEvent } from './twin-audit-event.js'
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* 승인 기록 조회 — **감사의 기본 질문에 답한다.**
|
|
9
|
+
*
|
|
10
|
+
* "요즘 무슨 조치가 나갔나" → 최근순 목록
|
|
11
|
+
* "김현장이 승인한 것 전부" → 행위자 검색(인덱스 있음)
|
|
12
|
+
* "AI 가 제안한 것만" → origin 필터
|
|
13
|
+
* "거절된 시도" → accepted 필터
|
|
14
|
+
*
|
|
15
|
+
* 저널에 섞여 있을 때는 이 중 어느 것도 못 했다 — 행위자가 payload JSON 안이라 인덱스를 걸 수 없었다.
|
|
16
|
+
*/
|
|
17
|
+
/** 시각 필터 값을 날짜로 — 못 읽는 값은 그대로 둔다(지어내지 않는다). */
|
|
18
|
+
function toDateValue(v: any): any {
|
|
19
|
+
const one = (x: any) => {
|
|
20
|
+
if (x instanceof Date) return x
|
|
21
|
+
const t = typeof x === 'string' ? Date.parse(x) : NaN
|
|
22
|
+
return Number.isNaN(t) ? x : new Date(t)
|
|
23
|
+
}
|
|
24
|
+
return Array.isArray(v) ? v.map(one) : one(v)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@Resolver()
|
|
28
|
+
export class TwinAuditQuery {
|
|
29
|
+
/* 조회 게이트는 달지 않는다 — 이 저장소의 규약이고(mutation-gate 검사가 정본을 지킨다),
|
|
30
|
+
도메인 필터가 테넌트 경계를 이미 지킨다. 테넌트를 넘어 읽는 계기판만 OPS_GATE 를 쓴다. */
|
|
31
|
+
@Query(returns => ScalarObject, {
|
|
32
|
+
nullable: true,
|
|
33
|
+
description:
|
|
34
|
+
'List twin command audit records (who approved what, and what the kernel answered) with the standard list contract, plus the total match count.'
|
|
35
|
+
})
|
|
36
|
+
async twinAuditList(
|
|
37
|
+
/* 표준 목록 계약은 `@Args` 로 펼쳐 받는다 — `@Arg` 로 받으면 type-graphql 이 입력 타입을
|
|
38
|
+
결정하지 못해 **스키마 조립 자체가 실패**한다(서버가 아예 안 뜬다). */
|
|
39
|
+
@Args(type => ListParam) params: ListParam,
|
|
40
|
+
@Ctx() context: ResolverContext
|
|
41
|
+
): Promise<any> {
|
|
42
|
+
const domainId = context.state.domain?.id
|
|
43
|
+
if (!domainId) return { items: [], total: 0 }
|
|
44
|
+
|
|
45
|
+
const effective: ListParam = {
|
|
46
|
+
...(params ?? ({} as ListParam)),
|
|
47
|
+
/* 기본 정렬 — 최근 것부터. 감사는 "요즘" 에서 시작한다. */
|
|
48
|
+
sortings: params?.sortings?.length ? params.sortings : [{ name: 'eventTime', desc: true }],
|
|
49
|
+
/* 시각 필터는 날짜로 옮겨서 넘긴다 — 문자열 그대로면 조용히 0건이 된다(저널과 같은 이유). */
|
|
50
|
+
filters: (params?.filters ?? []).map(f => (f.name === 'eventTime' ? { ...f, value: toDateValue(f.value) } : f))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const qb = getQueryBuilderFromListParams({
|
|
54
|
+
repository: getRepository(TwinAuditEvent),
|
|
55
|
+
params: effective,
|
|
56
|
+
domain: context.state.domain,
|
|
57
|
+
alias: 'a',
|
|
58
|
+
/*
|
|
59
|
+
* `searchables` 는 전부 **실컬럼**이다 — 프레임워크 질의 빌더는 여기 없는 컬럼의 LIKE 를
|
|
60
|
+
* 경고 후 무시한다(인덱스 없는 전체 스캔 방지). 행위자로 찾는 것이 이 테이블을 구분한 이유다.
|
|
61
|
+
*/
|
|
62
|
+
searchables: ['actorEmail', 'actorName', 'commandType', 'origin', 'errorCode', 'reason'],
|
|
63
|
+
sortables: ['eventTime', 'commandType', 'actorEmail', 'origin', 'accepted'],
|
|
64
|
+
/* 감사 목록은 사람이 읽는 속도로 본다 — 크게 퍼 오는 것이 이득이 아니다. */
|
|
65
|
+
defaultLimit: 50,
|
|
66
|
+
maxLimit: 200
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const [items, total] = await qb.getManyAndCount()
|
|
70
|
+
return { items, total }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import { Arg, Ctx, Mutation, Resolver } from 'type-graphql'
|
|
1
|
+
import { Arg, Ctx, Mutation, Resolver, Directive} from 'type-graphql'
|
|
2
|
+
|
|
3
|
+
import { validateScenario } from '@operato/twin-kernel'
|
|
2
4
|
|
|
3
5
|
import { ScalarObject } from '@things-factory/shell'
|
|
4
6
|
|
|
7
|
+
import { getRepository } from '@things-factory/shell'
|
|
8
|
+
|
|
5
9
|
import { TwinEngine } from '../../engine/index.js'
|
|
10
|
+
import { TwinAuditEvent } from '../twin-audit/twin-audit-event.js'
|
|
11
|
+
import { buildCommandAudit } from '../twin-audit/command-audit.js'
|
|
6
12
|
|
|
7
13
|
/*
|
|
8
14
|
* 트윈 Command·Scenario 채널 — 상향 mutation.
|
|
@@ -15,27 +21,48 @@ import { TwinEngine } from '../../engine/index.js'
|
|
|
15
21
|
@Resolver()
|
|
16
22
|
export class TwinControlMutation {
|
|
17
23
|
/** Command 채널 — 오더 hold/resume/release 등. 커널 dispatch 로 위임, ack{accepted,error} 반환. */
|
|
24
|
+
@Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
|
|
18
25
|
@Mutation(returns => ScalarObject, { description: 'dispatch a twin command (order.hold/resume/release …). Returns {accepted, error?}.' })
|
|
19
26
|
async dispatchTwinCommand(
|
|
20
27
|
@Arg('instanceId') instanceId: string,
|
|
21
28
|
@Arg('command', type => ScalarObject) command: any,
|
|
22
29
|
@Ctx() context: ResolverContext
|
|
23
|
-
): Promise<{ accepted: boolean; error?: string }> {
|
|
30
|
+
): Promise<{ accepted: boolean; error?: string; errorCode?: string; errorParams?: Record<string, string | number> }> {
|
|
24
31
|
const domainId = context.state.domain?.id
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
/*
|
|
32
|
+
if (!domainId) return { accepted: false, error: 'no-domain', errorCode: 'no-domain' }
|
|
33
|
+
/*
|
|
34
|
+
* 라우팅은 엔진이 한다 — 예전에는 여기서 `inst.runtime.dispatch` 를 곧바로 불렀고, `runtime` 이
|
|
35
|
+
* 시뮬에만 있어서 미러 트윈에서는 이 한 줄이 TypeError 로 터졌다(깔끔한 거절이 아니라 실패).
|
|
36
|
+
* 권위(모드) × 성격(주석/액추에이션) 판정은 한 곳에만 둔다.
|
|
37
|
+
*/
|
|
38
|
+
/* 원본을 그대로 감사에 넘긴다 — 무엇이 들어왔는지가 기록의 대상이다(정규화는 감사 모듈이 한다). */
|
|
39
|
+
const cmdWithOrigin = command
|
|
40
|
+
const ack = await TwinEngine.dispatchCommand(domainId, instanceId, command)
|
|
41
|
+
/* 라이브 구조 변이(resource.add)는 저장 model 에도 반영 — 프로비저닝 편집기·재기동이 추가분을 잃지 않게. */
|
|
32
42
|
if (ack?.accepted && command?.type === 'resource.add') {
|
|
33
43
|
await TwinEngine.syncBoardEquipment(domainId, instanceId).catch(err => console.error('twin syncBoardEquipment fail', err))
|
|
34
44
|
}
|
|
35
|
-
|
|
45
|
+
/*
|
|
46
|
+
* 감사 기록 — 승인 게이트가 앞을 막고, 이 줄이 뒤를 설명한다. 둘 다 있어야 "사람 손을 거쳤다" 를
|
|
47
|
+
* 증명할 수 있다. 커널이 거절한 것도 남긴다 — 무엇을 시도했는지가 감사의 절반이다.
|
|
48
|
+
*
|
|
49
|
+
* 기록이 실패해도 커맨드 결과는 그대로 돌려준다. 감사가 본업을 막으면 안 되지만, 실패를 삼키지도
|
|
50
|
+
* 않는다(로그에 남긴다) — 조용히 안 남으면 나중에 "기록이 없다" 와 "일이 없었다" 를 구분 못 한다.
|
|
51
|
+
*/
|
|
52
|
+
try {
|
|
53
|
+
const audit = buildCommandAudit({ command: cmdWithOrigin, ack, actor: context.state.user, at: new Date() })
|
|
54
|
+
const repo = getRepository(TwinAuditEvent)
|
|
55
|
+
await repo.save(repo.create({ domain: context.state.domain, instanceId, ...audit }))
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.error(`[twin-control] command audit not recorded for "${instanceId}"`, err)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* 거절 사유는 **언어 중립 코드**로 함께 돌려준다 — 화면이 `twin.cmderr.<code>` 로 사람 말을 만든다. */
|
|
61
|
+
return { accepted: !!ack?.accepted, error: ack?.error, errorCode: ack?.errorCode, errorParams: ack?.errorParams }
|
|
36
62
|
}
|
|
37
63
|
|
|
38
64
|
/** Scenario 채널 — 생성기 시나리오 제어(load/start/pause/setSpeed). */
|
|
65
|
+
@Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
|
|
39
66
|
@Mutation(returns => Boolean, { description: 'control twin scenario (load|start|pause|setSpeed)' })
|
|
40
67
|
async controlTwinScenario(
|
|
41
68
|
@Arg('instanceId') instanceId: string,
|
|
@@ -44,14 +71,27 @@ export class TwinControlMutation {
|
|
|
44
71
|
@Arg('scenario', type => ScalarObject, { nullable: true }) scenario?: any
|
|
45
72
|
): Promise<boolean> {
|
|
46
73
|
const domainId = context.state.domain?.id
|
|
47
|
-
|
|
48
|
-
if (!
|
|
49
|
-
const runtime =
|
|
74
|
+
/* 테넌트 격리 — 레지스트리를 직접 색인하지 않는다(키에 도메인이 들어 있다). */
|
|
75
|
+
if (!TwinEngine.owns(domainId, instanceId)) return false
|
|
76
|
+
const runtime = TwinEngine.runtime(domainId, instanceId)
|
|
50
77
|
/*
|
|
51
78
|
* 자극 주입 가드 — mirror 트윈(외부 실물이 진실)은 커널 시나리오 엔진이 없다(startLive=projector, runtime 없음).
|
|
52
79
|
* what-if 를 여기에 주입하면 현실을 오염시키므로 거부한다(sim-experiment/sim-world 만 자극 가능). silent throw 방지.
|
|
53
80
|
*/
|
|
54
81
|
if (!runtime?.scenario) return false
|
|
82
|
+
/*
|
|
83
|
+
* **잘못된 선언은 문 앞에서 막는다.** 예전에는 검증 없이 실었고, 다음 tick 에서 `rate.meanPerHour`
|
|
84
|
+
* 를 읽다 터져 **서버 프로세스가 내려갔다**(클라이언트가 보낸 값 하나로 앱 전체가 죽었다).
|
|
85
|
+
* 규칙은 커널이 들고 있다(계약의 주인) — 호스트가 따로 만들면 방언이 생긴다.
|
|
86
|
+
*/
|
|
87
|
+
if (action === 'load') {
|
|
88
|
+
const v = validateScenario(scenario)
|
|
89
|
+
if (v.ok === false) {
|
|
90
|
+
/* 사유를 로그로 남긴다 — 이 뮤테이션의 반환은 Boolean 계약이라 사유를 실어 보낼 자리가 없다. */
|
|
91
|
+
console.warn(`[twin] scenario rejected for "${instanceId}": ${v.errorCode} ${JSON.stringify(v.errorParams ?? {})}`)
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
94
|
+
}
|
|
55
95
|
if (action === 'load' && scenario) runtime.scenario.load(scenario)
|
|
56
96
|
else if (action === 'start') runtime.scenario.start()
|
|
57
97
|
else if (action === 'pause') runtime.scenario.pause()
|
|
@@ -83,7 +83,7 @@ export function locationOf(envelope: any): string | undefined {
|
|
|
83
83
|
* 설비·무버 — 운영 델타(equipment.status·task.status)가 대상을 가리키는 축.
|
|
84
84
|
*
|
|
85
85
|
* EPCIS 어휘가 아니라서 다른 축 어디에도 안 잡힌다. 이게 없으면 "이 지게차가 오늘 무엇을 했나" 를
|
|
86
|
-
* 서버에서 물을 방법이 없어, 화면이 저널을 통째로 받아
|
|
86
|
+
* 서버에서 물을 방법이 없어, 화면이 저널을 통째로 받아 전수 조회하는 수밖에 없다.
|
|
87
87
|
*/
|
|
88
88
|
export function equipmentIdOf(envelope: any): string | undefined {
|
|
89
89
|
const d = envelope?.data ?? envelope ?? {}
|
|
@@ -23,7 +23,7 @@ import { Domain, ScalarObject } from '@things-factory/shell'
|
|
|
23
23
|
*
|
|
24
24
|
* bizStep·locationId·moverId 는 컬럼만 두고 인덱스는 두지 않는다 — 한 트윈 안에서 카디널리티가
|
|
25
25
|
* 낮아(업무단계 몇 개, 위치 수백, 설비 수십) (domain,instanceId) 로 이미 좁혀진 뒤의 잔여 필터로
|
|
26
|
-
* 충분하고, 뜨거운 표에 인덱스를 더 얹을
|
|
26
|
+
* 충분하고, 뜨거운 표에 인덱스를 더 얹을 가치가 없다. 저널 하나가 아주 커져서 이 축들의 조회가
|
|
27
27
|
* 느려지면 그때 측정을 근거로 인덱스를 추가할 일이지, 지레 얹어 쓰기를 무겁게 할 일은 아니다.
|
|
28
28
|
*/
|
|
29
29
|
@Entity()
|
|
@@ -32,6 +32,8 @@ import { Domain, ScalarObject } from '@things-factory/shell'
|
|
|
32
32
|
@Index('ix_twin_event_2', (e: TwinEvent) => [e.domain, e.instanceId, e.eventType, e.revision], { unique: false })
|
|
33
33
|
@Index('ix_twin_event_3', (e: TwinEvent) => [e.domain, e.instanceId, e.epc], { unique: false })
|
|
34
34
|
@Index('ix_twin_event_4', (e: TwinEvent) => [e.domain, e.instanceId, e.orderId], { unique: false })
|
|
35
|
+
/* ix_5 — "이 지시가 무엇을 일으켰나". 감사 기록의 commandId 로 저널을 다시 계산하는 축. */
|
|
36
|
+
@Index('ix_twin_event_5', (e: TwinEvent) => [e.domain, e.correlationId], { unique: false })
|
|
35
37
|
@ObjectType({ description: 'Append-only twin event journal record (EPCIS event or operational delta).' })
|
|
36
38
|
export class TwinEvent {
|
|
37
39
|
@PrimaryGeneratedColumn('uuid')
|
|
@@ -45,17 +47,19 @@ export class TwinEvent {
|
|
|
45
47
|
@RelationId((e: TwinEvent) => e.domain)
|
|
46
48
|
domainId?: string
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
@
|
|
54
|
-
|
|
50
|
+
/*
|
|
51
|
+
* 이 사실을 낸 트윈. **빌 수 없다** — 저널 행을 만드는 곳은 `journalRow` 하나뿐이고 거기서
|
|
52
|
+
* 필수 인자로 받는다. nullable 로 두면 읽는 쪽이 매번 `?? ''` 를 달게 되고, 그 방어가 실제로는
|
|
53
|
+
* 일어나지 않는 경우를 대비하느라 "빈 인스턴스의 이벤트" 라는 없는 개념을 코드에 남긴다.
|
|
54
|
+
*/
|
|
55
|
+
@Column()
|
|
56
|
+
@Field({ description: 'Twin runtime instance id that emitted the event.' })
|
|
57
|
+
instanceId: string
|
|
55
58
|
|
|
56
|
-
|
|
57
|
-
@
|
|
58
|
-
|
|
59
|
+
/* 커널 봉투의 필수 필드(`CanonicalEnvelope.eventType`)라 빌 수 없다 — 계약을 그대로 옮긴다. */
|
|
60
|
+
@Column()
|
|
61
|
+
@Field({ description: 'Event type: epcis.* | task.status | equipment.status | order.status.' })
|
|
62
|
+
eventType: string
|
|
59
63
|
|
|
60
64
|
@Column({ type: 'int', nullable: true })
|
|
61
65
|
@Field(type => Int, { nullable: true, description: 'Monotonic state revision at which the event was emitted.' })
|
|
@@ -66,7 +70,7 @@ export class TwinEvent {
|
|
|
66
70
|
*
|
|
67
71
|
* 이것이 없던 시절에는 구조가 바뀌면 저널을 통째로 지우는 것이 유일한 길이었다(안 지우면 옛
|
|
68
72
|
* 이벤트를 새 공장에 대고 접어 이력이 거짓말을 한다). 이 한 칸이 셋째 길을 연다 — 재생이
|
|
69
|
-
* 구조가 바뀌는 지점에서
|
|
73
|
+
* 구조가 바뀌는 지점에서 전환한 뒤 이어 접는다.
|
|
70
74
|
*
|
|
71
75
|
* **비어 있을 수 있다**: 컬럼이 생기기 전에 쓰인 행이다. 그 행들은 **가장 오래된 리비전**에
|
|
72
76
|
* 속한다(0 을 채워 넣지 않는다 — 0 은 "모름" 이 아니라 유효한 번호처럼 보인다).
|
|
@@ -75,9 +79,18 @@ export class TwinEvent {
|
|
|
75
79
|
@Field(type => Int, { nullable: true, description: 'Structure revision (TwinStructure.rev) in force when this event happened. Absent on rows written before structure revisions existed; those belong to the oldest revision.' })
|
|
76
80
|
structureRev?: number
|
|
77
81
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
/*
|
|
83
|
+
* 이 사실이 일어난 시각(트윈의 시계). **날짜 타입이다.**
|
|
84
|
+
*
|
|
85
|
+
* 예전에는 문자열이었다. 읽을 때마다 `Date.parse` 했고, 정렬은 UTC ISO 만 들어온다는 전제 위에서
|
|
86
|
+
* 사전식 비교가 우연히 맞았다 — `+09:00` 오프셋 표기가 하나 섞이면 조용히 순서가 틀린다.
|
|
87
|
+
* 그 전제는 컬럼 어디에도 적혀 있지 않았다.
|
|
88
|
+
*
|
|
89
|
+
* `datetime` 을 쓴다. `timestamp` 는 sqlite 가 지원하지 않아 5개 드라이버 공통이 아니다.
|
|
90
|
+
*/
|
|
91
|
+
@Column({ type: 'datetime', nullable: true })
|
|
92
|
+
@Field({ nullable: true, description: 'Time at which the fact happened on the twin clock.' })
|
|
93
|
+
eventTime?: Date
|
|
81
94
|
|
|
82
95
|
/*
|
|
83
96
|
* ── 승격된 검색 축 ────────────────────────────────────────────────────────
|
|
@@ -86,7 +99,31 @@ export class TwinEvent {
|
|
|
86
99
|
* simple-json(TEXT) 안의 값은 5개 DB 드라이버 공통으로 거를 방법이 없어서(멀티DB 호환 규칙상
|
|
87
100
|
* DB별 JSON 연산자 금지) 승격 외의 선택지가 없다.
|
|
88
101
|
* 길이 상한 255 는 GS1 식별자 규격 대비 충분하며, 넘치는 값은 조용히 잘리지 않고 경고를 남긴다.
|
|
102
|
+
*
|
|
103
|
+
* ── 축을 새로 추가할 때 ──────────────────────────────────────────────────
|
|
104
|
+
* 새 승격 컬럼을 만들면 **이미 쌓인 행은 비어 있다.** 그 행들은 새 축으로 검색되지 않고,
|
|
105
|
+
* 사용자에게는 "과거가 없는 것" 으로 보인다(조용히 빠진 데이터).
|
|
106
|
+
*
|
|
107
|
+
* 예전에는 기동 훅이 매번 테이블을 살펴 채웠다. 그건 두 가지로 틀렸다 — 저널이 커질수록 부팅이
|
|
108
|
+
* 느려지고, "bizStep 이 비었으면 옛 행" 이라는 전제를 코드에 고정해 그 전제가 깨지는 순간
|
|
109
|
+
* (감사 행처럼 업무 단계가 없는 행) 매 부팅마다 헛돌았다. 그래서 걷어냈다(git: backfill-keys.ts).
|
|
110
|
+
*
|
|
111
|
+
* 채우는 일은 **마이그레이션 한 번**이 맞다(`yarn migration`). 그 컬럼에 맞는 것을 그때 쓴다 —
|
|
112
|
+
* 범용 스캐너를 되살리지 말 것. 개발 데이타라면 그냥 버리는 것도 답이다.
|
|
113
|
+
*/
|
|
114
|
+
/**
|
|
115
|
+
* 이 사실을 일으킨 **지시** — 커널이 커맨드에서 이어 준 상관값(기본은 commandId).
|
|
116
|
+
*
|
|
117
|
+
* 이것이 감사 기록(TwinAuditEvent.commandId)과 저널을 잇는 유일한 다리다. 없으면 승인 기록은
|
|
118
|
+
* "사람이 허락했다" 까지이고, 그 뒤 공장이 실제로 어떻게 움직였는지와 이어지지 않는다.
|
|
119
|
+
*
|
|
120
|
+
* **대부분의 행은 비어 있다** — 커맨드가 아니라 시뮬 시간이 낳은 사실이기 때문이다. 그것이 정상이고,
|
|
121
|
+
* 비어 있다고 "덜 채워진 옛 행" 으로 보면 안 된다(같은 오해를 bizStep 에서 이미 한 번 했다).
|
|
89
122
|
*/
|
|
123
|
+
@Column({ length: 255, nullable: true })
|
|
124
|
+
@Field({ nullable: true, description: 'Id of the command that caused this fact (kernel correlation id; defaults to the commandId). Empty for facts that simulation time produced rather than a command.' })
|
|
125
|
+
correlationId?: string
|
|
126
|
+
|
|
90
127
|
@Column({ length: 255, nullable: true })
|
|
91
128
|
@Field({ nullable: true, description: 'Business step (CBV bizStep tail), promoted from the payload for indexed filtering.' })
|
|
92
129
|
bizStep?: string
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 예측 지표 — **스냅샷에서 수 하나를 뽑는 규칙.**
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 순수 모듈인가 (2026-08-15) ───────────────────────────────────────────
|
|
5
|
+
* 이 표가 리졸버 안에 있어서 시험이 불러올 수 없었다(type-graphql·shell 을 물고 있다). 그런데 여기서
|
|
6
|
+
* 틀리면 예측 그래프 전체가 조용히 다른 것을 그린다 — 오류 없이. 규칙은 시험이 닿는 자리에 둔다.
|
|
7
|
+
*
|
|
8
|
+
* ── 에너지 지표를 더한 이유 ─────────────────────────────────────────────────
|
|
9
|
+
* 예측 화면의 지표가 물류 여섯(재고·점유·작업·오더·출고·백로그)으로 고정돼 있어서, 에너지 트윈을
|
|
10
|
+
* 고르면 **남의 질문에 답하는 그래프**가 떴다. 에너지 트윈에는 재고도 오더도 없으므로 그 값들은
|
|
11
|
+
* 전부 0 이고, 0 은 「없다」가 아니라 「이 트윈의 질문이 아니다」이다.
|
|
12
|
+
*
|
|
13
|
+
* 여기 더한 셋은 **커널이 이미 상태로 들고 있는 것**이다 — 새 계산을 만들지 않았다. 특히 부하 합은
|
|
14
|
+
* 커널이 뿌리 계량기로만 세는데(계층 계량의 이중 계상 방지), 그 규칙을 여기서 다시 쓰면 두 벌이 된다.
|
|
15
|
+
* 그래서 지점을 직접 더하지 않고 **구간 값**을 읽는다.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** 스냅샷 → 수 하나. 값이 없으면 0 이 아니라 **그 지표를 그릴 수 없다**는 뜻이지만, 그래프 계약이
|
|
19
|
+
* 수를 요구하므로 0 을 낸다 — 그래서 에너지 지표는 「없음」이 0 과 헷갈리지 않는 것만 골랐다. */
|
|
20
|
+
export type MetricFn = (s: any) => number
|
|
21
|
+
|
|
22
|
+
export const FORECAST_METRICS: Record<string, MetricFn> = {
|
|
23
|
+
items: s => (s?.items || []).length,
|
|
24
|
+
occupancy: s => (s?.locations || []).reduce((a: number, n: any) => a + (n.occupancy || 0), 0),
|
|
25
|
+
tasks: s => (s?.tasks || []).length,
|
|
26
|
+
orders: s => (s?.orders || []).length,
|
|
27
|
+
// 이행 지표 — 라이브 예측의 핵심 질문("언제 다 나가나·백로그 언제 풀리나").
|
|
28
|
+
shipped: s => (s?.orders || []).filter((o: any) => o.status === 'shipped').length,
|
|
29
|
+
backlog: s => (s?.orders || []).filter((o: any) => o.status !== 'shipped' && o.status !== 'cancelled').length,
|
|
30
|
+
|
|
31
|
+
/*
|
|
32
|
+
* ── 에너지 ────────────────────────────────────────────────────────────────
|
|
33
|
+
* `peakKW` 는 **마감된 구간의 최대**다 — 단조 증가라 지평선 위에서 「최대수요가 어디까지 오르나」로
|
|
34
|
+
* 읽힌다. 열린 구간은 넣지 않는다(아직 확정이 아니다 — 커널이 지키는 규율을 여기서도 지킨다).
|
|
35
|
+
*/
|
|
36
|
+
peakKW: s => Number(s?.energy?.peakSince?.kW) || 0,
|
|
37
|
+
/*
|
|
38
|
+
* 이번 구간이 이대로 가면 얼마로 마감되나 — 커널이 낸 투영(`mean-so-far`)을 그대로 읽는다.
|
|
39
|
+
* 우리가 다시 계산하지 않는다: 투영 규칙이 두 벌이 되면 화면과 판정이 다른 말을 한다.
|
|
40
|
+
*/
|
|
41
|
+
projectedKW: s => Number(s?.energy?.open?.projectedKW ?? s?.energy?.open?.meanKW) || 0,
|
|
42
|
+
/*
|
|
43
|
+
* 계약을 넘긴 구간 수 — 「몇 번 넘나」는 요금이 걸린 질문이라 최대치와 따로 본다.
|
|
44
|
+
* 계약을 모르는 트윈에서는 늘 0 이다(넘김을 판정할 근거가 없다 — 지어내지 않는다).
|
|
45
|
+
*/
|
|
46
|
+
overWindows: s => (s?.energy?.closed || []).filter((w: any) => w?.overContract === true).length
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 이 지표가 그 종류의 트윈에서 뜻이 있나 — 화면이 목록을 고를 때 쓴다. */
|
|
50
|
+
export const ENERGY_METRICS = ['projectedKW', 'peakKW', 'overWindows'] as const
|
|
51
|
+
export const FLOW_METRICS = ['items', 'occupancy', 'tasks', 'orders', 'shipped', 'backlog'] as const
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 트윈 종류에 맞는 지표 목록 — **한 곳에서 정한다.**
|
|
55
|
+
*
|
|
56
|
+
* 화면이 각자 목록을 들고 있으면 종류가 늘 때 어느 화면이 뒤처졌는지 알 수 없다.
|
|
57
|
+
*/
|
|
58
|
+
export function metricsForKind(kind?: string): string[] {
|
|
59
|
+
return kind === 'ems' ? [...ENERGY_METRICS] : [...FLOW_METRICS]
|
|
60
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 예측을 얼마나 잘게 굴릴 것인가 — **한 회차의 틱 수를 묶는다.**
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 필요한가 (2026-08-17 실측) ───────────────────────────────────────────
|
|
5
|
+
* 예측 비용의 거의 전부가 틱이다. 트윈 하나의 예측 한 번이 30회차 × 300틱 = 9,000틱, 한 틱 3.2ms →
|
|
6
|
+
* 28.5초였다. 그런데 몬테카를로는 지평선 **끝에서** 지표를 한 번 읽는다 — 중간을 1초 단위로 볼 이유가
|
|
7
|
+
* 없다면 성글게 굴려도 답이 같다.
|
|
8
|
+
*
|
|
9
|
+
* 정말 같은지는 재서 확인했다(`operato-twin/test/fork-cost.test.ts`, 10회차·지평선 5분):
|
|
10
|
+
*
|
|
11
|
+
* step 1s → 3,048ms · 재고 p50 325 · 완료작업 8 · 완료오더 0
|
|
12
|
+
* step 5s → 628ms · 재고 p50 325 · 완료작업 8 · 완료오더 0 ← 답이 같고 5배 싸다
|
|
13
|
+
* step 10s → 286ms · 재고 p50 326 · … ← 여기서부터 흔들린다
|
|
14
|
+
*
|
|
15
|
+
* ── 왜 고정 step 이 아니라 틱 수 상한인가 ───────────────────────────────────
|
|
16
|
+
* step 을 5초로 못 박으면 1분짜리 지평선은 12틱이 되어 너무 성글고, 2시간짜리는 1,440틱이라 여전히
|
|
17
|
+
* 비싸다. 묶어야 하는 것은 step 이 아니라 **한 회차가 도는 횟수**다.
|
|
18
|
+
*
|
|
19
|
+
* 상한(10초)을 따로 두는 이유: 지평선이 길다고 무한정 성글게 굴리면 짧은 작업(초 단위 피킹)이 통째로
|
|
20
|
+
* 한 틱에 삼켜진다. 그 지점을 넘지 않는다.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** 한 회차가 도는 틱 수의 목표 — 이보다 잘게 굴려야 할 이유가 생기면 여기를 고친다. */
|
|
24
|
+
export const TARGET_TICKS_PER_RUN = 60
|
|
25
|
+
/** 아무리 지평선이 길어도 이보다 성글게 굴리지 않는다 — 짧은 작업이 한 틱에 삼켜진다. */
|
|
26
|
+
export const MAX_STEP_MS = 10_000
|
|
27
|
+
/** 커널의 시각 단위 — 이보다 잘게 굴릴 이유는 없다. */
|
|
28
|
+
export const MIN_STEP_MS = 1_000
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 이 지평선을 굴릴 틱 크기.
|
|
32
|
+
*
|
|
33
|
+
* 초 단위로 떨어뜨린다 — 3,333ms 같은 값은 사람이 읽을 수도, 화면이 설명할 수도 없다.
|
|
34
|
+
*/
|
|
35
|
+
export function forecastStepMs(horizonMs: number): number {
|
|
36
|
+
if (!Number.isFinite(horizonMs) || horizonMs <= 0) return MIN_STEP_MS
|
|
37
|
+
const raw = horizonMs / TARGET_TICKS_PER_RUN
|
|
38
|
+
const rounded = Math.round(raw / MIN_STEP_MS) * MIN_STEP_MS
|
|
39
|
+
return Math.min(MAX_STEP_MS, Math.max(MIN_STEP_MS, rounded))
|
|
40
|
+
}
|