@things-factory/headless-twin 10.0.11 → 10.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-server/engine/canonical-ingest.d.ts +81 -3
- package/dist-server/engine/canonical-ingest.js +89 -10
- package/dist-server/engine/canonical-ingest.js.map +1 -1
- package/dist-server/engine/command-routing.js +1 -1
- package/dist-server/engine/command-routing.js.map +1 -1
- package/dist-server/engine/declared-stimulus.d.ts +43 -0
- package/dist-server/engine/declared-stimulus.js +57 -0
- package/dist-server/engine/declared-stimulus.js.map +1 -0
- package/dist-server/engine/index.d.ts +5 -0
- package/dist-server/engine/index.js +6 -0
- package/dist-server/engine/index.js.map +1 -1
- package/dist-server/engine/ingest-health.d.ts +335 -0
- package/dist-server/engine/ingest-health.js +434 -0
- package/dist-server/engine/ingest-health.js.map +1 -0
- package/dist-server/engine/integration-coverage.d.ts +76 -0
- package/dist-server/engine/integration-coverage.js +73 -0
- package/dist-server/engine/integration-coverage.js.map +1 -0
- package/dist-server/engine/integration-probes.d.ts +65 -0
- package/dist-server/engine/integration-probes.js +100 -0
- package/dist-server/engine/integration-probes.js.map +1 -0
- package/dist-server/engine/integration-runner.d.ts +45 -0
- package/dist-server/engine/integration-runner.js +59 -0
- package/dist-server/engine/integration-runner.js.map +1 -0
- package/dist-server/engine/integration-target-profile.d.ts +57 -0
- package/dist-server/engine/integration-target-profile.js +79 -0
- package/dist-server/engine/integration-target-profile.js.map +1 -0
- package/dist-server/engine/kpi-fold.d.ts +39 -0
- package/dist-server/engine/kpi-fold.js +68 -5
- package/dist-server/engine/kpi-fold.js.map +1 -1
- package/dist-server/engine/kpi-query.d.ts +3 -3
- package/dist-server/engine/kpi-query.js +132 -18
- package/dist-server/engine/kpi-query.js.map +1 -1
- package/dist-server/engine/live-feed-registry.d.ts +1 -1
- package/dist-server/engine/live-feed-registry.js +3 -17
- package/dist-server/engine/live-feed-registry.js.map +1 -1
- package/dist-server/engine/load-meter.d.ts +1 -1
- package/dist-server/engine/load-meter.js +12 -3
- package/dist-server/engine/load-meter.js.map +1 -1
- package/dist-server/engine/local-declarations.d.ts +3 -6
- package/dist-server/engine/local-declarations.js +97 -16
- package/dist-server/engine/local-declarations.js.map +1 -1
- package/dist-server/engine/log.d.ts +18 -0
- package/dist-server/engine/log.js +80 -0
- package/dist-server/engine/log.js.map +1 -0
- package/dist-server/engine/loop-lag.d.ts +54 -0
- package/dist-server/engine/loop-lag.js +87 -0
- package/dist-server/engine/loop-lag.js.map +1 -0
- package/dist-server/engine/measured-yield.d.ts +42 -0
- package/dist-server/engine/measured-yield.js +75 -0
- package/dist-server/engine/measured-yield.js.map +1 -0
- package/dist-server/engine/model-gap.d.ts +108 -0
- package/dist-server/engine/model-gap.js +95 -0
- package/dist-server/engine/model-gap.js.map +1 -0
- package/dist-server/engine/operation-basis.d.ts +16 -0
- package/dist-server/engine/operation-basis.js +20 -2
- package/dist-server/engine/operation-basis.js.map +1 -1
- package/dist-server/engine/property-effects.js +17 -0
- package/dist-server/engine/property-effects.js.map +1 -1
- package/dist-server/engine/restart-policy.d.ts +13 -0
- package/dist-server/engine/restart-policy.js +52 -0
- package/dist-server/engine/restart-policy.js.map +1 -0
- package/dist-server/engine/runtime-key.js +1 -1
- package/dist-server/engine/runtime-key.js.map +1 -1
- package/dist-server/engine/spec-coverage.d.ts +8 -0
- package/dist-server/engine/spec-coverage.js +3 -1
- package/dist-server/engine/spec-coverage.js.map +1 -1
- package/dist-server/engine/stage-path.d.ts +83 -0
- package/dist-server/engine/stage-path.js +118 -0
- package/dist-server/engine/stage-path.js.map +1 -0
- package/dist-server/engine/twin-engine.d.ts +400 -31
- package/dist-server/engine/twin-engine.js +1198 -187
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/index.js +35 -3
- package/dist-server/index.js.map +1 -1
- package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.d.ts +5 -0
- package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js +58 -0
- package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js.map +1 -0
- package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.d.ts +5 -0
- package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js +55 -0
- package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js.map +1 -0
- package/dist-server/migrations/index.js +7 -1
- package/dist-server/migrations/index.js.map +1 -1
- package/dist-server/service/index.d.ts +5 -2
- package/dist-server/service/index.js +18 -7
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/reference/control-routing.d.ts +14 -0
- package/dist-server/service/reference/control-routing.js +64 -0
- package/dist-server/service/reference/control-routing.js.map +1 -0
- package/dist-server/service/reference/discovery-result.d.ts +1 -1
- package/dist-server/service/reference/discovery-result.js +1 -1
- package/dist-server/service/reference/discovery-result.js.map +1 -1
- package/dist-server/service/reference/index.d.ts +5 -1
- package/dist-server/service/reference/index.js +8 -1
- package/dist-server/service/reference/index.js.map +1 -1
- package/dist-server/service/reference/reference-adapter.d.ts +251 -3
- package/dist-server/service/reference/reference-adapter.js +43 -1
- package/dist-server/service/reference/reference-adapter.js.map +1 -1
- package/dist-server/service/reference/reference-assessment.d.ts +68 -0
- package/dist-server/service/reference/reference-assessment.js +136 -0
- package/dist-server/service/reference/reference-assessment.js.map +1 -0
- package/dist-server/service/reference/reference-live.d.ts +12 -1
- package/dist-server/service/reference/reference-live.js +119 -14
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-master.d.ts +47 -1
- package/dist-server/service/reference/reference-master.js +34 -4
- package/dist-server/service/reference/reference-master.js.map +1 -1
- package/dist-server/service/reference/reference-probe.d.ts +92 -0
- package/dist-server/service/reference/reference-probe.js +186 -0
- package/dist-server/service/reference/reference-probe.js.map +1 -0
- package/dist-server/service/reference/reference-progress-subscription.d.ts +17 -0
- package/dist-server/service/reference/reference-progress-subscription.js +94 -0
- package/dist-server/service/reference/reference-progress-subscription.js.map +1 -0
- package/dist-server/service/reference/reference-progress.d.ts +38 -0
- package/dist-server/service/reference/reference-progress.js +71 -0
- package/dist-server/service/reference/reference-progress.js.map +1 -0
- package/dist-server/service/reference/reference-resolver.d.ts +3 -3
- package/dist-server/service/reference/reference-resolver.js +242 -58
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/reference/twin-reference.d.ts +32 -0
- package/dist-server/service/reference/twin-reference.js +10 -0
- package/dist-server/service/reference/twin-reference.js.map +1 -1
- package/dist-server/service/twin-audit/command-audit.d.ts +34 -0
- package/dist-server/service/twin-audit/command-audit.js +15 -1
- package/dist-server/service/twin-audit/command-audit.js.map +1 -1
- package/dist-server/service/twin-audit/twin-audit-event.d.ts +3 -0
- package/dist-server/service/twin-audit/twin-audit-event.js +10 -0
- package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -1
- package/dist-server/service/twin-control/twin-control-mutation.d.ts +15 -2
- package/dist-server/service/twin-control/twin-control-mutation.js +88 -36
- package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
- package/dist-server/service/twin-event/journal-count.d.ts +21 -0
- package/dist-server/service/twin-event/journal-count.js +26 -0
- package/dist-server/service/twin-event/journal-count.js.map +1 -0
- package/dist-server/service/twin-event/twin-event-keys.d.ts +9 -0
- package/dist-server/service/twin-event/twin-event-keys.js +15 -18
- package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
- package/dist-server/service/twin-event/twin-event-type.d.ts +3 -1
- package/dist-server/service/twin-event/twin-event-type.js +18 -1
- package/dist-server/service/twin-event/twin-event-type.js.map +1 -1
- package/dist-server/service/twin-event/twin-event.d.ts +1 -0
- package/dist-server/service/twin-event/twin-event.js +9 -1
- package/dist-server/service/twin-event/twin-event.js.map +1 -1
- package/dist-server/service/twin-forecast/twin-forecast-query.js +2 -1
- package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
- package/dist-server/service/twin-ingest-window/index.d.ts +5 -0
- package/dist-server/service/twin-ingest-window/index.js +10 -0
- package/dist-server/service/twin-ingest-window/index.js.map +1 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-query.d.ts +12 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js +123 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js.map +1 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.d.ts +1 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js +51 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js.map +1 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window.d.ts +16 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window.js +108 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window.js.map +1 -0
- package/dist-server/service/twin-instance/twin-instance.d.ts +1 -1
- package/dist-server/service/twin-instance/twin-instance.js +2 -2
- package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
- package/dist-server/service/twin-journal/twin-journal-query.js +58 -5
- package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +14 -0
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +74 -3
- 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 +78 -3
- package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
- package/dist-server/service/twin-model/axis-journal-evidence.d.ts +2 -0
- package/dist-server/service/twin-model/axis-journal-evidence.js +89 -0
- package/dist-server/service/twin-model/axis-journal-evidence.js.map +1 -0
- package/dist-server/service/twin-model/epcis-coverage.d.ts +1 -1
- package/dist-server/service/twin-model/epcis-coverage.js +19 -7
- package/dist-server/service/twin-model/epcis-coverage.js.map +1 -1
- package/dist-server/service/twin-model/iec61850-coverage.d.ts +1 -1
- package/dist-server/service/twin-model/iec61850-coverage.js +18 -7
- package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -1
- package/dist-server/service/twin-model/isa95-coverage.d.ts +14 -0
- package/dist-server/service/twin-model/isa95-coverage.js +23 -9
- package/dist-server/service/twin-model/isa95-coverage.js.map +1 -1
- package/dist-server/service/twin-model/item-ref.d.ts +24 -0
- package/dist-server/service/twin-model/item-ref.js +88 -0
- package/dist-server/service/twin-model/item-ref.js.map +1 -0
- package/dist-server/service/twin-model/name-index.d.ts +36 -0
- package/dist-server/service/twin-model/name-index.js +116 -0
- package/dist-server/service/twin-model/name-index.js.map +1 -0
- package/dist-server/service/twin-model/project-structure.js +1 -1
- package/dist-server/service/twin-model/project-structure.js.map +1 -1
- package/dist-server/service/twin-model/status-tally.d.ts +9 -0
- package/dist-server/service/twin-model/status-tally.js +37 -0
- package/dist-server/service/twin-model/status-tally.js.map +1 -0
- package/dist-server/service/twin-model/twin-lineage-query.js +40 -12
- package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-item-query.js +38 -39
- package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-query.js +157 -5
- package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-tree-query.js +7 -0
- package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
- package/dist-server/service/twin-readiness/index.d.ts +2 -0
- package/dist-server/service/twin-readiness/index.js +6 -0
- package/dist-server/service/twin-readiness/index.js.map +1 -0
- package/dist-server/service/twin-readiness/twin-readiness-query.d.ts +3 -0
- package/dist-server/service/twin-readiness/twin-readiness-query.js +103 -0
- package/dist-server/service/twin-readiness/twin-readiness-query.js.map +1 -0
- package/dist-server/service/twin-space/twin-space-resolver.d.ts +2 -2
- package/dist-server/service/twin-space/twin-space-resolver.js +5 -4
- package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
- package/dist-shared/entity-delta.d.ts +23 -3
- package/dist-shared/entity-delta.js +12 -8
- package/dist-shared/entity-delta.js.map +1 -1
- package/dist-shared/kpi-broadcast.js +1 -1
- package/dist-shared/kpi-broadcast.js.map +1 -1
- package/dist-shared/touched-items.d.ts +7 -0
- package/dist-shared/touched-items.js +80 -0
- package/dist-shared/touched-items.js.map +1 -0
- package/package.json +7 -7
- package/server/engine/canonical-ingest.ts +164 -15
- package/server/engine/command-routing.ts +1 -1
- package/server/engine/declared-stimulus.ts +66 -0
- package/server/engine/index.ts +6 -0
- package/server/engine/ingest-health.ts +704 -0
- package/server/engine/integration-coverage.ts +147 -0
- package/server/engine/integration-probes.ts +144 -0
- package/server/engine/integration-runner.ts +95 -0
- package/server/engine/integration-target-profile.ts +103 -0
- package/server/engine/kpi-fold.ts +101 -5
- package/server/engine/kpi-query.ts +137 -23
- package/server/engine/live-feed-registry.ts +4 -3
- package/server/engine/load-meter.ts +12 -3
- package/server/engine/local-declarations.ts +97 -19
- package/server/engine/log.ts +72 -0
- package/server/engine/loop-lag.ts +120 -0
- package/server/engine/measured-yield.ts +89 -0
- package/server/engine/model-gap.ts +168 -0
- package/server/engine/operation-basis.ts +33 -2
- package/server/engine/property-effects.ts +17 -0
- package/server/engine/restart-policy.ts +55 -0
- package/server/engine/runtime-key.ts +1 -1
- package/server/engine/spec-coverage.ts +23 -3
- package/server/engine/stage-path.ts +172 -0
- package/server/engine/twin-engine.ts +1318 -186
- package/server/index.ts +36 -3
- package/server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.ts +54 -0
- package/server/migrations/1786200000000-CarryLiveFeedCursor.ts +53 -0
- package/server/migrations/index.ts +7 -1
- package/server/service/index.ts +11 -0
- package/server/service/reference/control-routing.ts +62 -0
- package/server/service/reference/discovery-result.ts +1 -1
- package/server/service/reference/index.ts +7 -1
- package/server/service/reference/reference-adapter.ts +275 -5
- package/server/service/reference/reference-assessment.ts +215 -0
- package/server/service/reference/reference-live.ts +126 -14
- package/server/service/reference/reference-master.ts +64 -4
- package/server/service/reference/reference-probe.ts +264 -0
- package/server/service/reference/reference-progress-subscription.ts +73 -0
- package/server/service/reference/reference-progress.ts +95 -0
- package/server/service/reference/reference-resolver.ts +246 -19
- package/server/service/reference/twin-reference.ts +39 -1
- package/server/service/twin-audit/command-audit.ts +34 -1
- package/server/service/twin-audit/twin-audit-event.ts +20 -0
- package/server/service/twin-control/twin-control-mutation.ts +78 -30
- package/server/service/twin-event/journal-count.ts +53 -0
- package/server/service/twin-event/twin-event-keys.ts +16 -1
- package/server/service/twin-event/twin-event-type.ts +26 -2
- package/server/service/twin-event/twin-event.ts +7 -0
- package/server/service/twin-forecast/twin-forecast-query.ts +2 -1
- package/server/service/twin-ingest-window/index.ts +7 -0
- package/server/service/twin-ingest-window/twin-ingest-window-query.ts +125 -0
- package/server/service/twin-ingest-window/twin-ingest-window-writer.ts +57 -0
- package/server/service/twin-ingest-window/twin-ingest-window.ts +113 -0
- package/server/service/twin-instance/twin-instance.ts +16 -10
- package/server/service/twin-journal/twin-journal-query.ts +59 -5
- package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +79 -4
- package/server/service/twin-metrics/twin-metrics-query.ts +81 -3
- package/server/service/twin-model/axis-journal-evidence.ts +57 -0
- package/server/service/twin-model/epcis-coverage.ts +2 -8
- package/server/service/twin-model/iec61850-coverage.ts +2 -8
- package/server/service/twin-model/isa95-coverage.ts +22 -9
- package/server/service/twin-model/item-ref.ts +80 -0
- package/server/service/twin-model/name-index.ts +94 -0
- package/server/service/twin-model/project-structure.ts +1 -1
- package/server/service/twin-model/status-tally.ts +37 -0
- package/server/service/twin-model/twin-lineage-query.ts +38 -9
- package/server/service/twin-model/twin-model-item-query.ts +26 -27
- package/server/service/twin-model/twin-model-query.ts +156 -5
- package/server/service/twin-model/twin-model-tree-query.ts +7 -0
- package/server/service/twin-readiness/index.ts +3 -0
- package/server/service/twin-readiness/twin-readiness-query.ts +96 -0
- package/server/service/twin-space/twin-space-resolver.ts +5 -4
- package/shared/entity-delta.ts +31 -7
- package/shared/kpi-broadcast.ts +1 -1
- package/shared/touched-items.ts +73 -0
- package/test/adopt-structure-live.test.ts +7 -7
- package/test/axis-journal-evidence.test.ts +71 -0
- package/test/axis-read.test.ts +101 -1
- package/test/boot-resume.test.ts +93 -27
- package/test/broadcast-cost-baseline.test.ts +200 -0
- package/test/broadcast-period.test.ts +58 -0
- package/test/canonical-ingest-vocabularies.test.ts +36 -1
- package/test/canonical-quantity-door.test.ts +61 -0
- package/test/command-routing.test.ts +1 -1
- package/test/control-capability.test.ts +103 -0
- package/test/declared-location-types.test.ts +89 -0
- package/test/declared-stimulus.test.ts +88 -0
- package/test/discovery-result.test.ts +1 -1
- package/test/duration-estimators.test.ts +1 -1
- package/test/entity-delta.test.ts +2 -2
- package/test/ingest-bench.test.ts +3 -3
- package/test/ingest-health-engine.test.ts +248 -0
- package/test/ingest-health-wiring.test.ts +119 -0
- package/test/ingest-health.test.ts +306 -0
- package/test/ingest-history.test.ts +247 -0
- package/test/ingest-running-guard.test.ts +5 -5
- package/test/instance-cache-lifecycle.test.ts +1 -1
- package/test/integration-probes.test.ts +103 -0
- package/test/integration-runner.test.ts +95 -0
- package/test/item-ref.test.ts +78 -0
- package/test/journal-read-discipline.test.ts +177 -0
- package/test/journal-retention.test.ts +133 -0
- package/test/journal-sort-axis.test.ts +142 -0
- package/test/journal-write-door.test.ts +110 -0
- package/test/journal-write-trend.test.ts +115 -0
- package/test/kernel-kind-guard.test.ts +4 -4
- package/test/kpi-baseline-db.test.ts +1 -1
- package/test/kpi-fold.test.ts +90 -3
- package/test/kpi-query-bench.test.ts +3 -3
- package/test/lineage-survives-restart.test.ts +20 -4
- package/test/live-cursor-wiring.test.ts +87 -0
- package/test/live-feed-registry.test.ts +15 -7
- package/test/live-kernel-facts.test.ts +7 -7
- package/test/live-mirror-parity.test.ts +6 -0
- package/test/load-meter.test.ts +29 -15
- package/test/local-declarations.test.ts +109 -2
- package/test/log-stamp.test.ts +59 -0
- package/test/loop-lag.test.ts +82 -0
- package/test/measured-yield.test.ts +90 -0
- package/test/model-gap.test.ts +153 -0
- package/test/oee-accumulator.test.ts +4 -0
- package/test/operation-basis.test.ts +28 -1
- package/test/operational-vocabulary.test.ts +108 -0
- package/test/operations-capability-db.test.ts +4 -4
- package/test/project-structure-db.test.ts +1 -1
- package/test/projection-reaches-screen.test.ts +1 -1
- package/test/property-effects.test.ts +28 -0
- package/test/read-failure-visible.test.ts +145 -0
- package/test/reference-grounding.test.ts +70 -0
- package/test/resolve-ts-siblings.mjs +52 -0
- package/test/restart-policy.test.ts +111 -0
- package/test/resync-origin-site.test.ts +7 -1
- package/test/revision-axis.test.ts +93 -0
- package/test/runtime-key.test.ts +2 -2
- package/test/scale-twin-bench.test.ts +2 -2
- package/test/source-outcome-audit.test.ts +104 -0
- package/test/spec-coverage.test.ts +1 -1
- package/test/stage-path.test.ts +95 -0
- package/test/standard-coverage.test.ts +15 -3
- package/test/status-tally.test.ts +55 -0
- package/test/structure-revision-db.test.ts +31 -27
- package/test/tenant-registry-db.test.ts +3 -3
- package/test/time-range.test.ts +152 -0
- package/test/touched-items.test.ts +61 -0
- package/test/twin-event-keys.test.ts +10 -2
- package/test/twin-model-item-db.test.ts +5 -3
- package/test/twin-model-tree-db.test.ts +7 -7
- package/test/twin-origin-resync.test.ts +4 -4
- package/test/warm-start-seam.test.ts +4 -0
- package/test/yield-loop.test.ts +199 -0
- package/tsconfig.shared.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { twinError } from '../../engine/log.js'
|
|
2
|
+
import { Arg, Ctx, Mutation, Query, Resolver, Directive } from 'type-graphql'
|
|
2
3
|
|
|
3
4
|
import { validateScenario } from '@operato/twin-kernel'
|
|
4
5
|
|
|
@@ -9,6 +10,8 @@ import { getRepository } from '@things-factory/shell'
|
|
|
9
10
|
import { TwinEngine } from '../../engine/index.js'
|
|
10
11
|
import { TwinAuditEvent } from '../twin-audit/twin-audit-event.js'
|
|
11
12
|
import { buildCommandAudit } from '../twin-audit/command-audit.js'
|
|
13
|
+
/* 구동은 원본을 지난다(ADR-0029 §8) — 트윈 런타임을 직접 잡지 않는다. */
|
|
14
|
+
import { routeControl, controlCapabilityOf } from '../reference/control-routing.js'
|
|
12
15
|
|
|
13
16
|
/*
|
|
14
17
|
* 트윈 Command·Scenario 채널 — 상향 mutation.
|
|
@@ -40,7 +43,7 @@ export class TwinControlMutation {
|
|
|
40
43
|
const ack = await TwinEngine.dispatchCommand(domainId, instanceId, command)
|
|
41
44
|
/* 라이브 구조 변이(resource.add)는 저장 model 에도 반영 — 프로비저닝 편집기·재기동이 추가분을 잃지 않게. */
|
|
42
45
|
if (ack?.accepted && command?.type === 'resource.add') {
|
|
43
|
-
await TwinEngine.syncBoardEquipment(domainId, instanceId).catch(err =>
|
|
46
|
+
await TwinEngine.syncBoardEquipment(domainId, instanceId).catch(err => twinError('twin syncBoardEquipment fail', err))
|
|
44
47
|
}
|
|
45
48
|
/*
|
|
46
49
|
* 감사 기록 — 승인 게이트가 앞을 막고, 이 줄이 뒤를 설명한다. 둘 다 있어야 "사람 손을 거쳤다" 를
|
|
@@ -54,49 +57,94 @@ export class TwinControlMutation {
|
|
|
54
57
|
const repo = getRepository(TwinAuditEvent)
|
|
55
58
|
await repo.save(repo.create({ domain: context.state.domain, instanceId, ...audit }))
|
|
56
59
|
} catch (err) {
|
|
57
|
-
|
|
60
|
+
twinError(`[twin-control] command audit not recorded for "${instanceId}"`, err)
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
/* 거절 사유는 **언어 중립 코드**로 함께 돌려준다 — 화면이 `twin.cmderr.<code>` 로 사람 말을 만든다. */
|
|
61
64
|
return { accepted: !!ack?.accepted, error: ack?.error, errorCode: ack?.errorCode, errorParams: ack?.errorParams }
|
|
62
65
|
}
|
|
63
66
|
|
|
64
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* 구동 채널 — **원본을 지난다**(ADR-0029 §8).
|
|
69
|
+
*
|
|
70
|
+
* ── 무엇이 바뀌었나 (2026-08-19) ──────────────────────────────────────────
|
|
71
|
+
* 예전에는 이 문이 `TwinEngine.runtime(...).scenario` 를 **직접** 잡았다. 시뮬만 할 수 있는 일(자극·배속·
|
|
72
|
+
* 정지)이 계약 밖의 지름길로 들어온 셈이고, 실 시스템에는 그 자리가 아예 없었다. 이제 길은 하나다:
|
|
73
|
+
* 트윈 → 그 트윈의 원본 → 어댑터가 **선언한** `control` 능력. 선언하지 않은 원본에는 문이 열리지 않고,
|
|
74
|
+
* 그 사실을 사유 코드로 말한다(`not-declared`) — 실 WMS 가 정지되지 않는 것은 결손이 아니다.
|
|
75
|
+
*
|
|
76
|
+
* 반환을 Boolean 에서 객체로 바꿨다: 예전에는 거절 사유를 실을 자리가 없어 **로그로만** 남겼다.
|
|
77
|
+
* 화면은 왜 안 되는지 알 수 없었다.
|
|
78
|
+
*/
|
|
65
79
|
@Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
|
|
66
|
-
@Mutation(returns =>
|
|
67
|
-
|
|
80
|
+
@Mutation(returns => ScalarObject, {
|
|
81
|
+
description:
|
|
82
|
+
"Drive the source behind a twin: stimulus | start | pause | speed | reset. The command goes through the source adapter's declared control capability — a source that does not declare it refuses with code 'not-declared' (a real system is not paused or seeded, and that is a fact rather than a gap). Returns { ok, code?, detail? }."
|
|
83
|
+
})
|
|
84
|
+
async controlTwinSource(
|
|
68
85
|
@Arg('instanceId') instanceId: string,
|
|
69
86
|
@Arg('action') action: string,
|
|
70
87
|
@Ctx() context: ResolverContext,
|
|
71
|
-
@Arg('scenario', type => ScalarObject, { nullable: true }) scenario?: any
|
|
72
|
-
|
|
88
|
+
@Arg('scenario', type => ScalarObject, { nullable: true }) scenario?: any,
|
|
89
|
+
@Arg('speed', { nullable: true }) speed?: number
|
|
90
|
+
): Promise<any> {
|
|
73
91
|
const domainId = context.state.domain?.id
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
92
|
+
if (!domainId) return { ok: false, code: 'no-domain' }
|
|
93
|
+
const allowed = ['stimulus', 'start', 'pause', 'speed', 'reset']
|
|
94
|
+
if (!allowed.includes(action)) return { ok: false, code: 'unsupported-action', detail: `one of ${allowed.join(' | ')}` }
|
|
77
95
|
/*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
96
|
+
* **잘못된 선언은 문 앞에서 막는다.** 예전에는 검증 없이 실었고, 다음 tick 에서 `rate.meanPerHour` 를
|
|
97
|
+
* 읽다 터져 **서버 프로세스가 내려갔다**(클라이언트가 보낸 값 하나로 앱 전체가 죽었다). 규칙은 커널이
|
|
98
|
+
* 들고 있다(계약의 주인) — 호스트가 따로 만들면 방언이 생긴다.
|
|
80
99
|
*/
|
|
81
|
-
if (
|
|
100
|
+
if (action === 'stimulus') {
|
|
101
|
+
const v = validateScenario(scenario)
|
|
102
|
+
if (v.ok === false) return { ok: false, code: 'invalid', detail: v.errorCode, params: v.errorParams }
|
|
103
|
+
}
|
|
104
|
+
if (action === 'speed' && !(Number(speed) > 0)) return { ok: false, code: 'invalid', detail: 'speed must be greater than 0' }
|
|
105
|
+
|
|
82
106
|
/*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
107
|
+
* **구동도 사람이 한 일이다 — 남긴다** (ADR-0029 §1 결정 C, 2026-08-19).
|
|
108
|
+
*
|
|
109
|
+
* 이 문은 감사를 아예 남기지 않았다. 그런데 자극을 바꾸고 배속을 올리고 원본을 멈추는 것은 그 트윈의
|
|
110
|
+
* 사실을 바꾸는 행위다 — 커맨드 채널은 남기는데 이쪽이 비어 있으면, 나중에 「무엇이 이 공장을
|
|
111
|
+
* 흔들었나」에 답할 수 없다.
|
|
112
|
+
*
|
|
113
|
+
* 결정 C 그대로: 게이트는 여기 남고(위 `@privilege`), **원본의 답을 같은 행에 닫는다.** 답을 못 받으면
|
|
114
|
+
* `unknown` 이다 — 성공으로 적지 않는다.
|
|
86
115
|
*/
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return false
|
|
93
|
-
}
|
|
116
|
+
let result: { ok: boolean; code?: string; detail?: string }
|
|
117
|
+
try {
|
|
118
|
+
result = await routeControl(domainId, instanceId, { action: action as any, scenario, speed: speed != null ? Number(speed) : undefined })
|
|
119
|
+
} catch (e: any) {
|
|
120
|
+
result = { ok: false, code: 'failed', detail: e?.message ?? 'control failed' }
|
|
94
121
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
122
|
+
try {
|
|
123
|
+
const audit = buildCommandAudit({
|
|
124
|
+
/* 커맨드 채널과 같은 모양으로 적는다 — 감사를 읽는 쪽이 두 어휘를 배우지 않게. */
|
|
125
|
+
command: { type: `source.${action}`, origin: 'user', args: { ...(scenario ? { scenario } : {}), ...(speed != null ? { speed } : {}) } },
|
|
126
|
+
/* 게이트는 통과했다(여기까지 왔다) — 그 사실과 원본의 답은 다른 칸이다. */
|
|
127
|
+
ack: { accepted: true },
|
|
128
|
+
source: { answered: typeof result?.ok === 'boolean', ok: result?.ok, code: result?.code },
|
|
129
|
+
actor: context.state.user,
|
|
130
|
+
at: new Date()
|
|
131
|
+
})
|
|
132
|
+
const repo = getRepository(TwinAuditEvent)
|
|
133
|
+
await repo.save(repo.create({ domain: context.state.domain, instanceId, ...audit }))
|
|
134
|
+
} catch (err) {
|
|
135
|
+
/* 기록이 실패해도 결과는 돌려준다 — 다만 삼키지 않는다(「기록 없음」과 「일 없음」을 구별해야 한다). */
|
|
136
|
+
twinError(`[twin-control] source control audit not recorded for "${instanceId}"`, err)
|
|
137
|
+
}
|
|
138
|
+
return result
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 이 트윈의 원본이 **무엇을 받을 수 있나** — 화면이 버튼을 열기 전에 묻는 자리. */
|
|
142
|
+
@Query(returns => ScalarObject, {
|
|
143
|
+
description: "What the source behind this twin can do: its declared capabilities ('live', 'control'). Empty when the twin has no source or its adapter declares nothing."
|
|
144
|
+
})
|
|
145
|
+
async twinSourceCapability(@Arg('instanceId') instanceId: string, @Ctx() context: ResolverContext): Promise<any> {
|
|
146
|
+
const domainId = context.state.domain?.id
|
|
147
|
+
if (!domainId) return { capabilities: [] }
|
|
148
|
+
return controlCapabilityOf(domainId, instanceId)
|
|
101
149
|
}
|
|
102
150
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 저널을 **상한까지만 센다** — 세는 것도 읽는 것이다 (2026-08-22).
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 (실측) ───────────────────────────────────────────────────────────────
|
|
5
|
+
* 조건에 맞는 행을 전부 세는 질의는 행을 실어 오지 않아도 비싸다. 개발 저널에서 트윈 하나가 358만
|
|
6
|
+
* 행이었고 그 한 번이 **5.32초**였다. 색인만 상한까지 읽으면 같은 조건에서 **0.04초**다(130배).
|
|
7
|
+
*
|
|
8
|
+
* 그 5초 동안 이벤트 루프는 다른 요청을 처리하지 못한다 — 이력 화면 한 번, 준비도 화면 한 번이
|
|
9
|
+
* 사용자에게는 「눌렀는데 아무 반응이 없다」로 나타난다. 준비도 화면은 그런 세기를 **넷** 했다.
|
|
10
|
+
*
|
|
11
|
+
* ── 규율 ────────────────────────────────────────────────────────────────────
|
|
12
|
+
* 세는 목적이 둘 중 하나다.
|
|
13
|
+
* ① **판정** — 「한 건이라도 있나」. 그때는 하나만 찾으면 끝이다(`anyJournalRow`).
|
|
14
|
+
* ② **표시** — 사람에게 수를 보인다. 그때는 상한까지 세고, 넘으면 **모른다고 말한다**(`countUpTo`).
|
|
15
|
+
*
|
|
16
|
+
* 상한을 넘었을 때 상한값을 전체로 적으면 화면이 거짓을 말한다. 그래서 `capped` 를 함께 낸다 —
|
|
17
|
+
* 없는 것과 모르는 것을 가르는 이 프로젝트의 규율이 세기에도 같이 적용된다.
|
|
18
|
+
*/
|
|
19
|
+
import type { ObjectLiteral, Repository } from 'typeorm'
|
|
20
|
+
|
|
21
|
+
/** 표시용 세기의 기본 상한 — 화면이 한 번에 보여 줄 범위를 훨씬 넘는 값. */
|
|
22
|
+
export const JOURNAL_COUNT_CAP = 10_000
|
|
23
|
+
|
|
24
|
+
export interface CappedCount {
|
|
25
|
+
/** 센 수. `capped` 면 상한까지만 센 값이므로 **전체가 아니다**. */
|
|
26
|
+
count: number
|
|
27
|
+
/** 상한을 넘었나 — 그때 전체 수는 모르는 것이 사실이다. */
|
|
28
|
+
capped: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 상한까지만 세기 — 키 컬럼만 읽으므로 행 본문을 만들지 않는다.
|
|
33
|
+
*
|
|
34
|
+
* `count()` 를 쓰지 않는 이유: 그것은 상한을 받지 않는다(조건에 맞는 전부를 센다).
|
|
35
|
+
*/
|
|
36
|
+
export async function countUpTo<T extends ObjectLiteral>(
|
|
37
|
+
repo: Repository<T>,
|
|
38
|
+
where: any,
|
|
39
|
+
cap = JOURNAL_COUNT_CAP
|
|
40
|
+
): Promise<CappedCount> {
|
|
41
|
+
const rows = await repo.find({ where, select: ['id'] as any, take: cap + 1 })
|
|
42
|
+
return rows.length > cap ? { count: cap, capped: true } : { count: rows.length, capped: false }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 한 건이라도 있나 — **판정에는 수가 필요 없다.**
|
|
47
|
+
*
|
|
48
|
+
* 예전에는 전부 센 뒤 `> 0` 을 봤다. 그 답을 얻는 데 358만 행을 세고 있었다.
|
|
49
|
+
*/
|
|
50
|
+
export async function anyJournalRow<T extends ObjectLiteral>(repo: Repository<T>, where: any): Promise<boolean> {
|
|
51
|
+
const rows = await repo.find({ where, select: ['id'] as any, take: 1 })
|
|
52
|
+
return rows.length > 0
|
|
53
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { twinWarn } from '../../engine/log.js'
|
|
1
2
|
/*
|
|
2
3
|
* 저널 검색 키 추출 — **순수**. 인제스트가 기록할 때 한 번 뽑아 인덱스 가능한 실컬럼으로 승격한다.
|
|
3
4
|
*
|
|
@@ -18,6 +19,7 @@
|
|
|
18
19
|
|
|
19
20
|
/** 승격된 검색 키 — 전부 선택적. 뽑히지 않으면 **빈 문자열이 아니라 undefined**(결측≠빈값). */
|
|
20
21
|
export interface TwinEventKeys {
|
|
22
|
+
action?: string
|
|
21
23
|
bizStep?: string
|
|
22
24
|
epc?: string
|
|
23
25
|
orderId?: string
|
|
@@ -37,7 +39,7 @@ function clip(v: unknown, field: string): string | undefined {
|
|
|
37
39
|
const s = String(v)
|
|
38
40
|
if (!s) return undefined
|
|
39
41
|
if (s.length <= MAX_KEY) return s
|
|
40
|
-
|
|
42
|
+
twinWarn(
|
|
41
43
|
`[twin-event-keys] ${field} exceeds ${MAX_KEY} chars and was clipped for indexing — ` +
|
|
42
44
|
`search on this value may be incomplete. payload keeps the full value. (${s.slice(0, 60)}…)`
|
|
43
45
|
)
|
|
@@ -90,9 +92,22 @@ export function equipmentIdOf(envelope: any): string | undefined {
|
|
|
90
92
|
return d.moverId ?? undefined
|
|
91
93
|
}
|
|
92
94
|
|
|
95
|
+
/**
|
|
96
|
+
* EPCIS 행위 — `ADD`·`OBSERVE`·`DELETE`. 세 값이 아니면 `undefined`(원천의 잡값을 색인에 넣지 않는다).
|
|
97
|
+
*
|
|
98
|
+
* 이 키는 검색 축이 아니라 **판별식**이다: 되풀어 읽는 스냅샷은 원리적으로 `DELETE` 를 못 낸다.
|
|
99
|
+
* 그래서 「이 트윈이 반출을 한 번이라도 접었는가」가 진짜 delta 피드의 증거인데, payload 안에 있는
|
|
100
|
+
* 동안에는 5개 드라이버 어디에서도 그 질문을 할 수 없었다. 운영 델타에는 행위가 없다 — 비운다.
|
|
101
|
+
*/
|
|
102
|
+
export function actionOf(envelope: any): string | undefined {
|
|
103
|
+
const a = (envelope?.data ?? envelope ?? {}).action
|
|
104
|
+
return a === 'ADD' || a === 'OBSERVE' || a === 'DELETE' ? a : undefined
|
|
105
|
+
}
|
|
106
|
+
|
|
93
107
|
/** 한 이벤트에서 승격 키 전부 — 기록 경로가 이 함수 하나만 부른다. */
|
|
94
108
|
export function twinEventKeys(envelope: any): TwinEventKeys {
|
|
95
109
|
return {
|
|
110
|
+
action: actionOf(envelope),
|
|
96
111
|
bizStep: clip(bizStepOf(envelope), 'bizStep'),
|
|
97
112
|
epc: clip(epcOf(envelope), 'epc'),
|
|
98
113
|
orderId: clip(orderOf(envelope), 'orderId'),
|
|
@@ -14,8 +14,32 @@ export class TwinEventList {
|
|
|
14
14
|
@Field(type => [TwinEvent], { description: 'The events on this page, ordered by the requested sorting (revision descending by default).' })
|
|
15
15
|
items: TwinEvent[]
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
/*
|
|
18
|
+
* 전체 수 — **상한을 넘으면 비운다.**
|
|
19
|
+
*
|
|
20
|
+
* 세는 데도 값이 든다(2026-08-22 실측: 저널 358만 행인 트윈에서 전량 세기 5.32초 · 상한 1만까지
|
|
21
|
+
* 0.04초). 그래서 상한까지만 세고, 넘으면 「모른다」로 둔다. 그때 상한값을 전체로 적으면 화면이
|
|
22
|
+
* 거짓을 말한다 — 없는 것과 모르는 것을 가르는 것이 이 필드의 규율이다.
|
|
23
|
+
*/
|
|
24
|
+
@Field(type => Int, {
|
|
25
|
+
nullable: true,
|
|
26
|
+
description:
|
|
27
|
+
'Total number of events matching the filters, ignoring pagination. Lets the caller show an honest "shown of total" count instead of silently truncating. NULL when the server stopped counting at countCap: the exact total is then unknown, and capped says so — do not render the cap as if it were the total.'
|
|
28
|
+
})
|
|
29
|
+
total?: number
|
|
30
|
+
|
|
31
|
+
@Field({
|
|
32
|
+
nullable: true,
|
|
33
|
+
description:
|
|
34
|
+
'True when there are more matching events than countCap, so total could not be established. The caller should say "first N shown" and offer narrowing rather than implying it received everything.'
|
|
35
|
+
})
|
|
36
|
+
capped?: boolean
|
|
37
|
+
|
|
38
|
+
@Field(type => Int, {
|
|
39
|
+
nullable: true,
|
|
40
|
+
description: 'The counting limit the server applied. Reported so the screen can say how far the count is trustworthy instead of guessing.'
|
|
41
|
+
})
|
|
42
|
+
countCap?: number
|
|
19
43
|
|
|
20
44
|
/*
|
|
21
45
|
* 다음 페이지 커서. 저널은 **머리에 계속 쌓이는 목록**이라 offset 으로 뒤를 읽으면
|
|
@@ -34,6 +34,8 @@ import { Domain, ScalarObject } from '@things-factory/shell'
|
|
|
34
34
|
@Index('ix_twin_event_4', (e: TwinEvent) => [e.domain, e.instanceId, e.orderId], { unique: false })
|
|
35
35
|
/* ix_5 — "이 지시가 무엇을 일으켰나". 감사 기록의 commandId 로 저널을 다시 계산하는 축. */
|
|
36
36
|
@Index('ix_twin_event_5', (e: TwinEvent) => [e.domain, e.correlationId], { unique: false })
|
|
37
|
+
/* ix_6 — "반출을 한 번이라도 접었나". 없으면 그 질문이 인스턴스 저널 전량 주사가 된다. */
|
|
38
|
+
@Index('ix_twin_event_6', (e: TwinEvent) => [e.domain, e.instanceId, e.action], { unique: false })
|
|
37
39
|
@ObjectType({ description: 'Append-only twin event journal record (EPCIS event or operational delta).' })
|
|
38
40
|
export class TwinEvent {
|
|
39
41
|
@PrimaryGeneratedColumn('uuid')
|
|
@@ -132,6 +134,11 @@ export class TwinEvent {
|
|
|
132
134
|
@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.' })
|
|
133
135
|
correlationId?: string
|
|
134
136
|
|
|
137
|
+
/* 옛 행은 비어 있다 — 승격 전에 기록된 것이라 **모른다**(0 이나 OBSERVE 로 위장하지 않는다). */
|
|
138
|
+
@Column({ length: 16, nullable: true })
|
|
139
|
+
@Field({ nullable: true, description: 'EPCIS action (ADD / OBSERVE / DELETE), promoted from the payload. Empty for operational deltas, and for rows journaled before this column existed.' })
|
|
140
|
+
action?: string
|
|
141
|
+
|
|
135
142
|
@Column({ length: 255, nullable: true })
|
|
136
143
|
@Field({ nullable: true, description: 'Business step (CBV bizStep tail), promoted from the payload for indexed filtering.' })
|
|
137
144
|
bizStep?: string
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { twinLog } from '../../engine/log.js'
|
|
1
2
|
import { Arg, Ctx, Int, Mutation, Query, Resolver, Directive} from 'type-graphql'
|
|
2
3
|
|
|
3
4
|
import { getRepository, ScalarObject } from '@things-factory/shell'
|
|
@@ -193,7 +194,7 @@ export class TwinForecastQuery {
|
|
|
193
194
|
const basis = await TwinEngine.modelBasis(domainId, instanceId)
|
|
194
195
|
const stale = !!stored && modelBasisChanged(stored.basis, basis)
|
|
195
196
|
if (stale) {
|
|
196
|
-
|
|
197
|
+
twinLog(
|
|
197
198
|
`[twin-forecast] "${instanceId}": calibration not applied — model changed since it was learned.` +
|
|
198
199
|
` learned on: ${describeModelBasis(stored!.basis)} / now: ${describeModelBasis(basis)}`
|
|
199
200
|
)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { TwinIngestWindow } from './twin-ingest-window.js'
|
|
2
|
+
import { TwinIngestWindowQuery } from './twin-ingest-window-query.js'
|
|
3
|
+
|
|
4
|
+
export const entities = [TwinIngestWindow]
|
|
5
|
+
export const resolvers = [TwinIngestWindowQuery]
|
|
6
|
+
|
|
7
|
+
export { installIngestWindowWriter } from './twin-ingest-window-writer.js'
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Arg, Ctx, Int, Query, Resolver } from 'type-graphql'
|
|
2
|
+
import { Between } from 'typeorm'
|
|
3
|
+
|
|
4
|
+
import { ScalarObject, getRepository } from '@things-factory/shell'
|
|
5
|
+
|
|
6
|
+
import { TwinEngine } from '../../engine/index.js'
|
|
7
|
+
import { TwinIngestWindow } from './twin-ingest-window.js'
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* 인제스트 창 **이력** — 재기동 뒤에도 추이를 이어 본다.
|
|
11
|
+
*
|
|
12
|
+
* ── 왜 따로 있나 (2026-08-20) ───────────────────────────────────────────────
|
|
13
|
+
* 창을 표로 남기기 시작했는데 **읽는 코드가 없었다.** 21행이 쌓여 있고 아무도 보지 않았다 — 이
|
|
14
|
+
* 프로젝트가 결함으로 보는 상태다(「필드만 만들고 아무도 읽지 않는다」). 표를 만든 이유가 「메모리
|
|
15
|
+
* 장부는 재기동에 사라지고 6시간까지인데 추이는 그보다 오래 봐야 한다」였으니, 목적이 미완이었다.
|
|
16
|
+
*
|
|
17
|
+
* ── 왜 `twinIngestHealth` 에 합치지 않나 ────────────────────────────────────
|
|
18
|
+
* 그 조회는 **화면이 자주 묻는다**(상세를 열 때마다, 목록은 응답에 요약이 함께 온다). 거기에 DB 주사를
|
|
19
|
+
* 얹으면 이 세션에서 없앤 것과 같은 종류의 비용이 돌아온다 — 자주 묻는 조회가 자라는 표를 읽는 것.
|
|
20
|
+
* 이력은 **다른 질문**이고 덜 자주 묻는다: 「더 뒤까지 보여 달라」.
|
|
21
|
+
*
|
|
22
|
+
* ── 이어 붙이기는 서버가 한다 ───────────────────────────────────────────────
|
|
23
|
+
* 표와 메모리는 겹친다(프로세스가 살아 있는 동안 닫힌 창은 둘 다에 있다). 겹침을 화면이 정리하게 두면
|
|
24
|
+
* 화면마다 규칙이 갈리고, 같은 구간이 두 번 그려지거나 사라진다. 그래서 여기서 한 벌로 이어 붙이고
|
|
25
|
+
* **어디서부터 메모리인지**(`liveFrom`)를 함께 낸다 — 화면이 근거를 말할 수 있게.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 한 번에 돌려주는 창 수 상한 — 144창 × 10분 = 24시간.
|
|
30
|
+
*
|
|
31
|
+
* 상한이 필요한 이유는 이 표가 **자란다**는 것이다(트윈 1,000개에 하루 14.4만 행). 상한에서 밀려난
|
|
32
|
+
* 수는 **세어서 낸다** — 조용히 자르면 화면이 「그 앞에는 아무 일도 없었다」로 읽는다.
|
|
33
|
+
*/
|
|
34
|
+
export const HISTORY_WINDOW_LIMIT = 144
|
|
35
|
+
|
|
36
|
+
/** 물을 수 있는 기간의 상한(시간). 넘겨도 여기서 자르고, 자른 사실을 낸다. */
|
|
37
|
+
export const HISTORY_HOURS_MAX = 168
|
|
38
|
+
|
|
39
|
+
@Resolver()
|
|
40
|
+
export class TwinIngestWindowQuery {
|
|
41
|
+
@Query(returns => ScalarObject, {
|
|
42
|
+
nullable: true,
|
|
43
|
+
description:
|
|
44
|
+
'Ingest windows for one twin over a past span, stitched from two sources so the trend survives a restart: closed windows persisted in the table (older) and the in-memory ledger (recent, roughly the last 6 hours). Overlap is resolved here rather than on screen — a window present in both is reported once, from memory, and `liveFrom` says where memory takes over so a screen can state its basis. Each window carries offered / accepted / rejected / undelivered plus the journal rows written and the write round-trips behind them; `acceptedRatio` is null when nothing was offered, because an empty window is not a perfect one. Deliberately separate from twinIngestHealth: that query is polled whenever a screen opens, and making it scan a growing table would reintroduce the cost this codebase just removed elsewhere. Counts that could not be returned are reported rather than dropped — `storedDropped` when the row cap was hit and `hoursClamped` when the requested span was longer than allowed. Null if the instance is not in the current domain.'
|
|
45
|
+
})
|
|
46
|
+
async twinIngestHistory(
|
|
47
|
+
@Arg('instanceId') instanceId: string,
|
|
48
|
+
@Arg('hours', type => Int, { nullable: true, description: 'How far back to read, in hours. Defaults to 24 and is clamped to 168.' })
|
|
49
|
+
hours: number | undefined,
|
|
50
|
+
@Ctx() context: ResolverContext
|
|
51
|
+
): Promise<any> {
|
|
52
|
+
const domainId = context.state.domain.id
|
|
53
|
+
/* 테넌트 격리 — 멈춘 트윈의 이력도 볼 수 있어야 한다(그때가 가장 보고 싶은 때다). */
|
|
54
|
+
if (!TwinEngine.owns(domainId, instanceId) && !(await TwinEngine.ownsRegistered(domainId, instanceId))) return null
|
|
55
|
+
|
|
56
|
+
const asked = Number.isFinite(Number(hours)) && Number(hours) > 0 ? Math.floor(Number(hours)) : 24
|
|
57
|
+
const span = Math.min(asked, HISTORY_HOURS_MAX)
|
|
58
|
+
const now = Date.now()
|
|
59
|
+
const fromMs = now - span * 3_600_000
|
|
60
|
+
|
|
61
|
+
/*
|
|
62
|
+
* 표에서 그 구간의 닫힌 창을 읽는다. **상한을 둔다** — 자라는 표이고, 이 조회가 화면에서 열린다.
|
|
63
|
+
* 최신 쪽이 중요하므로 내려서 정렬해 가져오고, 낼 때 다시 올린다.
|
|
64
|
+
*/
|
|
65
|
+
const repo = getRepository(TwinIngestWindow)
|
|
66
|
+
const where = {
|
|
67
|
+
domain: { id: domainId } as any,
|
|
68
|
+
instanceId,
|
|
69
|
+
closedAt: Between(new Date(fromMs), new Date(now))
|
|
70
|
+
}
|
|
71
|
+
const rows = await repo.find({ where, order: { closedAt: 'DESC' }, take: HISTORY_WINDOW_LIMIT })
|
|
72
|
+
|
|
73
|
+
/*
|
|
74
|
+
* 상한에 닿았으면 **몇 개가 밀렸는지 센다.** 「더 있음」만 말하면 화면이 그 앞을 짐작하게 된다.
|
|
75
|
+
* 상한에 닿지 않았으면 셀 필요가 없다(왕복을 아낀다).
|
|
76
|
+
*/
|
|
77
|
+
const storedDropped = rows.length === HISTORY_WINDOW_LIMIT ? Math.max(0, (await repo.count({ where })) - rows.length) : 0
|
|
78
|
+
|
|
79
|
+
const stored = rows.reverse().map(r => ({
|
|
80
|
+
from: r.openedAt?.toISOString?.() ?? new Date(r.openedAt as any).toISOString(),
|
|
81
|
+
to: r.closedAt?.toISOString?.() ?? new Date(r.closedAt as any).toISOString(),
|
|
82
|
+
offered: r.offered,
|
|
83
|
+
/* 통과는 **트윈에 닿은** 것만. 표의 값을 그대로 쓴다 — 읽는 쪽에서 다시 나누면 반올림이 갈린다. */
|
|
84
|
+
accepted: r.offered - r.rejected - (r.undelivered ?? 0),
|
|
85
|
+
rejected: r.rejected,
|
|
86
|
+
undelivered: r.undelivered ?? 0,
|
|
87
|
+
acceptedRatio: typeof r.acceptedRatio === 'number' ? r.acceptedRatio : null,
|
|
88
|
+
rows: r.journaledRows ?? 0,
|
|
89
|
+
flushes: r.journalFlushes ?? 0,
|
|
90
|
+
source: 'stored' as const
|
|
91
|
+
}))
|
|
92
|
+
|
|
93
|
+
/* 메모리 장부의 닫힌 창 — 이쪽이 최신이고, 겹치는 구간은 이쪽을 쓴다(같은 사실의 원본이다). */
|
|
94
|
+
const live = (TwinEngine.ingestHealthOf(domainId, instanceId).trend ?? []).map((w: any) => ({
|
|
95
|
+
...w,
|
|
96
|
+
source: 'live' as const
|
|
97
|
+
}))
|
|
98
|
+
|
|
99
|
+
/*
|
|
100
|
+
* 겹침 정리 — 창의 **여는 시각**이 키다. 같은 창이 표와 메모리에 둘 다 있으면 하나만 낸다.
|
|
101
|
+
* 시각 문자열을 그대로 키로 쓰지 않는다: 드라이버가 밀리초를 자르거나 형식을 바꿀 수 있어
|
|
102
|
+
* 같은 창이 다른 키가 된다. epoch 로 견준다.
|
|
103
|
+
*/
|
|
104
|
+
const liveStarts = new Set(live.map((w: any) => Date.parse(w.from)))
|
|
105
|
+
const windows = [...stored.filter(w => !liveStarts.has(Date.parse(w.from))), ...live]
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
windows,
|
|
109
|
+
hours: span,
|
|
110
|
+
/* 물은 것보다 짧게 잘렸으면 밝힌다. */
|
|
111
|
+
...(span < asked ? { hoursClamped: asked } : {}),
|
|
112
|
+
from: new Date(fromMs).toISOString(),
|
|
113
|
+
to: new Date(now).toISOString(),
|
|
114
|
+
/** 표에서 읽은 창 수 · 상한에서 밀려난 수. */
|
|
115
|
+
storedCount: stored.length,
|
|
116
|
+
storedDropped,
|
|
117
|
+
/**
|
|
118
|
+
* 어디서부터 메모리인가 — 화면이 근거를 말할 수 있게. 메모리에 닫힌 창이 없으면 `null` 이다
|
|
119
|
+
* (재기동 직후가 그렇다: 그때 보이는 것은 전부 표에서 온 것이다).
|
|
120
|
+
*/
|
|
121
|
+
liveFrom: live.length ? live[0].from : null,
|
|
122
|
+
liveCount: live.length
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { twinWarn } from '../../engine/log.js'
|
|
2
|
+
import { getRepository } from '@things-factory/shell'
|
|
3
|
+
|
|
4
|
+
import { TwinEngine } from '../../engine/index.js'
|
|
5
|
+
import { TwinIngestWindow } from './twin-ingest-window.js'
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* 닫힌 창을 표로 옮긴다 — **영속이 붙는 유일한 자리.**
|
|
9
|
+
*
|
|
10
|
+
* 커널 층(`engine/ingest-health.ts`)은 저장 계층을 모른다. 창이 닫힐 때 부를 곳을 엔진이 문으로 열어
|
|
11
|
+
* 두었고(`onIngestWindowClosed`), 이 파일이 부팅에서 한 번 등록한다. 그래서 기록 경로가 갈리지 않는다.
|
|
12
|
+
*
|
|
13
|
+
* ── 실패를 삼키지 않는다 ────────────────────────────────────────────────────
|
|
14
|
+
* 쓰기가 터져도 메모리 장부는 계속 굴러간다(`rollIngestWindow` 가 훅을 감싼다). 다만 **조용히 넘기지는
|
|
15
|
+
* 않는다** — 실패를 로그로 남긴다. 그러지 않으면 추이 표에 구멍이 생긴 이유를 나중에 알 수 없다.
|
|
16
|
+
*
|
|
17
|
+
* ── 왜 await 하지 않나 ──────────────────────────────────────────────────────
|
|
18
|
+
* 이 훅은 인제스트 경로에서 불린다(라이브 피드의 콜백 안). 여기서 DB 를 기다리면 **유입이 DB 속도에
|
|
19
|
+
* 묶인다** — 이 기능이 막으려던 부하가 그 모양으로 돌아온다. 그래서 띄워 보내고 결과만 로그로 받는다.
|
|
20
|
+
*/
|
|
21
|
+
export function installIngestWindowWriter(): void {
|
|
22
|
+
TwinEngine.onIngestWindowClosed((at, closed) => {
|
|
23
|
+
const repo = getRepository(TwinIngestWindow)
|
|
24
|
+
const offered = closed.offered
|
|
25
|
+
repo
|
|
26
|
+
.save(
|
|
27
|
+
repo.create({
|
|
28
|
+
domain: { id: at.domainId } as any,
|
|
29
|
+
instanceId: at.instanceId,
|
|
30
|
+
openedAt: new Date(closed.startMs),
|
|
31
|
+
closedAt: new Date(closed.endMs),
|
|
32
|
+
offered,
|
|
33
|
+
rejected: closed.rejected,
|
|
34
|
+
undelivered: closed.undelivered,
|
|
35
|
+
/* 적힌 것도 같은 행에 — 창이 유입 없이 닫혔어도(시뮬) 이 두 수는 사실이다. */
|
|
36
|
+
journaledRows: closed.rows,
|
|
37
|
+
journalFlushes: closed.flushes,
|
|
38
|
+
/*
|
|
39
|
+
* 제시가 0 이면 비워 둔다 — 1 로 채우면 「전부 통과」로 읽힌다.
|
|
40
|
+
* 통과는 **트윈에 닿은** 것만이다: 버려진 것을 빼지 않으면 멈춘 트윈의 창이 100% 로 남는다.
|
|
41
|
+
*/
|
|
42
|
+
...(offered > 0
|
|
43
|
+
? { acceptedRatio: round4((offered - closed.rejected - closed.undelivered) / offered) }
|
|
44
|
+
: {})
|
|
45
|
+
})
|
|
46
|
+
)
|
|
47
|
+
.catch(err =>
|
|
48
|
+
twinWarn(
|
|
49
|
+
`[twin-ingest-window] "${at.instanceId}" window not persisted (trend will have a gap here):`,
|
|
50
|
+
err?.message ?? err
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 0.999 가 1 로 반올림되면 「전부 통과」로 읽힌다 — 자릿수를 넉넉히 준다(메모리 장부와 같은 규칙). */
|
|
57
|
+
const round4 = (v: number) => Math.round(v * 10000) / 10000
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { CreateDateColumn, Entity, Index, Column, RelationId, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'
|
|
2
|
+
import { ObjectType, Field, ID, Int, Float } from 'type-graphql'
|
|
3
|
+
|
|
4
|
+
import { Domain } from '@things-factory/shell'
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* 인제스트 창 하나 — **동기화 추이를 재기동 뒤에도 이어 보기 위한 행.**
|
|
8
|
+
*
|
|
9
|
+
* ── 왜 창마다이고 건마다가 아닌가 ───────────────────────────────────────────
|
|
10
|
+
* 거부마다 행을 쓰면 라이브 계측 규모에서 초당 1만 행이고, 그 부하가 이벤트 루프로 돌아온다. 창마다면
|
|
11
|
+
* 트윈 1,000개에 하루 14.4만 행(약 14MB)이다. 그래서 **닫힌 창 하나가 영속의 단위**다.
|
|
12
|
+
*
|
|
13
|
+
* 건마다 남기는 자리는 이미 따로 있다: 시나리오 경로가 `PayloadLog` 로 dead-letter 를 남긴다(배치성
|
|
14
|
+
* 유입이라 건수가 감당된다). 이 표는 그것을 대신하지 않고 **추이**를 답한다.
|
|
15
|
+
*
|
|
16
|
+
* ── 왜 사유·표본은 여기 없나 ────────────────────────────────────────────────
|
|
17
|
+
* 사유 문구는 길고 표본에는 **고객의 레코드 원문**이 들어 있다. 그것을 창마다 영속하면 표가 커지고
|
|
18
|
+
* 고객 데이터가 오래 남는다. 사유와 표본은 메모리 장부가 답하고(최근 6시간), 이 표는 숫자만 든다.
|
|
19
|
+
*
|
|
20
|
+
* ── 고치지 않는다 ───────────────────────────────────────────────────────────
|
|
21
|
+
* 닫힌 창은 지나간 사실이다. 그래서 `updatedAt`·`deletedAt` 을 두지 않는다(저널과 같은 규율).
|
|
22
|
+
*/
|
|
23
|
+
@Entity()
|
|
24
|
+
@Index('ix_twin_ingest_window_0', (e: TwinIngestWindow) => [e.domain, e.instanceId, e.closedAt], { unique: false })
|
|
25
|
+
@ObjectType({
|
|
26
|
+
description:
|
|
27
|
+
'One closed ingest window for a twin: how many records were offered, accepted and rejected between two times. Written when a window closes (about every 10 minutes per twin) so the sync trend survives a restart and reaches further back than the in-memory ledger. Reasons and rejected-record samples are deliberately not stored here — they live in the in-memory ledger, which covers roughly the last 6 hours.'
|
|
28
|
+
})
|
|
29
|
+
export class TwinIngestWindow {
|
|
30
|
+
@PrimaryGeneratedColumn('uuid')
|
|
31
|
+
@Field(type => ID, { description: 'Unique identifier of the window record.' })
|
|
32
|
+
readonly id: string
|
|
33
|
+
|
|
34
|
+
@ManyToOne(type => Domain)
|
|
35
|
+
@Field(type => Domain, { nullable: true, description: 'Owning tenant domain.' })
|
|
36
|
+
domain?: Domain
|
|
37
|
+
|
|
38
|
+
@RelationId((e: TwinIngestWindow) => e.domain)
|
|
39
|
+
domainId?: string
|
|
40
|
+
|
|
41
|
+
@Column()
|
|
42
|
+
@Field({ description: 'Twin runtime instance this window belongs to.' })
|
|
43
|
+
instanceId: string
|
|
44
|
+
|
|
45
|
+
@Column()
|
|
46
|
+
@Field({ description: 'When the window opened.' })
|
|
47
|
+
openedAt: Date
|
|
48
|
+
|
|
49
|
+
@Column()
|
|
50
|
+
@Field({ description: 'When the window closed. Windows close on the next ingest after the window length elapses, so a feed that stops leaves its last window open and unwritten.' })
|
|
51
|
+
closedAt: Date
|
|
52
|
+
|
|
53
|
+
@Column({ type: 'int' })
|
|
54
|
+
@Field(type => Int, { description: 'Records offered in this window, whether or not they were accepted.' })
|
|
55
|
+
offered: number
|
|
56
|
+
|
|
57
|
+
@Column({ type: 'int' })
|
|
58
|
+
@Field(type => Int, { description: 'Records the kernel refused in this window because they did not map — the fix is in the mapping.' })
|
|
59
|
+
rejected: number
|
|
60
|
+
|
|
61
|
+
/*
|
|
62
|
+
* 거부와 따로 센다 — **처방이 다르다.**
|
|
63
|
+
*
|
|
64
|
+
* 트윈이 라이브로 돌지 않으면 커널이 레코드를 버린다. 그것을 거부에 합치면 「매핑을 고쳐라」로 읽히고,
|
|
65
|
+
* 통과에 합치면 멈춘 트윈이 100% 로 보인다(실제로 그렇게 떴다). 그래서 제3의 수로 남긴다.
|
|
66
|
+
*/
|
|
67
|
+
@Column({ type: 'int', default: 0 })
|
|
68
|
+
@Field(type => Int, {
|
|
69
|
+
description:
|
|
70
|
+
'Records that mapped fine but never reached the twin, because the twin was not running live while the feed kept delivering. Distinct from rejected: the fix here is to start the twin or disconnect the feed, not to change the mapping. Offered minus rejected minus undelivered is what actually reached the twin.'
|
|
71
|
+
})
|
|
72
|
+
undelivered: number
|
|
73
|
+
|
|
74
|
+
/*
|
|
75
|
+
* 통과율을 **함께 적는다.** 읽는 쪽에서 나누게 하면 소비처마다 반올림이 갈리고(0.999 를 1 로 쓰는
|
|
76
|
+
* 곳이 생긴다), 제시가 0 인 창에서 0으로 나누는 실수가 각자 생긴다. 제시가 0 이면 **비어 둔다** —
|
|
77
|
+
* 1 로 채우면 「전부 통과」로 읽힌다.
|
|
78
|
+
*/
|
|
79
|
+
@Column({ type: 'float', nullable: true })
|
|
80
|
+
@Field(type => Float, {
|
|
81
|
+
nullable: true,
|
|
82
|
+
description:
|
|
83
|
+
'Share of offered records that reached the twin (0–1), rounded to four places. Null when nothing was offered in this window — an empty window is not a perfect one, and writing 1 there would read as "everything passed".'
|
|
84
|
+
})
|
|
85
|
+
acceptedRatio?: number
|
|
86
|
+
|
|
87
|
+
/*
|
|
88
|
+
* ── 적힌 것도 같은 창에 든다 ────────────────────────────────────────────────
|
|
89
|
+
* 유입과 **다른 축**이다. 시뮬 트윈은 받는 것 없이 자기가 낸 사실을 적으므로 `offered` 가 0 이고
|
|
90
|
+
* `journaledRows` 는 0 이 아니다. 이 칸이 없으면 도는 트윈 대부분이 시뮬인 서버의 활동이 어느 추이에도
|
|
91
|
+
* 남지 않는다(계기는 지금만 답하고 재기동에 0 으로 돌아간다).
|
|
92
|
+
*
|
|
93
|
+
* 두 수를 따로 세는 이유: 2026-08-20 에 고친 것이 **행당 왕복**이다(델타마다 한 번 → 창당 한 번).
|
|
94
|
+
* 행만 세면 그 개선도, 되돌아가는 것도 보이지 않는다.
|
|
95
|
+
*/
|
|
96
|
+
@Column({ type: 'int', default: 0 })
|
|
97
|
+
@Field(type => Int, {
|
|
98
|
+
description:
|
|
99
|
+
'Journal rows written for this twin during this window. This is a different axis from ingest: a twin driven by a simulator source receives nothing (offered = 0) yet writes what it produces, so without this column its activity appears in no trend at all.'
|
|
100
|
+
})
|
|
101
|
+
journaledRows: number
|
|
102
|
+
|
|
103
|
+
@Column({ type: 'int', default: 0 })
|
|
104
|
+
@Field(type => Int, {
|
|
105
|
+
description:
|
|
106
|
+
'Number of journal write round-trips (flushes) behind those rows. Counted separately from the rows themselves so that a regression back to per-delta writes — the defect fixed on 2026-08-20, where each kernel delta cost one insert — is visible as rows-per-flush falling back towards 1.'
|
|
107
|
+
})
|
|
108
|
+
journalFlushes: number
|
|
109
|
+
|
|
110
|
+
@CreateDateColumn()
|
|
111
|
+
@Field({ nullable: true, description: 'When this record was written.' })
|
|
112
|
+
createdAt?: Date
|
|
113
|
+
}
|