@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
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Domain } from '@things-factory/shell';
|
|
2
|
+
export declare class TwinIngestWindow {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
domain?: Domain;
|
|
5
|
+
domainId?: string;
|
|
6
|
+
instanceId: string;
|
|
7
|
+
openedAt: Date;
|
|
8
|
+
closedAt: Date;
|
|
9
|
+
offered: number;
|
|
10
|
+
rejected: number;
|
|
11
|
+
undelivered: number;
|
|
12
|
+
acceptedRatio?: number;
|
|
13
|
+
journaledRows: number;
|
|
14
|
+
journalFlushes: number;
|
|
15
|
+
createdAt?: Date;
|
|
16
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TwinIngestWindow = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const typeorm_1 = require("typeorm");
|
|
6
|
+
const type_graphql_1 = require("type-graphql");
|
|
7
|
+
const shell_1 = require("@things-factory/shell");
|
|
8
|
+
/*
|
|
9
|
+
* 인제스트 창 하나 — **동기화 추이를 재기동 뒤에도 이어 보기 위한 행.**
|
|
10
|
+
*
|
|
11
|
+
* ── 왜 창마다이고 건마다가 아닌가 ───────────────────────────────────────────
|
|
12
|
+
* 거부마다 행을 쓰면 라이브 계측 규모에서 초당 1만 행이고, 그 부하가 이벤트 루프로 돌아온다. 창마다면
|
|
13
|
+
* 트윈 1,000개에 하루 14.4만 행(약 14MB)이다. 그래서 **닫힌 창 하나가 영속의 단위**다.
|
|
14
|
+
*
|
|
15
|
+
* 건마다 남기는 자리는 이미 따로 있다: 시나리오 경로가 `PayloadLog` 로 dead-letter 를 남긴다(배치성
|
|
16
|
+
* 유입이라 건수가 감당된다). 이 표는 그것을 대신하지 않고 **추이**를 답한다.
|
|
17
|
+
*
|
|
18
|
+
* ── 왜 사유·표본은 여기 없나 ────────────────────────────────────────────────
|
|
19
|
+
* 사유 문구는 길고 표본에는 **고객의 레코드 원문**이 들어 있다. 그것을 창마다 영속하면 표가 커지고
|
|
20
|
+
* 고객 데이터가 오래 남는다. 사유와 표본은 메모리 장부가 답하고(최근 6시간), 이 표는 숫자만 든다.
|
|
21
|
+
*
|
|
22
|
+
* ── 고치지 않는다 ───────────────────────────────────────────────────────────
|
|
23
|
+
* 닫힌 창은 지나간 사실이다. 그래서 `updatedAt`·`deletedAt` 을 두지 않는다(저널과 같은 규율).
|
|
24
|
+
*/
|
|
25
|
+
let TwinIngestWindow = class TwinIngestWindow {
|
|
26
|
+
};
|
|
27
|
+
exports.TwinIngestWindow = TwinIngestWindow;
|
|
28
|
+
tslib_1.__decorate([
|
|
29
|
+
(0, typeorm_1.PrimaryGeneratedColumn)('uuid'),
|
|
30
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.ID, { description: 'Unique identifier of the window record.' }),
|
|
31
|
+
tslib_1.__metadata("design:type", String)
|
|
32
|
+
], TwinIngestWindow.prototype, "id", void 0);
|
|
33
|
+
tslib_1.__decorate([
|
|
34
|
+
(0, typeorm_1.ManyToOne)(type => shell_1.Domain),
|
|
35
|
+
(0, type_graphql_1.Field)(type => shell_1.Domain, { nullable: true, description: 'Owning tenant domain.' }),
|
|
36
|
+
tslib_1.__metadata("design:type", shell_1.Domain)
|
|
37
|
+
], TwinIngestWindow.prototype, "domain", void 0);
|
|
38
|
+
tslib_1.__decorate([
|
|
39
|
+
(0, typeorm_1.RelationId)((e) => e.domain),
|
|
40
|
+
tslib_1.__metadata("design:type", String)
|
|
41
|
+
], TwinIngestWindow.prototype, "domainId", void 0);
|
|
42
|
+
tslib_1.__decorate([
|
|
43
|
+
(0, typeorm_1.Column)(),
|
|
44
|
+
(0, type_graphql_1.Field)({ description: 'Twin runtime instance this window belongs to.' }),
|
|
45
|
+
tslib_1.__metadata("design:type", String)
|
|
46
|
+
], TwinIngestWindow.prototype, "instanceId", void 0);
|
|
47
|
+
tslib_1.__decorate([
|
|
48
|
+
(0, typeorm_1.Column)(),
|
|
49
|
+
(0, type_graphql_1.Field)({ description: 'When the window opened.' }),
|
|
50
|
+
tslib_1.__metadata("design:type", Date)
|
|
51
|
+
], TwinIngestWindow.prototype, "openedAt", void 0);
|
|
52
|
+
tslib_1.__decorate([
|
|
53
|
+
(0, typeorm_1.Column)(),
|
|
54
|
+
(0, type_graphql_1.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.' }),
|
|
55
|
+
tslib_1.__metadata("design:type", Date)
|
|
56
|
+
], TwinIngestWindow.prototype, "closedAt", void 0);
|
|
57
|
+
tslib_1.__decorate([
|
|
58
|
+
(0, typeorm_1.Column)({ type: 'int' }),
|
|
59
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'Records offered in this window, whether or not they were accepted.' }),
|
|
60
|
+
tslib_1.__metadata("design:type", Number)
|
|
61
|
+
], TwinIngestWindow.prototype, "offered", void 0);
|
|
62
|
+
tslib_1.__decorate([
|
|
63
|
+
(0, typeorm_1.Column)({ type: 'int' }),
|
|
64
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'Records the kernel refused in this window because they did not map — the fix is in the mapping.' }),
|
|
65
|
+
tslib_1.__metadata("design:type", Number)
|
|
66
|
+
], TwinIngestWindow.prototype, "rejected", void 0);
|
|
67
|
+
tslib_1.__decorate([
|
|
68
|
+
(0, typeorm_1.Column)({ type: 'int', default: 0 }),
|
|
69
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Int, {
|
|
70
|
+
description: '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
|
+
tslib_1.__metadata("design:type", Number)
|
|
73
|
+
], TwinIngestWindow.prototype, "undelivered", void 0);
|
|
74
|
+
tslib_1.__decorate([
|
|
75
|
+
(0, typeorm_1.Column)({ type: 'float', nullable: true }),
|
|
76
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Float, {
|
|
77
|
+
nullable: true,
|
|
78
|
+
description: '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".'
|
|
79
|
+
}),
|
|
80
|
+
tslib_1.__metadata("design:type", Number)
|
|
81
|
+
], TwinIngestWindow.prototype, "acceptedRatio", void 0);
|
|
82
|
+
tslib_1.__decorate([
|
|
83
|
+
(0, typeorm_1.Column)({ type: 'int', default: 0 }),
|
|
84
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Int, {
|
|
85
|
+
description: '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.'
|
|
86
|
+
}),
|
|
87
|
+
tslib_1.__metadata("design:type", Number)
|
|
88
|
+
], TwinIngestWindow.prototype, "journaledRows", void 0);
|
|
89
|
+
tslib_1.__decorate([
|
|
90
|
+
(0, typeorm_1.Column)({ type: 'int', default: 0 }),
|
|
91
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Int, {
|
|
92
|
+
description: '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.'
|
|
93
|
+
}),
|
|
94
|
+
tslib_1.__metadata("design:type", Number)
|
|
95
|
+
], TwinIngestWindow.prototype, "journalFlushes", void 0);
|
|
96
|
+
tslib_1.__decorate([
|
|
97
|
+
(0, typeorm_1.CreateDateColumn)(),
|
|
98
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'When this record was written.' }),
|
|
99
|
+
tslib_1.__metadata("design:type", Date)
|
|
100
|
+
], TwinIngestWindow.prototype, "createdAt", void 0);
|
|
101
|
+
exports.TwinIngestWindow = TwinIngestWindow = tslib_1.__decorate([
|
|
102
|
+
(0, typeorm_1.Entity)(),
|
|
103
|
+
(0, typeorm_1.Index)('ix_twin_ingest_window_0', (e) => [e.domain, e.instanceId, e.closedAt], { unique: false }),
|
|
104
|
+
(0, type_graphql_1.ObjectType)({
|
|
105
|
+
description: '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.'
|
|
106
|
+
})
|
|
107
|
+
], TwinIngestWindow);
|
|
108
|
+
//# sourceMappingURL=twin-ingest-window.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"twin-ingest-window.js","sourceRoot":"","sources":["../../../server/service/twin-ingest-window/twin-ingest-window.ts"],"names":[],"mappings":"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","sourcesContent":["import { CreateDateColumn, Entity, Index, Column, RelationId, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'\nimport { ObjectType, Field, ID, Int, Float } from 'type-graphql'\n\nimport { Domain } from '@things-factory/shell'\n\n/*\n * 인제스트 창 하나 — **동기화 추이를 재기동 뒤에도 이어 보기 위한 행.**\n *\n * ── 왜 창마다이고 건마다가 아닌가 ───────────────────────────────────────────\n * 거부마다 행을 쓰면 라이브 계측 규모에서 초당 1만 행이고, 그 부하가 이벤트 루프로 돌아온다. 창마다면\n * 트윈 1,000개에 하루 14.4만 행(약 14MB)이다. 그래서 **닫힌 창 하나가 영속의 단위**다.\n *\n * 건마다 남기는 자리는 이미 따로 있다: 시나리오 경로가 `PayloadLog` 로 dead-letter 를 남긴다(배치성\n * 유입이라 건수가 감당된다). 이 표는 그것을 대신하지 않고 **추이**를 답한다.\n *\n * ── 왜 사유·표본은 여기 없나 ────────────────────────────────────────────────\n * 사유 문구는 길고 표본에는 **고객의 레코드 원문**이 들어 있다. 그것을 창마다 영속하면 표가 커지고\n * 고객 데이터가 오래 남는다. 사유와 표본은 메모리 장부가 답하고(최근 6시간), 이 표는 숫자만 든다.\n *\n * ── 고치지 않는다 ───────────────────────────────────────────────────────────\n * 닫힌 창은 지나간 사실이다. 그래서 `updatedAt`·`deletedAt` 을 두지 않는다(저널과 같은 규율).\n */\n@Entity()\n@Index('ix_twin_ingest_window_0', (e: TwinIngestWindow) => [e.domain, e.instanceId, e.closedAt], { unique: false })\n@ObjectType({\n description:\n '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.'\n})\nexport class TwinIngestWindow {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { description: 'Unique identifier of the window record.' })\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true, description: 'Owning tenant domain.' })\n domain?: Domain\n\n @RelationId((e: TwinIngestWindow) => e.domain)\n domainId?: string\n\n @Column()\n @Field({ description: 'Twin runtime instance this window belongs to.' })\n instanceId: string\n\n @Column()\n @Field({ description: 'When the window opened.' })\n openedAt: Date\n\n @Column()\n @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.' })\n closedAt: Date\n\n @Column({ type: 'int' })\n @Field(type => Int, { description: 'Records offered in this window, whether or not they were accepted.' })\n offered: number\n\n @Column({ type: 'int' })\n @Field(type => Int, { description: 'Records the kernel refused in this window because they did not map — the fix is in the mapping.' })\n rejected: number\n\n /*\n * 거부와 따로 센다 — **처방이 다르다.**\n *\n * 트윈이 라이브로 돌지 않으면 커널이 레코드를 버린다. 그것을 거부에 합치면 「매핑을 고쳐라」로 읽히고,\n * 통과에 합치면 멈춘 트윈이 100% 로 보인다(실제로 그렇게 떴다). 그래서 제3의 수로 남긴다.\n */\n @Column({ type: 'int', default: 0 })\n @Field(type => Int, {\n description:\n '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.'\n })\n undelivered: number\n\n /*\n * 통과율을 **함께 적는다.** 읽는 쪽에서 나누게 하면 소비처마다 반올림이 갈리고(0.999 를 1 로 쓰는\n * 곳이 생긴다), 제시가 0 인 창에서 0으로 나누는 실수가 각자 생긴다. 제시가 0 이면 **비어 둔다** —\n * 1 로 채우면 「전부 통과」로 읽힌다.\n */\n @Column({ type: 'float', nullable: true })\n @Field(type => Float, {\n nullable: true,\n description:\n '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\".'\n })\n acceptedRatio?: number\n\n /*\n * ── 적힌 것도 같은 창에 든다 ────────────────────────────────────────────────\n * 유입과 **다른 축**이다. 시뮬 트윈은 받는 것 없이 자기가 낸 사실을 적으므로 `offered` 가 0 이고\n * `journaledRows` 는 0 이 아니다. 이 칸이 없으면 도는 트윈 대부분이 시뮬인 서버의 활동이 어느 추이에도\n * 남지 않는다(계기는 지금만 답하고 재기동에 0 으로 돌아간다).\n *\n * 두 수를 따로 세는 이유: 2026-08-20 에 고친 것이 **행당 왕복**이다(델타마다 한 번 → 창당 한 번).\n * 행만 세면 그 개선도, 되돌아가는 것도 보이지 않는다.\n */\n @Column({ type: 'int', default: 0 })\n @Field(type => Int, {\n description:\n '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.'\n })\n journaledRows: number\n\n @Column({ type: 'int', default: 0 })\n @Field(type => Int, {\n description:\n '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.'\n })\n journalFlushes: number\n\n @CreateDateColumn()\n @Field({ nullable: true, description: 'When this record was written.' })\n createdAt?: Date\n}\n"]}
|
|
@@ -53,9 +53,9 @@ tslib_1.__decorate([
|
|
|
53
53
|
], TwinInstance.prototype, "kind", void 0);
|
|
54
54
|
tslib_1.__decorate([
|
|
55
55
|
(0, typeorm_1.Column)(),
|
|
56
|
-
(0, type_graphql_1.Field)({ description: "
|
|
56
|
+
(0, type_graphql_1.Field)({ description: "Restart policy: 'resync' (read again from the external source) | 'resume' (continue this twin's own journal) | 'reset' (wipe and replay from the seed). No default — an unspecified policy is refused." }),
|
|
57
57
|
tslib_1.__metadata("design:type", String)
|
|
58
|
-
], TwinInstance.prototype, "
|
|
58
|
+
], TwinInstance.prototype, "restartPolicy", void 0);
|
|
59
59
|
tslib_1.__decorate([
|
|
60
60
|
(0, typeorm_1.Column)({ type: 'simple-json', nullable: true, transformer: model_vocabulary_js_1.modelColumnTransformer }),
|
|
61
61
|
(0, type_graphql_1.Field)(type => shell_1.ScalarObject, { nullable: true, description: 'TwinModelDef blueprint (locations/equipment) required to replay events into state. Older stored boards used nodes/movers/equipmentList; they are normalised on read so consumers see one vocabulary.' }) // vocabulary-guard: allow — 옛 어휘를 흡수하는 것이 이 코드의 일이다(세대 이름을 적어야 흡수할 수 있다)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"twin-instance.js","sourceRoot":"","sources":["../../../server/service/twin-instance/twin-instance.ts"],"names":[],"mappings":"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","sourcesContent":["import { CreateDateColumn, Entity, Index, Column, RelationId, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'\nimport { ObjectType, Field, ID } from 'type-graphql'\n\nimport { Domain, ScalarObject } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\nimport { modelColumnTransformer } from '../../engine/model-vocabulary.js'\n\n/*\n * TwinInstance — 트윈 인스턴스 레지스트리(재부팅 복구의 앵커).\n * 복구(DB→replay)는 model(마스터 청사진)가 있어야 이벤트를 상태로 다시 계산할 수 있다 → model·kind 를 영속.\n * 이벤트(TwinEvent)와 달리 가변(status 갱신) — 인스턴스의 \"현재 등록 상태\".\n * model 는 simple-json(멀티DB 이식 — postgres/mysql/sqlite/mssql/oracle 공통).\n * (CLAUDE.md: 모든 @ObjectType/@Field 는 영문 description 필수.)\n */\n@Entity()\n@Index('ix_twin_instance_0', (e: TwinInstance) => [e.domain, e.instanceId], { unique: true })\n@Index('ix_twin_instance_1', (e: TwinInstance) => [e.domain, e.spaceId]) // 공간→인스턴스 1급 질의(공간 중심 존재론)\n@ObjectType({ description: 'Registry of a hosted twin runtime instance — anchors reboot recovery (model + kind).' })\nexport class TwinInstance {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { description: 'Unique identifier of the registry record.' })\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true, description: 'Owning tenant domain.' })\n domain?: Domain\n\n @RelationId((e: TwinInstance) => e.domain)\n domainId?: string\n\n @Column()\n @Field({ description: 'Twin runtime instance id (unique within a domain).' })\n instanceId: string\n\n /**\n * **사람이 부르는 이름** — 업무키(`instanceId`)와 나란히 두는 표시명(프레임워크 엔티티 규약).\n *\n * ── 왜 업무키와 이름을 나눠 두나 ─────────────────────────────────────────────\n * `instanceId` 는 URL·저널·구독이 전부 쓰는 **계약**이라 바꿀 수 없다. 반대로 이름은 바뀌고,\n * 중복되고(사이트 둘에 \"도장\" 이 있는 것이 정상), 언어마다 다르다. 둘을 한 컬럼에 담으면 개명이\n * 곧 다른 트윈이 되고 재동기가 끊긴다 — 그래서 나눈다(이 저장소에서 식별자가 밖에서 오는 것은\n * 전부 이 규약을 쓴다: `spaceId`·`locationId`·`equipmentId`·`operationKey`).\n *\n * 나눴으면 **이름을 반드시 함께 둬야 한다.** 이 자리가 없던 동안 사이트 이름은 model 안에만 있어서\n * 목록이 `crew-two` 만 보였다 — 나누기만 하고 절반을 빼먹으면 화면에는 id 만 남는다.\n *\n * 원본이 이름을 말하지 않으면 비운다(id 를 베끼지 않는다).\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Human name of this twin (the site name from its master). Empty when the source does not name it — never copied from instanceId.' })\n name?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Longer description when the source provides one.' })\n description?: string\n\n /* 어느 도메인 커널이 이 트윈을 운영하는가. 커널 없는 트윈은 없으므로 비어 있을 수 없다\n (`TwinEngine.start`·`register` 모두 필수 인자로 받는다). */\n @Column()\n @Field({ description: 'Domain kernel kind: wms | yms | mes.' })\n kind: string\n\n /*\n * realityMode — 이 트윈의 현실이 어디서 오는가(runtime-state-model §0·§1 \"지속 현실 하나 + 여러 렌즈\"의 ① 선언).\n * 'mirror' = 현실은 외부 실물 시스템. 트윈=순수 렌즈, 재기동 시 실물서 재동기(startLive).\n * 'sim-world' = 트윈이 낳은 타임라인이 곧 진실이며 이어진다 → 재기동 시 저널서 resume(현실 지속).\n * 'sim-experiment' = seed 재현형. 재기동 = 같은 우주 재실행(저널 reset, revision 0) — 백지 시작이 버그가 아니라 선언된 거동.\n *\n * **비어 있을 수 없다.** 이 값은 재기동 거동을 구분하는 선언이고, 없으면 읽는 쪽마다 각자\n * 기본값을 고르게 된다 — 같은 트윈이 부르는 자리에 따라 다르게 재기동하는 길이다.\n * 선언을 생략한 프로비저닝은 `register` 가 `DEFAULT_REALITY_MODE` 로 **각인해서 저장한다**.\n * 즉 \"미선언\" 이라는 상태 자체를 저장소에 두지 않는다.\n */\n @Column()\n @Field({ description: \"Reality source declaration: 'mirror' (external system is truth, resync on reboot) | 'sim-world' (generated timeline is truth and persists, resume on reboot) | 'sim-experiment' (seed-reproducible, resets on reboot).\" })\n realityMode: string\n\n /* 읽을 때 어휘 세대를 흡수한다(`nodes`/`movers`/`equipmentList` → `locations`/`equipment`). (vocabulary-guard: allow — 세대 이름 없이는 설명할 수 없다.)\n 자리마다 리더를 쓰게 하면 다음에 추가되는 자리가 또 빠지고, 빠진 것은 오류가 아니라 **빈 공장**이라\n 조용하다(2026-08-06: 모델러 엔티티 목록이 통째로 비었다). 상세 `engine/model-vocabulary.ts`. */\n @Column({ type: 'simple-json', nullable: true, transformer: modelColumnTransformer })\n @Field(type => ScalarObject, { nullable: true, description: 'TwinModelDef blueprint (locations/equipment) required to replay events into state. Older stored boards used nodes/movers/equipmentList; they are normalised on read so consumers see one vocabulary.' }) // vocabulary-guard: allow — 옛 어휘를 흡수하는 것이 이 코드의 일이다(세대 이름을 적어야 흡수할 수 있다)\n model?: any\n\n /*\n * 로컬 선언 겹 — **구조는 원천이, 수는 우리가.**\n *\n * 요금 단가·계약 정보·축전지 충전율처럼 원 시스템이 모르는 값을 넣는 자리다. `model` 에 직접 쓰지\n * 않는 이유는 그것이 **원천의 사본**이기 때문이다: 다시 읽기가 사본을 새로 그리면 직접 쓴 값은 조용히\n * 사라진다(요금이 사라지면 금액도 사라지고, 사용자는 이유를 알 수 없다).\n *\n * 그래서 여기 남기고, 모델을 그릴 때마다 그 위에 얹는다(`engine/local-declarations`). 얹힌 값에는\n * `source: 'local'` 이 붙는다 — 커널 계약의 `PropertySource` 축(`reference` ↔ `local`)이고, 「밖에서\n * 참조해 온 값이냐, 이 안에서 정한 값이냐」에 답한다.\n *\n * **이름에 장소 낱말을 쓰지 않는다**(옛 이름 `siteDeclarations`): 이 코드에서 `site` 는 **외부\n * 시스템이 부르는 사업장**이고(SAP `P3`·Oracle `W01` 이 같은 현장), 여기 담기는 것은 그 외부 시스템이\n * **모르는** 값이다 — 이름이 내용과 정반대였다. `space` 도 아니다: 그것은 「어디」에 답하고 이 축은\n * 「누가 말했나」에 답한다.\n */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => ScalarObject, { nullable: true, description: \"Locally-declared values layered on top of the reference-derived model (tariff, contract, state of charge) — values whose authority sits here, not in the source system. Kept apart from `model` so a resync cannot silently drop them.\" })\n localDeclarations?: any\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Owning space id (co-location anchor). Promoted from model.spaceId — first-class \"space → its twins\" query (space-centered ontology).' })\n spaceId?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Owning area id within the space (optional scope). Promoted from model.areaId.' })\n areaId?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Lifecycle status: running | stopped.' })\n status?: string\n\n /*\n * purpose — 이 인스턴스가 운영 트윈인지, 격리된 벤치 사본인지 1급 선언(이름 접두사가 아니라 속성으로 구별).\n * 'bench' 는 규모/부하 실험용 격리 사본 — 목록·정리·안전 가드가 이 속성을 키로 동작(운영 트윈 무오염 보장).\n *\n * **비어 있을 수 없다.** 프로비저닝이 `register` 에서 'operational' 로 각인하고, 벤치 사본만\n * `setPurpose` 로 뒤집는다. 예전에는 비워 둔 채 읽는 쪽에서 메웠는데, 그러면 \"운영이라고 선언된 것\"\n * 과 \"아무도 말하지 않은 것\" 이 같은 값으로 보여서 벤치 격리가 추측에 기대게 된다.\n */\n @Column()\n @Field({ description: \"Instance purpose: 'operational' (real twin) | 'bench' (isolated copy for scale/load experiments).\" })\n purpose: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'For a bench copy: the source spaceId it was copied from (provenance).' })\n copyOf?: string\n\n /**\n * **이 트윈을 다시 읽는 방법** — 어떤 원본에서 나왔나(`MasterOrigin`).\n *\n * 트윈은 원본을 비출 뿐이므로(ADR-0028) 원본이 달라지면 다시 읽어야 한다. 그런데 이 자리가 없던\n * 동안 트윈은 **자기가 무엇에서 나왔는지 몰랐다** — 어댑터로 들어온 트윈은 `TwinReference` 행이\n * 있었지만 템플릿으로 세운 트윈에는 아무 표식도 없었고, 그래서 첫날의 모습에 영원히 머무는\n * 트윈이 만들어졌다. 원천이 무엇이든 여기 한 자리에 남긴다.\n *\n * **비어 있을 수 있다** — 이 자리가 생기기 전에 만들어진 트윈이 그렇다. 그때는 \"다시 읽을 수\n * 없다\" 가 사실이므로 화면이 그대로 말한다(짐작해 채우지 않는다).\n */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => ScalarObject, {\n nullable: true,\n description:\n \"How this twin's master can be produced again — {kind:'reference', source} or {kind:'template', templateId, params}. Absent means this twin cannot be re-read from its source.\"\n })\n origin?: any\n\n /*\n * `revision` 컬럼은 **없앴다**(2026-08-08).\n *\n * `ADR-0015` 가 \"정의만 있고 write 안 됨 — 프로비저닝에서 최신 저널 revision 을 반영하도록\n * 채운다\" 고 적어 두었는데, 채우지 않는 편이 맞다. 저널의 최신 번호는 **저널이 아는 사실**이고\n * (`TwinEngine.list` 가 `MAX(revision)` 으로 낸다), 여기 복제하면 **같은 사실의 집이 둘**이 된다.\n * 이 세션에만 그 부류를 셋 겪었다(`model.operations` 두 집 · `materials` 두 집 · tenantId 중복).\n *\n * 복제한 값은 반드시 어긋난다 — 저널은 틱마다 늘고 이 컬럼은 저장할 때만 갱신되므로, 화면이\n * 어느 쪽을 읽느냐로 다른 숫자가 나온다. 그 조용한 어긋남을 만들 이유가 없다.\n */\n\n @CreateDateColumn()\n @Field({ nullable: true, description: 'Timestamp when the instance was first registered.' })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true, description: 'Timestamp when the instance registry row was last updated.' })\n updatedAt?: Date\n\n /*\n * 누가 만들고 누가 고쳤나 — **이 행은 사람이 만든다**(프로비저닝은 사용자 행위)이므로 규약의\n * 풀세트를 갖는다. 투영 행(자리·설비·공정)은 반대로 인제스트가 그린 것이라 저자가 사람이 아니고,\n * 그 저자는 `sourceRef`+`ingestedAt` 이 이미 말한다 — 그래서 그쪽에는 두지 않는다.\n *\n * 비어 있을 수 있다: 부팅 자동 프로비저닝처럼 **사람이 없는 경로**로 만들어진 행이 그렇다.\n * 그때 아무 사용자를 적어 넣으면 감사 기록이 거짓이 된다.\n */\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true, description: 'The user who first provisioned this twin. Empty when it was created by a system path (boot provisioning).' })\n creator?: User\n\n @RelationId((e: TwinInstance) => e.creator)\n creatorId?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true, description: 'The user who last changed this twin registry row.' })\n updater?: User\n\n @RelationId((e: TwinInstance) => e.updater)\n updaterId?: string\n}\n"]}
|
|
1
|
+
{"version":3,"file":"twin-instance.js","sourceRoot":"","sources":["../../../server/service/twin-instance/twin-instance.ts"],"names":[],"mappings":"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","sourcesContent":["import { CreateDateColumn, Entity, Index, Column, RelationId, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'\nimport { ObjectType, Field, ID } from 'type-graphql'\n\nimport { Domain, ScalarObject } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\nimport { modelColumnTransformer } from '../../engine/model-vocabulary.js'\n\n/*\n * TwinInstance — 트윈 인스턴스 레지스트리(재부팅 복구의 앵커).\n * 복구(DB→replay)는 model(마스터 청사진)가 있어야 이벤트를 상태로 다시 계산할 수 있다 → model·kind 를 영속.\n * 이벤트(TwinEvent)와 달리 가변(status 갱신) — 인스턴스의 \"현재 등록 상태\".\n * model 는 simple-json(멀티DB 이식 — postgres/mysql/sqlite/mssql/oracle 공통).\n * (CLAUDE.md: 모든 @ObjectType/@Field 는 영문 description 필수.)\n */\n@Entity()\n@Index('ix_twin_instance_0', (e: TwinInstance) => [e.domain, e.instanceId], { unique: true })\n@Index('ix_twin_instance_1', (e: TwinInstance) => [e.domain, e.spaceId]) // 공간→인스턴스 1급 질의(공간 중심 존재론)\n@ObjectType({ description: 'Registry of a hosted twin runtime instance — anchors reboot recovery (model + kind).' })\nexport class TwinInstance {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { description: 'Unique identifier of the registry record.' })\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true, description: 'Owning tenant domain.' })\n domain?: Domain\n\n @RelationId((e: TwinInstance) => e.domain)\n domainId?: string\n\n @Column()\n @Field({ description: 'Twin runtime instance id (unique within a domain).' })\n instanceId: string\n\n /**\n * **사람이 부르는 이름** — 업무키(`instanceId`)와 나란히 두는 표시명(프레임워크 엔티티 규약).\n *\n * ── 왜 업무키와 이름을 나눠 두나 ─────────────────────────────────────────────\n * `instanceId` 는 URL·저널·구독이 전부 쓰는 **계약**이라 바꿀 수 없다. 반대로 이름은 바뀌고,\n * 중복되고(사이트 둘에 \"도장\" 이 있는 것이 정상), 언어마다 다르다. 둘을 한 컬럼에 담으면 개명이\n * 곧 다른 트윈이 되고 재동기가 끊긴다 — 그래서 나눈다(이 저장소에서 식별자가 밖에서 오는 것은\n * 전부 이 규약을 쓴다: `spaceId`·`locationId`·`equipmentId`·`operationKey`).\n *\n * 나눴으면 **이름을 반드시 함께 둬야 한다.** 이 자리가 없던 동안 사이트 이름은 model 안에만 있어서\n * 목록이 `crew-two` 만 보였다 — 나누기만 하고 절반을 빼먹으면 화면에는 id 만 남는다.\n *\n * 원본이 이름을 말하지 않으면 비운다(id 를 베끼지 않는다).\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Human name of this twin (the site name from its master). Empty when the source does not name it — never copied from instanceId.' })\n name?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Longer description when the source provides one.' })\n description?: string\n\n /* 어느 도메인 커널이 이 트윈을 운영하는가. 커널 없는 트윈은 없으므로 비어 있을 수 없다\n (`TwinEngine.start`·`register` 모두 필수 인자로 받는다). */\n @Column()\n @Field({ description: 'Domain kernel kind: wms | yms | mes.' })\n kind: string\n\n /*\n * **재기동 정책** — 이 트윈은 다시 세워질 때 자기 과거를 어떻게 대하나(ADR-0029 §2).\n *\n * 'resync' = 외부 실물에서 다시 읽는다(원본이 진실이므로 우리 과거를 이어 붙이지 않는다 → startLive).\n * 'resume' = 저널을 이어 세운다(이 트윈이 낳은 타임라인이 곧 사실이다).\n * 'reset' = 저널을 비우고 씨앗부터 다시 돌린다(재현 가능한 실험 — 백지 시작이 선언된 거동이다).\n *\n * ── 왜 트윈에 남았나 (옛 이름 `realityMode`) ────────────────────────────────\n * 「현실이 어디서 오나」는 ADR-0029 가 **원본의 종류**로 옮긴 물음이다(`TwinReference.adapterType`) —\n * 트윈은 한 종류이고 언제나 원본을 반영한다. 그런데 코드가 이 축으로 실제로 가른 것은 **재기동 거동\n * 하나**였고, 그것은 원본의 성질이 아니라 **트윈과 자기 과거의 관계**다: 같은 시뮬레이터 원본을 보는 두\n * 트윈이 하나는 이어가고 하나는 매번 처음부터 재현할 수 있다. 그래서 축은 남고 이름만 바뀌었다.\n *\n * **비어 있을 수 없고, 기본값도 없다.** 예전에는 미선언이 조용히 `sim-experiment`(=저널 초기화)로\n * 떨어졌다 — 값이 하나 어긋나는 순간 이력을 지우는 길이다. 이제 읽는 쪽이 **오류를 낸다**\n * (`engine/restart-policy.ts`). 선언은 프로비저닝하는 쪽의 몫이다.\n */\n @Column()\n @Field({ description: \"Restart policy: 'resync' (read again from the external source) | 'resume' (continue this twin's own journal) | 'reset' (wipe and replay from the seed). No default — an unspecified policy is refused.\" })\n restartPolicy: string\n\n /* 읽을 때 어휘 세대를 흡수한다(`nodes`/`movers`/`equipmentList` → `locations`/`equipment`). (vocabulary-guard: allow — 세대 이름 없이는 설명할 수 없다.)\n 자리마다 리더를 쓰게 하면 다음에 추가되는 자리가 또 빠지고, 빠진 것은 오류가 아니라 **빈 공장**이라\n 조용하다(2026-08-06: 모델러 엔티티 목록이 통째로 비었다). 상세 `engine/model-vocabulary.ts`. */\n @Column({ type: 'simple-json', nullable: true, transformer: modelColumnTransformer })\n @Field(type => ScalarObject, { nullable: true, description: 'TwinModelDef blueprint (locations/equipment) required to replay events into state. Older stored boards used nodes/movers/equipmentList; they are normalised on read so consumers see one vocabulary.' }) // vocabulary-guard: allow — 옛 어휘를 흡수하는 것이 이 코드의 일이다(세대 이름을 적어야 흡수할 수 있다)\n model?: any\n\n /*\n * 로컬 선언 겹 — **구조는 원천이, 수는 우리가.**\n *\n * 요금 단가·계약 정보·축전지 충전율처럼 원 시스템이 모르는 값을 넣는 자리다. `model` 에 직접 쓰지\n * 않는 이유는 그것이 **원천의 사본**이기 때문이다: 다시 읽기가 사본을 새로 그리면 직접 쓴 값은 조용히\n * 사라진다(요금이 사라지면 금액도 사라지고, 사용자는 이유를 알 수 없다).\n *\n * 그래서 여기 남기고, 모델을 그릴 때마다 그 위에 얹는다(`engine/local-declarations`). 얹힌 값에는\n * `source: 'local'` 이 붙는다 — 커널 계약의 `PropertySource` 축(`reference` ↔ `local`)이고, 「밖에서\n * 참조해 온 값이냐, 이 안에서 정한 값이냐」에 답한다.\n *\n * **이름에 장소 낱말을 쓰지 않는다**(옛 이름 `siteDeclarations`): 이 코드에서 `site` 는 **외부\n * 시스템이 부르는 사업장**이고(SAP `P3`·Oracle `W01` 이 같은 현장), 여기 담기는 것은 그 외부 시스템이\n * **모르는** 값이다 — 이름이 내용과 정반대였다. `space` 도 아니다: 그것은 「어디」에 답하고 이 축은\n * 「누가 말했나」에 답한다.\n */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => ScalarObject, { nullable: true, description: \"Locally-declared values layered on top of the reference-derived model (tariff, contract, state of charge) — values whose authority sits here, not in the source system. Kept apart from `model` so a resync cannot silently drop them.\" })\n localDeclarations?: any\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Owning space id (co-location anchor). Promoted from model.spaceId — first-class \"space → its twins\" query (space-centered ontology).' })\n spaceId?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Owning area id within the space (optional scope). Promoted from model.areaId.' })\n areaId?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Lifecycle status: running | stopped.' })\n status?: string\n\n /*\n * purpose — 이 인스턴스가 운영 트윈인지, 격리된 벤치 사본인지 1급 선언(이름 접두사가 아니라 속성으로 구별).\n * 'bench' 는 규모/부하 실험용 격리 사본 — 목록·정리·안전 가드가 이 속성을 키로 동작(운영 트윈 무오염 보장).\n *\n * **비어 있을 수 없다.** 프로비저닝이 `register` 에서 'operational' 로 각인하고, 벤치 사본만\n * `setPurpose` 로 뒤집는다. 예전에는 비워 둔 채 읽는 쪽에서 메웠는데, 그러면 \"운영이라고 선언된 것\"\n * 과 \"아무도 말하지 않은 것\" 이 같은 값으로 보여서 벤치 격리가 추측에 기대게 된다.\n */\n @Column()\n @Field({ description: \"Instance purpose: 'operational' (real twin) | 'bench' (isolated copy for scale/load experiments).\" })\n purpose: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'For a bench copy: the source spaceId it was copied from (provenance).' })\n copyOf?: string\n\n /**\n * **이 트윈을 다시 읽는 방법** — 어떤 원본에서 나왔나(`MasterOrigin`).\n *\n * 트윈은 원본을 비출 뿐이므로(ADR-0028) 원본이 달라지면 다시 읽어야 한다. 그런데 이 자리가 없던\n * 동안 트윈은 **자기가 무엇에서 나왔는지 몰랐다** — 어댑터로 들어온 트윈은 `TwinReference` 행이\n * 있었지만 템플릿으로 세운 트윈에는 아무 표식도 없었고, 그래서 첫날의 모습에 영원히 머무는\n * 트윈이 만들어졌다. 원천이 무엇이든 여기 한 자리에 남긴다.\n *\n * **비어 있을 수 있다** — 이 자리가 생기기 전에 만들어진 트윈이 그렇다. 그때는 \"다시 읽을 수\n * 없다\" 가 사실이므로 화면이 그대로 말한다(짐작해 채우지 않는다).\n */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => ScalarObject, {\n nullable: true,\n description:\n \"How this twin's master can be produced again — {kind:'reference', source} or {kind:'template', templateId, params}. Absent means this twin cannot be re-read from its source.\"\n })\n origin?: any\n\n /*\n * `revision` 컬럼은 **없앴다**(2026-08-08).\n *\n * `ADR-0015` 가 \"정의만 있고 write 안 됨 — 프로비저닝에서 최신 저널 revision 을 반영하도록\n * 채운다\" 고 적어 두었는데, 채우지 않는 편이 맞다. 저널의 최신 번호는 **저널이 아는 사실**이고\n * (`TwinEngine.list` 가 `MAX(revision)` 으로 낸다), 여기 복제하면 **같은 사실의 집이 둘**이 된다.\n * 이 세션에만 그 부류를 셋 겪었다(`model.operations` 두 집 · `materials` 두 집 · tenantId 중복).\n *\n * 복제한 값은 반드시 어긋난다 — 저널은 틱마다 늘고 이 컬럼은 저장할 때만 갱신되므로, 화면이\n * 어느 쪽을 읽느냐로 다른 숫자가 나온다. 그 조용한 어긋남을 만들 이유가 없다.\n */\n\n @CreateDateColumn()\n @Field({ nullable: true, description: 'Timestamp when the instance was first registered.' })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true, description: 'Timestamp when the instance registry row was last updated.' })\n updatedAt?: Date\n\n /*\n * 누가 만들고 누가 고쳤나 — **이 행은 사람이 만든다**(프로비저닝은 사용자 행위)이므로 규약의\n * 풀세트를 갖는다. 투영 행(자리·설비·공정)은 반대로 인제스트가 그린 것이라 저자가 사람이 아니고,\n * 그 저자는 `sourceRef`+`ingestedAt` 이 이미 말한다 — 그래서 그쪽에는 두지 않는다.\n *\n * 비어 있을 수 있다: 부팅 자동 프로비저닝처럼 **사람이 없는 경로**로 만들어진 행이 그렇다.\n * 그때 아무 사용자를 적어 넣으면 감사 기록이 거짓이 된다.\n */\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true, description: 'The user who first provisioned this twin. Empty when it was created by a system path (boot provisioning).' })\n creator?: User\n\n @RelationId((e: TwinInstance) => e.creator)\n creatorId?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true, description: 'The user who last changed this twin registry row.' })\n updater?: User\n\n @RelationId((e: TwinInstance) => e.updater)\n updaterId?: string\n}\n"]}
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TwinJournalQuery = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
|
+
const log_js_1 = require("../../engine/log.js");
|
|
5
6
|
const typeorm_1 = require("typeorm");
|
|
6
7
|
const type_graphql_1 = require("type-graphql");
|
|
7
8
|
const shell_1 = require("@things-factory/shell");
|
|
8
9
|
const twin_event_js_1 = require("../twin-event/twin-event.js");
|
|
10
|
+
const journal_count_js_1 = require("../twin-event/journal-count.js");
|
|
9
11
|
const twin_event_type_js_1 = require("../twin-event/twin-event-type.js");
|
|
10
12
|
const index_js_1 = require("../../engine/index.js");
|
|
11
13
|
const state_axes_js_1 = require("../../engine/state-axes.js");
|
|
@@ -17,6 +19,11 @@ const kpi_query_js_1 = require("../../engine/kpi-query.js");
|
|
|
17
19
|
* twinReplay : 내구 저널을 replay 해 상태 재구성(재부팅 복구·시간여행) — untilRevision 으로 임의 과거.
|
|
18
20
|
* (실시간 스트림은 twinState subscription, 명령은 twin-control mutation. 이건 '질의' 표면.)
|
|
19
21
|
*/
|
|
22
|
+
/*
|
|
23
|
+
* 건수 상한은 **저널 세기의 문**에서 온다(`journal-count.ts`) — 값과 규율이 두 곳에 흩어지면 한쪽만
|
|
24
|
+
* 고쳐진다. 근거(2026-08-22 실측): 상한 1만이면 0.04초, 상한 없이 세면 5.32초(저널 358만 행).
|
|
25
|
+
*/
|
|
26
|
+
const COUNT_CAP = journal_count_js_1.JOURNAL_COUNT_CAP;
|
|
20
27
|
/**
|
|
21
28
|
* 시각 필터 값을 날짜로 — 배열(between)이면 원소마다.
|
|
22
29
|
*
|
|
@@ -45,8 +52,31 @@ let TwinJournalQuery = class TwinJournalQuery {
|
|
|
45
52
|
where.revision = (0, typeorm_1.MoreThanOrEqual)(fromRevision);
|
|
46
53
|
else if (toRevision != null)
|
|
47
54
|
where.revision = (0, typeorm_1.LessThanOrEqual)(toRevision);
|
|
48
|
-
/*
|
|
49
|
-
|
|
55
|
+
/*
|
|
56
|
+
* 최신순(DESC) 기본 — Ledger/recent 뷰. 범위 필터로 시간여행 구간 조회도 가능.
|
|
57
|
+
*
|
|
58
|
+
* ── 정렬 축을 **거르는 축과 맞춘다** (2026-08-22 실측) ─────────────────────
|
|
59
|
+
* `untilTime` 경로는 `eventTime <= T` 로 거르면서 `revision` 으로 정렬했다. 그러면 sqlite 는 조건에
|
|
60
|
+
* 맞는 행을 **전부 찾아 전부 정렬한 뒤** 상한을 적용한다 — 상한이 정렬 뒤에 걸리므로 200건을 얻는
|
|
61
|
+
* 데도 전량 정렬을 치른다. 실측(인스턴스 하나가 375만 행):
|
|
62
|
+
*
|
|
63
|
+
* ORDER BY revision DESC → 33.4초
|
|
64
|
+
* ORDER BY eventTime DESC, revision DESC → 0.007초 (4,700배)
|
|
65
|
+
*
|
|
66
|
+
* 계획이 그것을 그대로 말한다: 앞은 `USE TEMP B-TREE FOR ORDER BY`(전체 정렬), 뒤는
|
|
67
|
+
* `... FOR LAST TERM OF ORDER BY` — 시각은 인덱스(`ix_twin_event_1`)에서 순서대로 나오고
|
|
68
|
+
* **동시각만** 임시 정렬한다.
|
|
69
|
+
*
|
|
70
|
+
* **리비전 경로는 그대로 둔다.** 그때는 거르는 축이 `revision` 이고 정렬도 `revision` 이라 이미
|
|
71
|
+
* 맞아 있다(`ix_twin_event_0`). 축이 어긋난 경로만 고친다 — 맞는 자리를 건드리면 그쪽이 어긋난다.
|
|
72
|
+
*
|
|
73
|
+
* 순서의 뜻이 **완전히 같지는 않다**(측정해서 고친 문장): `revision` 순서와 `eventTime` 순서가
|
|
74
|
+
* 어긋나는 자리가 실재한다(426만 행 중 7건 — 미러는 원본의 시각을 싣고 도착 순서로 리비전을 받는다).
|
|
75
|
+
* 이 목록은 사람이 이력을 훑는 화면이고 **시각순으로 보는 것이 그 화면의 뜻**이므로, 그 어긋남은
|
|
76
|
+
* 결함이 아니라 오히려 맞는 순서다. 동시각의 순서를 위해 `revision` 을 둘째 축으로 남긴다.
|
|
77
|
+
*/
|
|
78
|
+
const order = untilTime != null ? { eventTime: 'DESC', revision: 'DESC' } : { revision: 'DESC' };
|
|
79
|
+
return (0, shell_1.getRepository)(twin_event_js_1.TwinEvent).find({ where, order, take: limit ?? 200 });
|
|
50
80
|
}
|
|
51
81
|
/**
|
|
52
82
|
* 저널 목록 — things-factory 표준 목록 계약(`ListParam` → `{ items, total }`).
|
|
@@ -108,11 +138,34 @@ let TwinJournalQuery = class TwinJournalQuery {
|
|
|
108
138
|
});
|
|
109
139
|
/* 대상 범위는 호출자가 필터로 빼먹을 수 있는 값이 아니다 — 계약상 필수라 여기서 강제한다. */
|
|
110
140
|
qb.andWhere(`${qb.alias}.instanceId IN (:...instanceIds)`, { instanceIds });
|
|
111
|
-
|
|
141
|
+
/*
|
|
142
|
+
* ── 건수는 **상한까지만** 센다 (2026-08-22 실측) ──────────────────────────
|
|
143
|
+
* 예전에는 `getManyAndCount()` 였다. 그 두 번째 질의가 조건에 맞는 행을 **전부** 세는데, 실측한
|
|
144
|
+
* 개발 저널에서 트윈 하나가 358만 행이라 **한 번에 5.32초**였다. 이력 화면을 열 때마다 그것을
|
|
145
|
+
* 물었고, 그 5초 동안 이벤트 루프가 다른 요청을 처리하지 못했다.
|
|
146
|
+
*
|
|
147
|
+
* 상한을 두면 같은 답이 **0.04초**다(130배). 상한을 넘으면 정확한 수를 모르는 것이 사실이므로
|
|
148
|
+
* 목록 규약의 「서버 상한에 부딪힘(total 모름)」 갈래로 낸다 — 화면은 「처음 N 건만」이라고 말하고
|
|
149
|
+
* 좁히기를 권한다. **조용히 자르지 않는다**: 상한을 넘었다는 사실을 `capped` 로 함께 낸다.
|
|
150
|
+
*
|
|
151
|
+
* 왜 하위 질의로 세지 않나: 5개 드라이버에서 같이 도는 형태여야 한다(이 저장소의 절대 규칙).
|
|
152
|
+
* 그래서 **키 컬럼만 상한까지 읽어 세는** 방식을 쓴다 — 색인만 읽으므로 행 본문을 만들지 않는다.
|
|
153
|
+
*/
|
|
154
|
+
const items = await qb.getMany();
|
|
155
|
+
const probe = qb
|
|
156
|
+
.clone()
|
|
157
|
+
.orderBy() /* 세는 데 순서는 필요 없다 — 정렬을 남기면 임의 축에서 전체 정렬이 된다. */
|
|
158
|
+
.skip(0)
|
|
159
|
+
.take(COUNT_CAP + 1)
|
|
160
|
+
.select(`${qb.alias}.id`);
|
|
161
|
+
const counted = await probe.getRawMany();
|
|
162
|
+
const capped = counted.length > COUNT_CAP;
|
|
163
|
+
/* 상한을 넘었으면 **전체 수를 모른다** — 상한값을 전체로 적으면 화면이 거짓을 말한다. */
|
|
164
|
+
const total = capped ? undefined : counted.length;
|
|
112
165
|
/* 다음 커서는 **이번 페이지의 마지막 행**에서 만든다. 페이지가 상한보다 짧으면 뒤가 없다. */
|
|
113
166
|
const axes = (0, shell_1.cursorSortings)(effective.sortings, 'id');
|
|
114
167
|
const nextCursor = items.length === limit ? (0, shell_1.buildNextCursor)(items[items.length - 1], axes) : undefined;
|
|
115
|
-
return { items, total, nextCursor };
|
|
168
|
+
return { items, total, capped, countCap: COUNT_CAP, nextCursor };
|
|
116
169
|
}
|
|
117
170
|
/**
|
|
118
171
|
* 업무 KPI — 시간창 처리량·소요시간·자원 점유. 저널을 **접어서** 만든다(새 계측을 주입하지 않는다).
|
|
@@ -155,7 +208,7 @@ let TwinJournalQuery = class TwinJournalQuery {
|
|
|
155
208
|
const picked = (0, state_axes_js_1.pickAxes)(state, axes);
|
|
156
209
|
/* 오타를 조용히 넘기지 않는다 — 화면이 빈 채로 뜨는 이유를 아무도 모르게 되는 것이 가장 나쁘다. */
|
|
157
210
|
if (picked.unknown.length)
|
|
158
|
-
|
|
211
|
+
(0, log_js_1.twinWarn)(`[twin] twinReplay: unknown axes ignored for "${instanceId}": ${picked.unknown.join(', ')}`);
|
|
159
212
|
if (!attentionLimit || !picked.state?.attentions)
|
|
160
213
|
return picked.state;
|
|
161
214
|
const d = (0, attention_digest_js_1.digestAttentions)(picked.state.attentions, attentionLimit);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"twin-journal-query.js","sourceRoot":"","sources":["../../../server/service/twin-journal/twin-journal-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { Between, LessThanOrEqual, MoreThanOrEqual } from 'typeorm'\nimport { Arg, Args, Ctx, Float, Int, Query, Resolver } from 'type-graphql'\n\nimport { buildNextCursor, cursorSortings, getQueryBuilderFromListParams, getRepository, ListParam, ScalarObject } from '@things-factory/shell'\n\nimport { TwinEvent } from '../twin-event/twin-event.js'\nimport { TwinEventList } from '../twin-event/twin-event-type.js'\nimport { TwinEngine } from '../../engine/index.js'\nimport { pickAxes } from '../../engine/state-axes.js'\nimport { digestAttentions } from '../../engine/attention-digest.js'\nimport { computeTwinKpi, resolveTwinTargets } from '../../engine/kpi-query.js'\n\n/*\n * 저널 읽기 채널 — 상향 query.\n * twinEvents : append-only 이벤트 저널 조회(Ledger/History/Entity360) — instanceId·revision 범위·eventType 필터.\n * twinReplay : 내구 저널을 replay 해 상태 재구성(재부팅 복구·시간여행) — untilRevision 으로 임의 과거.\n * (실시간 스트림은 twinState subscription, 명령은 twin-control mutation. 이건 '질의' 표면.)\n */\n/**\n * 시각 필터 값을 날짜로 — 배열(between)이면 원소마다.\n *\n * 못 읽는 값은 **그대로 둔다.** 여기서 지어내면 사용자가 요청하지 않은 구간을 조회하게 된다.\n */\nfunction toDateValue(v: any): any {\n const one = (x: any) => {\n if (x instanceof Date) return x\n const t = typeof x === 'string' ? Date.parse(x) : NaN\n return Number.isNaN(t) ? x : new Date(t)\n }\n return Array.isArray(v) ? v.map(one) : one(v)\n}\n\n@Resolver()\nexport class TwinJournalQuery {\n @Query(returns => [TwinEvent], {\n description: 'Read the append-only twin event journal (Ledger / History / audit), filtered and ordered by revision.'\n })\n async twinEvents(\n @Arg('instanceId') instanceId: string,\n @Arg('fromRevision', type => Int, { nullable: true }) fromRevision: number,\n @Arg('toRevision', type => Int, { nullable: true }) toRevision: number,\n @Arg('eventType', { nullable: true }) eventType: string,\n @Arg('limit', type => Int, { nullable: true }) limit: number,\n @Ctx() context: ResolverContext,\n @Arg('untilTime', { nullable: true }) untilTime?: string\n ): Promise<TwinEvent[]> {\n const where: any = { domain: { id: context.state.domain.id }, instanceId }\n if (eventType) where.eventType = eventType\n /* 시각 커서(untilTime, ISO) 우선 — eventTime ≤ T(공유 clock). 컬럼이 날짜라 **날짜로 넘긴다.** */\n if (untilTime != null) where.eventTime = LessThanOrEqual(new Date(untilTime))\n else if (fromRevision != null && toRevision != null) where.revision = Between(fromRevision, toRevision)\n else if (fromRevision != null) where.revision = MoreThanOrEqual(fromRevision)\n else if (toRevision != null) where.revision = LessThanOrEqual(toRevision)\n\n /* 최신순(DESC) 기본 — Ledger/recent 뷰. 범위 필터로 시간여행 구간 조회도 가능. */\n return getRepository(TwinEvent).find({ where, order: { revision: 'DESC' }, take: limit ?? 200 })\n }\n\n /**\n * 저널 목록 — things-factory 표준 목록 계약(`ListParam` → `{ items, total }`).\n *\n * ── 왜 `twinEvents` 와 따로 두는가 ─────────────────────────────────────────\n * `twinEvents` 는 배열만 돌려준다. 화면은 자기가 받은 게 전부인지 잘린 건지 알 수 없었고, 그래서\n * 리스트가 **조용히 잘린 채 \"이게 전부\" 처럼** 보였다(원장 limit 120 이 대표적). 총건수를 함께\n * 주면 \"48 / 12,904\" 라고 말할 수 있고, 사용자는 좁혀야 한다는 사실을 안다.\n * 기존 호출자를 깨지 않으려고 `twinEvents` 는 그대로 두고 목록 계약을 새로 연다.\n *\n * ── 검색이 진짜인 이유 ─────────────────────────────────────────────────────\n * `searchables` 는 전부 **인덱스 가능한 승격 컬럼**이다(payload JSON 안이 아니라). 프레임워크\n * 질의 빌더는 `searchables` 에 없는 컬럼의 LIKE 를 경고 후 무시한다 — 인덱스 없는 전체 스캔을\n * 막기 위해서다. 그 규율에 맞추려고 검색 축을 컬럼으로 승격했다(`twin-event-keys.ts`).\n *\n * 정렬 기본값은 revision DESC(최신순) — 저널의 자연 순서이자 ix_twin_event_0 가 그대로 타는 축.\n */\n @Query(returns => TwinEventList, {\n description:\n 'List twin journal events with the standard list contract (filters, pagination, sortings) plus the total match count. ' +\n 'Scope is either one twin (instanceId) or a whole site (spaceId, which folds in every running operational twin co-located there, the same targets twinKpi uses) — one of the two is required. ' +\n 'Searchable fields are the columns promoted out of the payload: epc (item), orderId, locationId, moverId (equipment), bizStep, eventType. ' +\n 'Returns total so a caller can show an honest \"shown of total\" count instead of silently truncating a long journal. ' +\n 'Defaults to newest first (revision descending) and to a page of 100; limit is capped at 500 per page.'\n })\n async twinEventList(\n @Args(type => ListParam) params: ListParam,\n @Ctx() context: ResolverContext,\n @Arg('instanceId', { nullable: true, description: 'Read one twin instance journal.' }) instanceId?: string,\n @Arg('spaceId', { nullable: true, description: 'Read every running operational twin co-located in this space, merged on the shared time axis.' }) spaceId?: string\n ): Promise<TwinEventList> {\n const { domain } = context.state\n if (!instanceId && !spaceId) throw new Error('either instanceId or spaceId is required')\n /* 테넌트 격리 — twinKpi 와 같은 규약. 남의 트윈은 빈 결과가 아니라 명시 실패로 알린다\n * (조용한 빈 목록은 \"권한 없음\" 과 \"데이터 없음\" 을 구분할 수 없게 만든다). */\n if (instanceId && !(await TwinEngine.ownsRegistered(domain.id, instanceId))) {\n throw new Error(`twin instance not found in this tenant: ${instanceId}`)\n }\n\n /* 대상 해소는 KPI 와 같은 창구를 쓴다 — 같은 공간을 보면서 화면마다 대상이 달라지면 안 된다. */\n const instanceIds = await resolveTwinTargets(domain.id, instanceId, spaceId)\n if (instanceIds.length === 0) return { items: [], total: 0 }\n\n /* 페이지 상한 — 화면이 실수로(또는 악의로) 저널 전체를 한 번에 달라고 해도 서버가 버틴다. */\n const limit = Math.min(params.pagination?.limit ?? 100, 500)\n const effective: ListParam = {\n ...params,\n pagination: { page: params.pagination?.page ?? 1, limit },\n /* 기본 정렬은 시각 내림차순 — 여러 트윈을 합칠 때 공통 축은 revision(트윈별 카운터)이 아니라\n * **시각**이다. revision 으로 섞으면 서로 다른 트윈의 무관한 카운터가 뒤엉킨다. */\n sortings: params.sortings?.length ? params.sortings : [{ name: 'eventTime', desc: true }],\n /*\n * 시각 필터는 **날짜로 바꿔서** 질의 빌더에 넘긴다.\n *\n * 화면은 ISO 문자열(`2026-08-07T11:00:00.000Z`)을 보내는데 eventTime 은 날짜 컬럼이고,\n * sqlite 는 그것을 공백 구분 형식(`2026-08-07 11:00:00.000`)으로 저장한다. 문자열 그대로\n * 비교하면 사전식으로 어긋나 **오류 없이 빈 결과**가 나온다(실측으로 확인했다).\n * 화면이 날짜를 어떻게 적어 보내든 서버가 받아 주는 편이 맞다 — 계약을 좁히는 대신 경계에서 옮긴다.\n */\n filters: (params.filters ?? []).map(f => (f.name === 'eventTime' ? { ...f, value: toDateValue(f.value) } : f))\n }\n\n const qb = getQueryBuilderFromListParams({\n repository: getRepository(TwinEvent),\n params: effective,\n domain,\n /* 전부 인덱스 가능한 승격 컬럼 — 자유 검색어는 서버가 이 축들로 펼친다. */\n searchables: ['epc', 'orderId', 'locationId', 'moverId', 'bizStep', 'eventType', 'correlationId'],\n /* 정렬은 인덱스가 받쳐 주는 축으로만 — 큰 저널에서 임의 컬럼 정렬은 전체 정렬 스캔이다. */\n sortables: ['eventTime', 'revision', 'eventType', 'bizStep'],\n defaultLimit: 100,\n maxLimit: 500\n })\n /* 대상 범위는 호출자가 필터로 빼먹을 수 있는 값이 아니다 — 계약상 필수라 여기서 강제한다. */\n qb.andWhere(`${qb.alias}.instanceId IN (:...instanceIds)`, { instanceIds })\n\n const [items, total] = await qb.getManyAndCount()\n\n /* 다음 커서는 **이번 페이지의 마지막 행**에서 만든다. 페이지가 상한보다 짧으면 뒤가 없다. */\n const axes = cursorSortings(effective.sortings, 'id')\n const nextCursor = items.length === limit ? buildNextCursor(items[items.length - 1], axes) : undefined\n\n return { items, total, nextCursor }\n }\n\n /**\n * 업무 KPI — 시간창 처리량·소요시간·자원 점유. 저널을 **접어서** 만든다(새 계측을 주입하지 않는다).\n *\n * `twinMetrics`(초당 이벤트 수 = 인프라 부하)와 **다른 것**이다: 이건 \"지난 한 시간에 몇 건 처리했고\n * 한 건에 얼마나 걸렸나\" 다. 이름이 비슷해 헷갈리기 쉬우므로 설명에 명시한다.\n *\n * 완료된 작업의 **시작 이벤트가 창 앞에 있을 수 있어** 조회 구간을 창보다 앞으로 넓힌다(lookback).\n * 그래도 못 찾은 것은 결과의 `unpaired` 로 드러난다 — 평균에서 조용히 빠지면 지표가 거짓이 된다.\n */\n @Query(returns => ScalarObject, {\n description:\n 'Windowed business KPI folded from the event journal, for one space (every running twin in it, folded together — averaging per-twin percentiles would be wrong) or one twin: throughput (completed tasks / orders), lead / work / wait time distributions (p50·p90), and per-resource busy ratio. Distinct from twinMetrics, which reports infrastructure throughput (events per second). When toTime is omitted the window ends at the twin\\'s own last recorded event time, not wall clock — a simulated twin lives on its own clock, so asking for \\'the last hour\\' in real time would always find nothing. The basis used is reported in window.basis. Pass groupBy (resource | taskKind | location | order | area | shift) to break the same window down by that perspective. The shift axis is **computed, not stored**: the journal records only times, and the shift is derived from each completion\\'s time plus the declared site shifts and time zone — so correcting a shift definition re-counts history correctly instead of leaving it frozen under the old boundaries. Sites that declare no shifts report every group as unknown rather than inventing shift names — per-group numbers use the same rules as the totals, so group counts sum to the total, and groups beyond groupLimit are reported as groups.truncated (how many groups) and groups.truncatedTasks (how much work those groups held) rather than silently dropped. Pass compareTo (previous | yesterday) to fold the same-length window shifted back and get comparison.delta (current minus that window); when the comparison window has no records, comparison.measured is false and no delta is produced rather than reporting zero change. Passing groupBy together with compareTo crosses the two: each group carries prevTasks / deltaTasks / deltaWorkP50Ms, groups absent from the comparison window are flagged isNew (no delta, so growth is not implied), and groups that had work before but none now are returned in groups.disappeared so a stopped area is never silently missing from the table. Area breakdown needs model locations to carry a parent area; when they do not, groups.note says so instead of implying there was no work. When the journal read hits its row cap, eventsCapped is true and every number is a lower bound — narrow the window and ask again rather than reading the smaller figures as a drop in performance. When operational targets are set for the space or instance, `targets` carries the verdicts [{metric, actual, target, direction, met, ratio}] — ratio is normalised so that 1 or more is always good, letting throughput and duration metrics be compared on one scale. A metric with no target, or with no measurement in the window, is absent from `targets` rather than reported as a miss, because a period with no work would otherwise read as a perfect score on duration metrics. Completions are counted in the window they complete; starts are looked up before the window, and any completion whose start was not found is reported in `unpaired` rather than silently dropped. Pass baselinePeriods (1-14, with baselineStride day | week) to fold the same time-of-day window on each of the last N days (or the same weekday on each of the last N weeks) and get `baseline.metrics` — per metric the distribution of those windows (min / median / max) plus where the current window sits: `rank` (1 = smallest, direction-free) and `standing` (best | better-than-typical | typical | worse-than-typical | worst, which does apply the metric direction). This answers what a single comparison cannot: comparing to yesterday alone is useless if yesterday was also bad. Every requested window is returned in `samples` with `measured` true or false — windows without records are kept and counted (measuredCount vs requested) rather than dropped, so \"the last 7 days\" never silently means 3. No position is reported when the current window has no value or fewer than two baseline windows were measured; `noPosition` says which, because a missing distribution drawn as neutral would read as \"typical\". The baseline is produced even when the current window itself has no records, since that is exactly when knowing the usual level matters (a stopped line).'\n })\n async twinKpi(\n @Ctx() context: ResolverContext,\n /* 공간이 사용자의 단위다 — 한 트윈만 보려면 instanceId 를 준다(둘 중 하나 필수). */\n @Arg('spaceId', { nullable: true }) spaceId?: string,\n @Arg('instanceId', { nullable: true }) instanceId?: string,\n /** 창 시작·끝(ISO). 미지정이면 최근 1시간. */\n @Arg('fromTime', { nullable: true }) fromTime?: string,\n @Arg('toTime', { nullable: true }) toTime?: string,\n /** 창 길이(분). fromTime 이 있으면 무시. 기본 60. */\n @Arg('windowMinutes', type => Int, { nullable: true }) windowMinutes?: number,\n /** 추세를 위해 창을 N 등분(1~48). 미지정이면 쪼개지 않는다. */\n @Arg('buckets', type => Int, { nullable: true }) buckets?: number,\n /** 시작 이벤트를 찾기 위해 창보다 앞으로 더 읽는 시간(분). 기본 60. */\n @Arg('lookbackMinutes', type => Int, { nullable: true }) lookbackMinutes?: number,\n /** 관점 축 — 같은 창을 자원별·작업종류별·도착지점별·오더별·구역별로 쪼갠다. */\n @Arg('groupBy', { nullable: true }) groupBy?: string,\n /** 축의 상한(기본 20, 최대 100). 넘치면 잘라내고 잘린 수를 알린다. */\n @Arg('groupLimit', type => Int, { nullable: true }) groupLimit?: number,\n /** 비교 기준 — 같은 길이의 앞 구간(previous)이나 하루 전 같은 시간(yesterday). */\n @Arg('compareTo', { nullable: true }) compareTo?: string,\n /** 여러 구간 동시 비교 — 최근 N 구간의 같은 시간대 분포(1~14). */\n @Arg('baselinePeriods', type => Int, { nullable: true }) baselinePeriods?: number,\n /** 분포의 간격 — day(같은 시간대) | week(같은 요일·시간대). 기본 day. */\n @Arg('baselineStride', { nullable: true }) baselineStride?: string\n ): Promise<any> {\n /* 테넌트 격리 — 트윈 지정이면 소속을 확인한다. 공간 지정이면 대상 해소가 도메인 목록에서\n * 이뤄지므로(computeTwinKpi) 남의 트윈이 섞이지 않는다. 조용한 빈 결과가 아니라 명시 실패. */\n if (instanceId && !(await TwinEngine.ownsRegistered(context.state.domain.id, instanceId))) {\n throw new Error(`twin instance not found in this tenant: ${instanceId}`)\n }\n if (!instanceId && !spaceId) throw new Error('either spaceId or instanceId is required')\n /* 창 해소·조회·폴드는 `computeTwinKpi` 가 소유한다 — 같은 규칙을 AI 도구와 공유해야 한다\n * (두 곳에 적으면 화면과 AI 가 다른 숫자를 말한다). */\n return computeTwinKpi({\n domainId: context.state.domain.id,\n instanceId,\n spaceId,\n fromTime,\n toTime,\n windowMinutes,\n buckets,\n lookbackMinutes,\n groupBy: groupBy as any,\n groupLimit,\n compareTo: compareTo as any,\n ...(baselinePeriods && baselinePeriods > 0\n ? { baseline: { periods: baselinePeriods, stride: (baselineStride as any) === 'week' ? 'week' : 'day' } }\n : {})\n })\n }\n\n @Query(returns => ScalarObject, {\n nullable: true,\n description: 'Reconstruct twin state from the durable journal (DB → replay). untilTime (ISO) for time-travel on the shared clock (preferred); untilRevision for the lens-internal counter; omit both for latest.'\n })\n async twinReplay(\n @Arg('instanceId') instanceId: string,\n @Arg('untilRevision', type => Int, { nullable: true }) untilRevision: number,\n @Arg('untilTime', { nullable: true }) untilTime: string,\n @Ctx() context: ResolverContext,\n /*\n * 필요한 축만 — 지도는 `locations`·`attentions` 만 쓰는데 상태를 통째로 받고 있었다(실 저널로\n * 재 보니 트윈 하나가 최대 710KB, 같은 공간에 셋이면 약 2MB). 화면 전용 질의를 만들지 않고\n * 호출부가 축을 말한다(무방언 — 어휘가 화면 이름이 아니라 상태의 축이다).\n * 말하지 않으면 자르지 않는다(기존 호출부 무영향).\n */\n @Arg('axes', type => [String], { nullable: true }) axes?: string[],\n /*\n * 신호는 몇 건만 필요한가 — 레일은 20건을 그리고 나머지는 \"N건 더\" 라고만 말한다. 그런데 신호가\n * 많은 트윈은 그 전량이 108KB 다(`hatio-yard` 397건). 상위 N 만 보내고 **전체 개수와 자리별\n * 최고 심각도는 전량을 근거로** 함께 보낸다 — 레일은 잘라도 되지만 지도 색은 잘라선 안 된다.\n * 말하지 않으면 자르지 않는다.\n */\n @Arg('attentionLimit', type => Int, { nullable: true }) attentionLimit?: number\n ): Promise<any> {\n const state = await TwinEngine.recover(context.state.domain.id, instanceId, untilRevision ?? undefined, untilTime ?? undefined)\n const picked = pickAxes(state, axes)\n /* 오타를 조용히 넘기지 않는다 — 화면이 빈 채로 뜨는 이유를 아무도 모르게 되는 것이 가장 나쁘다. */\n if (picked.unknown.length) console.warn(`[twin] twinReplay: unknown axes ignored for \"${instanceId}\": ${picked.unknown.join(', ')}`)\n if (!attentionLimit || !picked.state?.attentions) return picked.state\n const d = digestAttentions(picked.state.attentions, attentionLimit)\n /* 잘랐다는 사실을 값으로 남긴다 — 개수만 줄어들면 화면은 \"괜찮아졌다\" 로 읽는다. */\n return { ...picked.state, attentions: d.attentions, attentionTotal: d.attentionTotal, severityByLocation: d.severityByLocation }\n }\n\n @Query(returns => ScalarObject, {\n nullable: true,\n description:\n 'Steady-state capacity of a twin against a declared demand: per-operation supply vs required across equipment, personnel, physical assets and locations, plus the binding constraint and the daily ceiling. Answers \"can this plant meet the target?\" without running a simulation. Null when the twin is not running.'\n })\n async twinCapacity(\n @Arg('instanceId', { nullable: true }) instanceId: string,\n @Arg('spaceId', { nullable: true }) spaceId: string,\n /** 하루 몇 대를 낼 것인가 — 화면이 보고 있는 목표에서 나온다(선언된 수요). */\n @Arg('unitsPerDay', type => Float) unitsPerDay: number,\n /**\n * 가용 시간을 잴 기준 주의 시작(ISO, 월요일). 공휴일이 없는 평상주여야 한다 —\n * 공휴일은 연간 가용량을 따로 깎지, 이 공장의 평상시 상한을 정하지 않는다.\n */\n @Arg('sampleWeekStart', { nullable: true }) sampleWeekStart: string,\n @Ctx() context: ResolverContext\n ): Promise<any> {\n if (instanceId && !(await TwinEngine.ownsRegistered(context.state.domain.id, instanceId))) {\n throw new Error(`twin instance not found in this tenant: ${instanceId}`)\n }\n if (!instanceId && !spaceId) throw new Error('either spaceId or instanceId is required')\n if (!(unitsPerDay > 0)) throw new Error('unitsPerDay must be greater than zero')\n\n /*\n * 기준 주를 안 주면 **이번 주 월요일**을 쓴다. 그 주에 공휴일이 끼면 상한이 조금 낮게 나오는데,\n * 그것은 조용한 오류가 아니라 **이번 주의 실제 가용 시간**이라 답으로서 정직하다. 평상시 상한을\n * 보려는 호출자는 평상주를 지정한다.\n */\n const base = sampleWeekStart ? Date.parse(sampleWeekStart) : Date.now()\n if (Number.isNaN(base)) throw new Error(`sampleWeekStart is not a valid ISO time: ${sampleWeekStart}`)\n const d = new Date(base)\n const monday = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() - ((d.getUTCDay() + 6) % 7))\n\n return TwinEngine.capacity(context.state.domain.id, { instanceId, spaceId }, unitsPerDay, monday)\n }\n\n @Query(returns => ScalarObject, {\n nullable: true,\n description:\n 'Operations capability of a twin for a time window (ISA-95 Part 4 OperationsCapability): per operation the ceiling for that window split into Committed (already promised), Unattainable (lost to resources that cannot be used — down, held, off-shift, expired qualification) and Available (what is left). Answers \"can we take more work?\" — which the steady-state rate alone cannot. Null when the twin is not running.'\n })\n async twinOperationsCapability(\n @Arg('instanceId', { nullable: true }) instanceId: string,\n @Arg('spaceId', { nullable: true }) spaceId: string,\n @Arg('unitsPerDay', type => Float) unitsPerDay: number,\n /** 능력을 물을 구간 — **구간 없이 능력은 존재하지 않는다**(표준 StartTime/EndTime). */\n @Arg('fromTime') fromTime: string,\n @Arg('toTime') toTime: string,\n @Arg('sampleWeekStart', { nullable: true }) sampleWeekStart: string,\n @Ctx() context: ResolverContext\n ): Promise<any> {\n if (instanceId && !(await TwinEngine.ownsRegistered(context.state.domain.id, instanceId))) {\n throw new Error(`twin instance not found in this tenant: ${instanceId}`)\n }\n if (!instanceId && !spaceId) throw new Error('either spaceId or instanceId is required')\n if (!(unitsPerDay > 0)) throw new Error('unitsPerDay must be greater than zero')\n /* 구간은 **부르는 쪽이 말해야 한다** — 기본값을 두면 화면마다 다른 구간을 보면서 같은 값으로 읽는다. */\n const from = Date.parse(fromTime)\n const to = Date.parse(toTime)\n if (Number.isNaN(from) || Number.isNaN(to)) throw new Error(`window is not a valid ISO time: ${fromTime} ~ ${toTime}`)\n if (to <= from) throw new Error(`window is inverted: ${fromTime} ~ ${toTime}`)\n\n /* 기준 주 규율은 `twinCapacity` 와 같다(같은 계산을 쓰므로 같은 주를 골라야 한다). */\n const base = sampleWeekStart ? Date.parse(sampleWeekStart) : from\n if (Number.isNaN(base)) throw new Error(`sampleWeekStart is not a valid ISO time: ${sampleWeekStart}`)\n const d = new Date(base)\n const monday = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() - ((d.getUTCDay() + 6) % 7))\n\n return TwinEngine.operationsCapability(\n context.state.domain.id,\n { instanceId, spaceId },\n unitsPerDay,\n monday,\n { fromTime, toTime }\n )\n }\n\n @Query(returns => ScalarObject, {\n description:\n \"Structural revisions a twin has been through, newest last: when each took effect, how many journal events happened under it, and what changed from the previous one (counts by location type and equipment kind). The model itself is not returned — a revision holds the whole plant, but what a person wants is the difference.\"\n })\n async twinStructures(@Arg('instanceId') instanceId: string, @Ctx() context: ResolverContext): Promise<any> {\n /* 이력 질문이므로 **등록부**에 소유를 묻는다 — 꺼진 트윈의 이력도 읽을 수 있어야 한다. */\n if (!(await TwinEngine.ownsRegistered(context.state.domain.id, instanceId))) {\n throw new Error(`twin instance not found in this tenant: ${instanceId}`)\n }\n return TwinEngine.structureHistory(context.state.domain.id, instanceId)\n }\n\n @Query(returns => ScalarObject, {\n description: 'Event time range (min/max ISO) across all co-located twins of a space — the history scrubber time axis.'\n })\n async twinTimeRange(\n @Arg('spaceId') spaceId: string,\n @Ctx() context: ResolverContext\n ): Promise<{ minTime: string | null; maxTime: string | null }> {\n return TwinEngine.timeRange(context.state.domain.id, spaceId)\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"twin-journal-query.js","sourceRoot":"","sources":["../../../server/service/twin-journal/twin-journal-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { twinWarn } from '../../engine/log.js'\nimport { Between, LessThanOrEqual, MoreThanOrEqual } from 'typeorm'\nimport { Arg, Args, Ctx, Float, Int, Query, Resolver } from 'type-graphql'\n\nimport { buildNextCursor, cursorSortings, getQueryBuilderFromListParams, getRepository, ListParam, ScalarObject } from '@things-factory/shell'\n\nimport { TwinEvent } from '../twin-event/twin-event.js'\nimport { JOURNAL_COUNT_CAP } from '../twin-event/journal-count.js'\nimport { TwinEventList } from '../twin-event/twin-event-type.js'\nimport { TwinEngine } from '../../engine/index.js'\nimport { pickAxes } from '../../engine/state-axes.js'\nimport { digestAttentions } from '../../engine/attention-digest.js'\nimport { computeTwinKpi, resolveTwinTargets } from '../../engine/kpi-query.js'\n\n/*\n * 저널 읽기 채널 — 상향 query.\n * twinEvents : append-only 이벤트 저널 조회(Ledger/History/Entity360) — instanceId·revision 범위·eventType 필터.\n * twinReplay : 내구 저널을 replay 해 상태 재구성(재부팅 복구·시간여행) — untilRevision 으로 임의 과거.\n * (실시간 스트림은 twinState subscription, 명령은 twin-control mutation. 이건 '질의' 표면.)\n */\n/*\n * 건수 상한은 **저널 세기의 문**에서 온다(`journal-count.ts`) — 값과 규율이 두 곳에 흩어지면 한쪽만\n * 고쳐진다. 근거(2026-08-22 실측): 상한 1만이면 0.04초, 상한 없이 세면 5.32초(저널 358만 행).\n */\nconst COUNT_CAP = JOURNAL_COUNT_CAP\n\n/**\n * 시각 필터 값을 날짜로 — 배열(between)이면 원소마다.\n *\n * 못 읽는 값은 **그대로 둔다.** 여기서 지어내면 사용자가 요청하지 않은 구간을 조회하게 된다.\n */\nfunction toDateValue(v: any): any {\n const one = (x: any) => {\n if (x instanceof Date) return x\n const t = typeof x === 'string' ? Date.parse(x) : NaN\n return Number.isNaN(t) ? x : new Date(t)\n }\n return Array.isArray(v) ? v.map(one) : one(v)\n}\n\n@Resolver()\nexport class TwinJournalQuery {\n @Query(returns => [TwinEvent], {\n description: 'Read the append-only twin event journal (Ledger / History / audit), filtered and ordered by revision.'\n })\n async twinEvents(\n @Arg('instanceId') instanceId: string,\n @Arg('fromRevision', type => Int, { nullable: true }) fromRevision: number,\n @Arg('toRevision', type => Int, { nullable: true }) toRevision: number,\n @Arg('eventType', { nullable: true }) eventType: string,\n @Arg('limit', type => Int, { nullable: true }) limit: number,\n @Ctx() context: ResolverContext,\n @Arg('untilTime', { nullable: true }) untilTime?: string\n ): Promise<TwinEvent[]> {\n const where: any = { domain: { id: context.state.domain.id }, instanceId }\n if (eventType) where.eventType = eventType\n /* 시각 커서(untilTime, ISO) 우선 — eventTime ≤ T(공유 clock). 컬럼이 날짜라 **날짜로 넘긴다.** */\n if (untilTime != null) where.eventTime = LessThanOrEqual(new Date(untilTime))\n else if (fromRevision != null && toRevision != null) where.revision = Between(fromRevision, toRevision)\n else if (fromRevision != null) where.revision = MoreThanOrEqual(fromRevision)\n else if (toRevision != null) where.revision = LessThanOrEqual(toRevision)\n\n /*\n * 최신순(DESC) 기본 — Ledger/recent 뷰. 범위 필터로 시간여행 구간 조회도 가능.\n *\n * ── 정렬 축을 **거르는 축과 맞춘다** (2026-08-22 실측) ─────────────────────\n * `untilTime` 경로는 `eventTime <= T` 로 거르면서 `revision` 으로 정렬했다. 그러면 sqlite 는 조건에\n * 맞는 행을 **전부 찾아 전부 정렬한 뒤** 상한을 적용한다 — 상한이 정렬 뒤에 걸리므로 200건을 얻는\n * 데도 전량 정렬을 치른다. 실측(인스턴스 하나가 375만 행):\n *\n * ORDER BY revision DESC → 33.4초\n * ORDER BY eventTime DESC, revision DESC → 0.007초 (4,700배)\n *\n * 계획이 그것을 그대로 말한다: 앞은 `USE TEMP B-TREE FOR ORDER BY`(전체 정렬), 뒤는\n * `... FOR LAST TERM OF ORDER BY` — 시각은 인덱스(`ix_twin_event_1`)에서 순서대로 나오고\n * **동시각만** 임시 정렬한다.\n *\n * **리비전 경로는 그대로 둔다.** 그때는 거르는 축이 `revision` 이고 정렬도 `revision` 이라 이미\n * 맞아 있다(`ix_twin_event_0`). 축이 어긋난 경로만 고친다 — 맞는 자리를 건드리면 그쪽이 어긋난다.\n *\n * 순서의 뜻이 **완전히 같지는 않다**(측정해서 고친 문장): `revision` 순서와 `eventTime` 순서가\n * 어긋나는 자리가 실재한다(426만 행 중 7건 — 미러는 원본의 시각을 싣고 도착 순서로 리비전을 받는다).\n * 이 목록은 사람이 이력을 훑는 화면이고 **시각순으로 보는 것이 그 화면의 뜻**이므로, 그 어긋남은\n * 결함이 아니라 오히려 맞는 순서다. 동시각의 순서를 위해 `revision` 을 둘째 축으로 남긴다.\n */\n const order: any = untilTime != null ? { eventTime: 'DESC', revision: 'DESC' } : { revision: 'DESC' }\n return getRepository(TwinEvent).find({ where, order, take: limit ?? 200 })\n }\n\n /**\n * 저널 목록 — things-factory 표준 목록 계약(`ListParam` → `{ items, total }`).\n *\n * ── 왜 `twinEvents` 와 따로 두는가 ─────────────────────────────────────────\n * `twinEvents` 는 배열만 돌려준다. 화면은 자기가 받은 게 전부인지 잘린 건지 알 수 없었고, 그래서\n * 리스트가 **조용히 잘린 채 \"이게 전부\" 처럼** 보였다(원장 limit 120 이 대표적). 총건수를 함께\n * 주면 \"48 / 12,904\" 라고 말할 수 있고, 사용자는 좁혀야 한다는 사실을 안다.\n * 기존 호출자를 깨지 않으려고 `twinEvents` 는 그대로 두고 목록 계약을 새로 연다.\n *\n * ── 검색이 진짜인 이유 ─────────────────────────────────────────────────────\n * `searchables` 는 전부 **인덱스 가능한 승격 컬럼**이다(payload JSON 안이 아니라). 프레임워크\n * 질의 빌더는 `searchables` 에 없는 컬럼의 LIKE 를 경고 후 무시한다 — 인덱스 없는 전체 스캔을\n * 막기 위해서다. 그 규율에 맞추려고 검색 축을 컬럼으로 승격했다(`twin-event-keys.ts`).\n *\n * 정렬 기본값은 revision DESC(최신순) — 저널의 자연 순서이자 ix_twin_event_0 가 그대로 타는 축.\n */\n @Query(returns => TwinEventList, {\n description:\n 'List twin journal events with the standard list contract (filters, pagination, sortings) plus the total match count. ' +\n 'Scope is either one twin (instanceId) or a whole site (spaceId, which folds in every running operational twin co-located there, the same targets twinKpi uses) — one of the two is required. ' +\n 'Searchable fields are the columns promoted out of the payload: epc (item), orderId, locationId, moverId (equipment), bizStep, eventType. ' +\n 'Returns total so a caller can show an honest \"shown of total\" count instead of silently truncating a long journal. ' +\n 'Defaults to newest first (revision descending) and to a page of 100; limit is capped at 500 per page.'\n })\n async twinEventList(\n @Args(type => ListParam) params: ListParam,\n @Ctx() context: ResolverContext,\n @Arg('instanceId', { nullable: true, description: 'Read one twin instance journal.' }) instanceId?: string,\n @Arg('spaceId', { nullable: true, description: 'Read every running operational twin co-located in this space, merged on the shared time axis.' }) spaceId?: string\n ): Promise<TwinEventList> {\n const { domain } = context.state\n if (!instanceId && !spaceId) throw new Error('either instanceId or spaceId is required')\n /* 테넌트 격리 — twinKpi 와 같은 규약. 남의 트윈은 빈 결과가 아니라 명시 실패로 알린다\n * (조용한 빈 목록은 \"권한 없음\" 과 \"데이터 없음\" 을 구분할 수 없게 만든다). */\n if (instanceId && !(await TwinEngine.ownsRegistered(domain.id, instanceId))) {\n throw new Error(`twin instance not found in this tenant: ${instanceId}`)\n }\n\n /* 대상 해소는 KPI 와 같은 창구를 쓴다 — 같은 공간을 보면서 화면마다 대상이 달라지면 안 된다. */\n const instanceIds = await resolveTwinTargets(domain.id, instanceId, spaceId)\n if (instanceIds.length === 0) return { items: [], total: 0 }\n\n /* 페이지 상한 — 화면이 실수로(또는 악의로) 저널 전체를 한 번에 달라고 해도 서버가 버틴다. */\n const limit = Math.min(params.pagination?.limit ?? 100, 500)\n const effective: ListParam = {\n ...params,\n pagination: { page: params.pagination?.page ?? 1, limit },\n /* 기본 정렬은 시각 내림차순 — 여러 트윈을 합칠 때 공통 축은 revision(트윈별 카운터)이 아니라\n * **시각**이다. revision 으로 섞으면 서로 다른 트윈의 무관한 카운터가 뒤엉킨다. */\n sortings: params.sortings?.length ? params.sortings : [{ name: 'eventTime', desc: true }],\n /*\n * 시각 필터는 **날짜로 바꿔서** 질의 빌더에 넘긴다.\n *\n * 화면은 ISO 문자열(`2026-08-07T11:00:00.000Z`)을 보내는데 eventTime 은 날짜 컬럼이고,\n * sqlite 는 그것을 공백 구분 형식(`2026-08-07 11:00:00.000`)으로 저장한다. 문자열 그대로\n * 비교하면 사전식으로 어긋나 **오류 없이 빈 결과**가 나온다(실측으로 확인했다).\n * 화면이 날짜를 어떻게 적어 보내든 서버가 받아 주는 편이 맞다 — 계약을 좁히는 대신 경계에서 옮긴다.\n */\n filters: (params.filters ?? []).map(f => (f.name === 'eventTime' ? { ...f, value: toDateValue(f.value) } : f))\n }\n\n const qb = getQueryBuilderFromListParams({\n repository: getRepository(TwinEvent),\n params: effective,\n domain,\n /* 전부 인덱스 가능한 승격 컬럼 — 자유 검색어는 서버가 이 축들로 펼친다. */\n searchables: ['epc', 'orderId', 'locationId', 'moverId', 'bizStep', 'eventType', 'correlationId'],\n /* 정렬은 인덱스가 받쳐 주는 축으로만 — 큰 저널에서 임의 컬럼 정렬은 전체 정렬 스캔이다. */\n sortables: ['eventTime', 'revision', 'eventType', 'bizStep'],\n defaultLimit: 100,\n maxLimit: 500\n })\n /* 대상 범위는 호출자가 필터로 빼먹을 수 있는 값이 아니다 — 계약상 필수라 여기서 강제한다. */\n qb.andWhere(`${qb.alias}.instanceId IN (:...instanceIds)`, { instanceIds })\n\n /*\n * ── 건수는 **상한까지만** 센다 (2026-08-22 실측) ──────────────────────────\n * 예전에는 `getManyAndCount()` 였다. 그 두 번째 질의가 조건에 맞는 행을 **전부** 세는데, 실측한\n * 개발 저널에서 트윈 하나가 358만 행이라 **한 번에 5.32초**였다. 이력 화면을 열 때마다 그것을\n * 물었고, 그 5초 동안 이벤트 루프가 다른 요청을 처리하지 못했다.\n *\n * 상한을 두면 같은 답이 **0.04초**다(130배). 상한을 넘으면 정확한 수를 모르는 것이 사실이므로\n * 목록 규약의 「서버 상한에 부딪힘(total 모름)」 갈래로 낸다 — 화면은 「처음 N 건만」이라고 말하고\n * 좁히기를 권한다. **조용히 자르지 않는다**: 상한을 넘었다는 사실을 `capped` 로 함께 낸다.\n *\n * 왜 하위 질의로 세지 않나: 5개 드라이버에서 같이 도는 형태여야 한다(이 저장소의 절대 규칙).\n * 그래서 **키 컬럼만 상한까지 읽어 세는** 방식을 쓴다 — 색인만 읽으므로 행 본문을 만들지 않는다.\n */\n const items = await qb.getMany()\n const probe = qb\n .clone()\n .orderBy() /* 세는 데 순서는 필요 없다 — 정렬을 남기면 임의 축에서 전체 정렬이 된다. */\n .skip(0)\n .take(COUNT_CAP + 1)\n .select(`${qb.alias}.id`)\n const counted = await probe.getRawMany()\n const capped = counted.length > COUNT_CAP\n /* 상한을 넘었으면 **전체 수를 모른다** — 상한값을 전체로 적으면 화면이 거짓을 말한다. */\n const total = capped ? undefined : counted.length\n\n /* 다음 커서는 **이번 페이지의 마지막 행**에서 만든다. 페이지가 상한보다 짧으면 뒤가 없다. */\n const axes = cursorSortings(effective.sortings, 'id')\n const nextCursor = items.length === limit ? buildNextCursor(items[items.length - 1], axes) : undefined\n\n return { items, total, capped, countCap: COUNT_CAP, nextCursor }\n }\n\n /**\n * 업무 KPI — 시간창 처리량·소요시간·자원 점유. 저널을 **접어서** 만든다(새 계측을 주입하지 않는다).\n *\n * `twinMetrics`(초당 이벤트 수 = 인프라 부하)와 **다른 것**이다: 이건 \"지난 한 시간에 몇 건 처리했고\n * 한 건에 얼마나 걸렸나\" 다. 이름이 비슷해 헷갈리기 쉬우므로 설명에 명시한다.\n *\n * 완료된 작업의 **시작 이벤트가 창 앞에 있을 수 있어** 조회 구간을 창보다 앞으로 넓힌다(lookback).\n * 그래도 못 찾은 것은 결과의 `unpaired` 로 드러난다 — 평균에서 조용히 빠지면 지표가 거짓이 된다.\n */\n @Query(returns => ScalarObject, {\n description:\n 'Windowed business KPI folded from the event journal, for one space (every running twin in it, folded together — averaging per-twin percentiles would be wrong) or one twin: throughput (completed tasks / orders), lead / work / wait time distributions (p50·p90), and per-resource busy ratio. Distinct from twinMetrics, which reports infrastructure throughput (events per second). When toTime is omitted the window ends at the twin\\'s own last recorded event time, not wall clock — a simulated twin lives on its own clock, so asking for \\'the last hour\\' in real time would always find nothing. The basis used is reported in window.basis. Pass groupBy (resource | taskKind | location | order | area | shift) to break the same window down by that perspective. The shift axis is **computed, not stored**: the journal records only times, and the shift is derived from each completion\\'s time plus the declared site shifts and time zone — so correcting a shift definition re-counts history correctly instead of leaving it frozen under the old boundaries. Sites that declare no shifts report every group as unknown rather than inventing shift names — per-group numbers use the same rules as the totals, so group counts sum to the total, and groups beyond groupLimit are reported as groups.truncated (how many groups) and groups.truncatedTasks (how much work those groups held) rather than silently dropped. Pass compareTo (previous | yesterday) to fold the same-length window shifted back and get comparison.delta (current minus that window); when the comparison window has no records, comparison.measured is false and no delta is produced rather than reporting zero change. Passing groupBy together with compareTo crosses the two: each group carries prevTasks / deltaTasks / deltaWorkP50Ms, groups absent from the comparison window are flagged isNew (no delta, so growth is not implied), and groups that had work before but none now are returned in groups.disappeared so a stopped area is never silently missing from the table. Area breakdown needs model locations to carry a parent area; when they do not, groups.note says so instead of implying there was no work. When the journal read hits its row cap, eventsCapped is true and every number is a lower bound — narrow the window and ask again rather than reading the smaller figures as a drop in performance. When operational targets are set for the space or instance, `targets` carries the verdicts [{metric, actual, target, direction, met, ratio}] — ratio is normalised so that 1 or more is always good, letting throughput and duration metrics be compared on one scale. A metric with no target, or with no measurement in the window, is absent from `targets` rather than reported as a miss, because a period with no work would otherwise read as a perfect score on duration metrics. Completions are counted in the window they complete; starts are looked up before the window, and any completion whose start was not found is reported in `unpaired` rather than silently dropped. Pass baselinePeriods (1-14, with baselineStride day | week) to fold the same time-of-day window on each of the last N days (or the same weekday on each of the last N weeks) and get `baseline.metrics` — per metric the distribution of those windows (min / median / max) plus where the current window sits: `rank` (1 = smallest, direction-free) and `standing` (best | better-than-typical | typical | worse-than-typical | worst, which does apply the metric direction). This answers what a single comparison cannot: comparing to yesterday alone is useless if yesterday was also bad. Every requested window is returned in `samples` with `measured` true or false — windows without records are kept and counted (measuredCount vs requested) rather than dropped, so \"the last 7 days\" never silently means 3. No position is reported when the current window has no value or fewer than two baseline windows were measured; `noPosition` says which, because a missing distribution drawn as neutral would read as \"typical\". The baseline is produced even when the current window itself has no records, since that is exactly when knowing the usual level matters (a stopped line).'\n })\n async twinKpi(\n @Ctx() context: ResolverContext,\n /* 공간이 사용자의 단위다 — 한 트윈만 보려면 instanceId 를 준다(둘 중 하나 필수). */\n @Arg('spaceId', { nullable: true }) spaceId?: string,\n @Arg('instanceId', { nullable: true }) instanceId?: string,\n /** 창 시작·끝(ISO). 미지정이면 최근 1시간. */\n @Arg('fromTime', { nullable: true }) fromTime?: string,\n @Arg('toTime', { nullable: true }) toTime?: string,\n /** 창 길이(분). fromTime 이 있으면 무시. 기본 60. */\n @Arg('windowMinutes', type => Int, { nullable: true }) windowMinutes?: number,\n /** 추세를 위해 창을 N 등분(1~48). 미지정이면 쪼개지 않는다. */\n @Arg('buckets', type => Int, { nullable: true }) buckets?: number,\n /** 시작 이벤트를 찾기 위해 창보다 앞으로 더 읽는 시간(분). 기본 60. */\n @Arg('lookbackMinutes', type => Int, { nullable: true }) lookbackMinutes?: number,\n /** 관점 축 — 같은 창을 자원별·작업종류별·도착지점별·오더별·구역별로 쪼갠다. */\n @Arg('groupBy', { nullable: true }) groupBy?: string,\n /** 축의 상한(기본 20, 최대 100). 넘치면 잘라내고 잘린 수를 알린다. */\n @Arg('groupLimit', type => Int, { nullable: true }) groupLimit?: number,\n /** 비교 기준 — 같은 길이의 앞 구간(previous)이나 하루 전 같은 시간(yesterday). */\n @Arg('compareTo', { nullable: true }) compareTo?: string,\n /** 여러 구간 동시 비교 — 최근 N 구간의 같은 시간대 분포(1~14). */\n @Arg('baselinePeriods', type => Int, { nullable: true }) baselinePeriods?: number,\n /** 분포의 간격 — day(같은 시간대) | week(같은 요일·시간대). 기본 day. */\n @Arg('baselineStride', { nullable: true }) baselineStride?: string\n ): Promise<any> {\n /* 테넌트 격리 — 트윈 지정이면 소속을 확인한다. 공간 지정이면 대상 해소가 도메인 목록에서\n * 이뤄지므로(computeTwinKpi) 남의 트윈이 섞이지 않는다. 조용한 빈 결과가 아니라 명시 실패. */\n if (instanceId && !(await TwinEngine.ownsRegistered(context.state.domain.id, instanceId))) {\n throw new Error(`twin instance not found in this tenant: ${instanceId}`)\n }\n if (!instanceId && !spaceId) throw new Error('either spaceId or instanceId is required')\n /* 창 해소·조회·폴드는 `computeTwinKpi` 가 소유한다 — 같은 규칙을 AI 도구와 공유해야 한다\n * (두 곳에 적으면 화면과 AI 가 다른 숫자를 말한다). */\n return computeTwinKpi({\n domainId: context.state.domain.id,\n instanceId,\n spaceId,\n fromTime,\n toTime,\n windowMinutes,\n buckets,\n lookbackMinutes,\n groupBy: groupBy as any,\n groupLimit,\n compareTo: compareTo as any,\n ...(baselinePeriods && baselinePeriods > 0\n ? { baseline: { periods: baselinePeriods, stride: (baselineStride as any) === 'week' ? 'week' : 'day' } }\n : {})\n })\n }\n\n @Query(returns => ScalarObject, {\n nullable: true,\n description: 'Reconstruct twin state from the durable journal (DB → replay). untilTime (ISO) for time-travel on the shared clock (preferred); untilRevision for the lens-internal counter; omit both for latest.'\n })\n async twinReplay(\n @Arg('instanceId') instanceId: string,\n @Arg('untilRevision', type => Int, { nullable: true }) untilRevision: number,\n @Arg('untilTime', { nullable: true }) untilTime: string,\n @Ctx() context: ResolverContext,\n /*\n * 필요한 축만 — 지도는 `locations`·`attentions` 만 쓰는데 상태를 통째로 받고 있었다(실 저널로\n * 재 보니 트윈 하나가 최대 710KB, 같은 공간에 셋이면 약 2MB). 화면 전용 질의를 만들지 않고\n * 호출부가 축을 말한다(무방언 — 어휘가 화면 이름이 아니라 상태의 축이다).\n * 말하지 않으면 자르지 않는다(기존 호출부 무영향).\n */\n @Arg('axes', type => [String], { nullable: true }) axes?: string[],\n /*\n * 신호는 몇 건만 필요한가 — 레일은 20건을 그리고 나머지는 \"N건 더\" 라고만 말한다. 그런데 신호가\n * 많은 트윈은 그 전량이 108KB 다(`hatio-yard` 397건). 상위 N 만 보내고 **전체 개수와 자리별\n * 최고 심각도는 전량을 근거로** 함께 보낸다 — 레일은 잘라도 되지만 지도 색은 잘라선 안 된다.\n * 말하지 않으면 자르지 않는다.\n */\n @Arg('attentionLimit', type => Int, { nullable: true }) attentionLimit?: number\n ): Promise<any> {\n const state = await TwinEngine.recover(context.state.domain.id, instanceId, untilRevision ?? undefined, untilTime ?? undefined)\n const picked = pickAxes(state, axes)\n /* 오타를 조용히 넘기지 않는다 — 화면이 빈 채로 뜨는 이유를 아무도 모르게 되는 것이 가장 나쁘다. */\n if (picked.unknown.length) twinWarn(`[twin] twinReplay: unknown axes ignored for \"${instanceId}\": ${picked.unknown.join(', ')}`)\n if (!attentionLimit || !picked.state?.attentions) return picked.state\n const d = digestAttentions(picked.state.attentions, attentionLimit)\n /* 잘랐다는 사실을 값으로 남긴다 — 개수만 줄어들면 화면은 \"괜찮아졌다\" 로 읽는다. */\n return { ...picked.state, attentions: d.attentions, attentionTotal: d.attentionTotal, severityByLocation: d.severityByLocation }\n }\n\n @Query(returns => ScalarObject, {\n nullable: true,\n description:\n 'Steady-state capacity of a twin against a declared demand: per-operation supply vs required across equipment, personnel, physical assets and locations, plus the binding constraint and the daily ceiling. Answers \"can this plant meet the target?\" without running a simulation. Null when the twin is not running.'\n })\n async twinCapacity(\n @Arg('instanceId', { nullable: true }) instanceId: string,\n @Arg('spaceId', { nullable: true }) spaceId: string,\n /** 하루 몇 대를 낼 것인가 — 화면이 보고 있는 목표에서 나온다(선언된 수요). */\n @Arg('unitsPerDay', type => Float) unitsPerDay: number,\n /**\n * 가용 시간을 잴 기준 주의 시작(ISO, 월요일). 공휴일이 없는 평상주여야 한다 —\n * 공휴일은 연간 가용량을 따로 깎지, 이 공장의 평상시 상한을 정하지 않는다.\n */\n @Arg('sampleWeekStart', { nullable: true }) sampleWeekStart: string,\n @Ctx() context: ResolverContext\n ): Promise<any> {\n if (instanceId && !(await TwinEngine.ownsRegistered(context.state.domain.id, instanceId))) {\n throw new Error(`twin instance not found in this tenant: ${instanceId}`)\n }\n if (!instanceId && !spaceId) throw new Error('either spaceId or instanceId is required')\n if (!(unitsPerDay > 0)) throw new Error('unitsPerDay must be greater than zero')\n\n /*\n * 기준 주를 안 주면 **이번 주 월요일**을 쓴다. 그 주에 공휴일이 끼면 상한이 조금 낮게 나오는데,\n * 그것은 조용한 오류가 아니라 **이번 주의 실제 가용 시간**이라 답으로서 정직하다. 평상시 상한을\n * 보려는 호출자는 평상주를 지정한다.\n */\n const base = sampleWeekStart ? Date.parse(sampleWeekStart) : Date.now()\n if (Number.isNaN(base)) throw new Error(`sampleWeekStart is not a valid ISO time: ${sampleWeekStart}`)\n const d = new Date(base)\n const monday = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() - ((d.getUTCDay() + 6) % 7))\n\n return TwinEngine.capacity(context.state.domain.id, { instanceId, spaceId }, unitsPerDay, monday)\n }\n\n @Query(returns => ScalarObject, {\n nullable: true,\n description:\n 'Operations capability of a twin for a time window (ISA-95 Part 4 OperationsCapability): per operation the ceiling for that window split into Committed (already promised), Unattainable (lost to resources that cannot be used — down, held, off-shift, expired qualification) and Available (what is left). Answers \"can we take more work?\" — which the steady-state rate alone cannot. Null when the twin is not running.'\n })\n async twinOperationsCapability(\n @Arg('instanceId', { nullable: true }) instanceId: string,\n @Arg('spaceId', { nullable: true }) spaceId: string,\n @Arg('unitsPerDay', type => Float) unitsPerDay: number,\n /** 능력을 물을 구간 — **구간 없이 능력은 존재하지 않는다**(표준 StartTime/EndTime). */\n @Arg('fromTime') fromTime: string,\n @Arg('toTime') toTime: string,\n @Arg('sampleWeekStart', { nullable: true }) sampleWeekStart: string,\n @Ctx() context: ResolverContext\n ): Promise<any> {\n if (instanceId && !(await TwinEngine.ownsRegistered(context.state.domain.id, instanceId))) {\n throw new Error(`twin instance not found in this tenant: ${instanceId}`)\n }\n if (!instanceId && !spaceId) throw new Error('either spaceId or instanceId is required')\n if (!(unitsPerDay > 0)) throw new Error('unitsPerDay must be greater than zero')\n /* 구간은 **부르는 쪽이 말해야 한다** — 기본값을 두면 화면마다 다른 구간을 보면서 같은 값으로 읽는다. */\n const from = Date.parse(fromTime)\n const to = Date.parse(toTime)\n if (Number.isNaN(from) || Number.isNaN(to)) throw new Error(`window is not a valid ISO time: ${fromTime} ~ ${toTime}`)\n if (to <= from) throw new Error(`window is inverted: ${fromTime} ~ ${toTime}`)\n\n /* 기준 주 규율은 `twinCapacity` 와 같다(같은 계산을 쓰므로 같은 주를 골라야 한다). */\n const base = sampleWeekStart ? Date.parse(sampleWeekStart) : from\n if (Number.isNaN(base)) throw new Error(`sampleWeekStart is not a valid ISO time: ${sampleWeekStart}`)\n const d = new Date(base)\n const monday = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() - ((d.getUTCDay() + 6) % 7))\n\n return TwinEngine.operationsCapability(\n context.state.domain.id,\n { instanceId, spaceId },\n unitsPerDay,\n monday,\n { fromTime, toTime }\n )\n }\n\n @Query(returns => ScalarObject, {\n description:\n \"Structural revisions a twin has been through, newest last: when each took effect, how many journal events happened under it, and what changed from the previous one (counts by location type and equipment kind). The model itself is not returned — a revision holds the whole plant, but what a person wants is the difference.\"\n })\n async twinStructures(@Arg('instanceId') instanceId: string, @Ctx() context: ResolverContext): Promise<any> {\n /* 이력 질문이므로 **등록부**에 소유를 묻는다 — 꺼진 트윈의 이력도 읽을 수 있어야 한다. */\n if (!(await TwinEngine.ownsRegistered(context.state.domain.id, instanceId))) {\n throw new Error(`twin instance not found in this tenant: ${instanceId}`)\n }\n return TwinEngine.structureHistory(context.state.domain.id, instanceId)\n }\n\n @Query(returns => ScalarObject, {\n description: 'Event time range (min/max ISO) across all co-located twins of a space — the history scrubber time axis.'\n })\n async twinTimeRange(\n @Arg('spaceId') spaceId: string,\n @Ctx() context: ResolverContext\n ): Promise<{ minTime: string | null; maxTime: string | null }> {\n return TwinEngine.timeRange(context.state.domain.id, spaceId)\n }\n}\n"]}
|
|
@@ -2,6 +2,20 @@ export declare class TwinLifecycleMutation {
|
|
|
2
2
|
/** 기동 — model 를 주면 provision+start, 없으면 레지스트리에 저장된 구조로 start. */
|
|
3
3
|
startTwinInstance(instanceId: string, context: ResolverContext, kind?: string, model?: any): Promise<boolean>;
|
|
4
4
|
stopTwinInstance(instanceId: string, context: ResolverContext): Promise<boolean>;
|
|
5
|
+
/**
|
|
6
|
+
* 이 트윈이 **자기 과거를 어떻게 대할 것인가**를 다시 선언한다 (2026-08-20).
|
|
7
|
+
*
|
|
8
|
+
* ── 왜 이 문이 필요한가 ────────────────────────────────────────────────────
|
|
9
|
+
* `restartPolicy` 는 사람이 하는 선언인데, 만들 때 한 번 정하면 **고칠 길이 없었다.** 그래서 「씨앗
|
|
10
|
+
* 재현용」으로 세운 트윈이 나중에 사람들이 이력을 보는 트윈이 되어도 선언은 `reset` 으로 남았다.
|
|
11
|
+
* 선언과 쓰임이 어긋난 채로 두면, 선언대로 구현하는 순간(저널 초기화) 이력을 잃는다.
|
|
12
|
+
*
|
|
13
|
+
* ── 지금 도는 것에는 손대지 않는다 ─────────────────────────────────────────
|
|
14
|
+
* 이 값은 **다음 기동**의 규칙이다. 돌고 있는 런타임의 구동 방식을 바꾸려면 정지·기동이 필요하고,
|
|
15
|
+
* 그 판단은 사람의 것이다(도는 트윈을 조용히 갈아 끼우면 그 사이의 사실이 어디로 갔는지 알 수 없다).
|
|
16
|
+
* 그래서 결과에 「지금 도는 것과 다르다」는 사실을 함께 낸다.
|
|
17
|
+
*/
|
|
18
|
+
setTwinRestartPolicy(instanceId: string, restartPolicy: string, context: ResolverContext): Promise<any>;
|
|
5
19
|
/** 삭제 — 정지 + 레지스트리 삭제 + 저널 purge(domain 스코프). */
|
|
6
20
|
deleteTwinInstance(instanceId: string, context: ResolverContext): Promise<boolean>;
|
|
7
21
|
/** 관리 목록 — 등록된 인스턴스 전체(kind/status/running/revision/노드·무버 수). 데이터 없으면 빈 배열. */
|