@things-factory/headless-twin 10.0.15 → 10.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-server/engine/attention-digest.d.ts +4 -4
- package/dist-server/engine/attention-digest.js +7 -7
- package/dist-server/engine/attention-digest.js.map +1 -1
- package/dist-server/engine/canonical-ingest.d.ts +18 -82
- package/dist-server/engine/canonical-ingest.js +183 -18
- 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.js +1 -1
- package/dist-server/engine/declared-stimulus.js.map +1 -1
- package/dist-server/engine/energy-topology.js +4 -4
- package/dist-server/engine/energy-topology.js.map +1 -1
- package/dist-server/engine/ingest-dedupe.d.ts +38 -0
- package/dist-server/engine/ingest-dedupe.js +145 -0
- package/dist-server/engine/ingest-dedupe.js.map +1 -0
- package/dist-server/engine/ingest-health.d.ts +90 -10
- package/dist-server/engine/ingest-health.js +125 -16
- package/dist-server/engine/ingest-health.js.map +1 -1
- package/dist-server/engine/integration-probes.d.ts +3 -3
- package/dist-server/engine/integration-probes.js +6 -6
- package/dist-server/engine/integration-probes.js.map +1 -1
- package/dist-server/engine/kpi-baseline.js +4 -4
- package/dist-server/engine/kpi-baseline.js.map +1 -1
- package/dist-server/engine/kpi-fold.d.ts +212 -14
- package/dist-server/engine/kpi-fold.js +374 -21
- package/dist-server/engine/kpi-fold.js.map +1 -1
- package/dist-server/engine/kpi-query.d.ts +11 -11
- package/dist-server/engine/kpi-query.js +281 -36
- 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 +1 -1
- 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.map +1 -1
- package/dist-server/engine/local-declarations.d.ts +7 -7
- package/dist-server/engine/local-declarations.js +15 -14
- package/dist-server/engine/local-declarations.js.map +1 -1
- package/dist-server/engine/log.d.ts +1 -1
- package/dist-server/engine/log.js +1 -1
- package/dist-server/engine/log.js.map +1 -1
- package/dist-server/engine/measured-estimator.js +1 -1
- package/dist-server/engine/measured-estimator.js.map +1 -1
- package/dist-server/engine/measured-yield.js +1 -1
- package/dist-server/engine/measured-yield.js.map +1 -1
- package/dist-server/engine/model-basis.d.ts +1 -1
- package/dist-server/engine/model-basis.js +1 -1
- package/dist-server/engine/model-basis.js.map +1 -1
- package/dist-server/engine/model-gap.d.ts +3 -3
- package/dist-server/engine/model-gap.js +1 -1
- package/dist-server/engine/model-gap.js.map +1 -1
- package/dist-server/engine/model-vocabulary.js +3 -3
- package/dist-server/engine/model-vocabulary.js.map +1 -1
- package/dist-server/engine/oee-accumulator.d.ts +6 -5
- package/dist-server/engine/oee-accumulator.js +6 -5
- package/dist-server/engine/oee-accumulator.js.map +1 -1
- package/dist-server/engine/property-effects.js +2 -2
- package/dist-server/engine/property-effects.js.map +1 -1
- package/dist-server/engine/read-time.d.ts +22 -0
- package/dist-server/engine/read-time.js +81 -0
- package/dist-server/engine/read-time.js.map +1 -0
- package/dist-server/engine/restart-policy.js +1 -1
- package/dist-server/engine/restart-policy.js.map +1 -1
- 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 +2 -2
- package/dist-server/engine/spec-coverage.js +1 -1
- package/dist-server/engine/spec-coverage.js.map +1 -1
- package/dist-server/engine/stage-path.d.ts +4 -4
- package/dist-server/engine/stage-path.js +1 -1
- package/dist-server/engine/stage-path.js.map +1 -1
- package/dist-server/engine/state-axes.d.ts +1 -1
- package/dist-server/engine/state-axes.js +2 -2
- package/dist-server/engine/state-axes.js.map +1 -1
- package/dist-server/engine/travel-estimator.d.ts +2 -2
- package/dist-server/engine/travel-estimator.js +1 -1
- package/dist-server/engine/travel-estimator.js.map +1 -1
- package/dist-server/engine/twin-engine.d.ts +307 -54
- package/dist-server/engine/twin-engine.js +822 -196
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/engine/warm-start.d.ts +2 -2
- package/dist-server/engine/warm-start.js +5 -5
- package/dist-server/engine/warm-start.js.map +1 -1
- package/dist-server/index.js +1 -1
- package/dist-server/index.js.map +1 -1
- package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js +1 -1
- package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js.map +1 -1
- package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js +3 -3
- package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js.map +1 -1
- package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js +2 -2
- package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js.map +1 -1
- package/dist-server/migrations/1786300000000-IndexEventTypeByTime.d.ts +5 -0
- package/dist-server/migrations/1786300000000-IndexEventTypeByTime.js +54 -0
- package/dist-server/migrations/1786300000000-IndexEventTypeByTime.js.map +1 -0
- package/dist-server/migrations/1786400000000-IndexEventCreatedAt.d.ts +5 -0
- package/dist-server/migrations/1786400000000-IndexEventCreatedAt.js +44 -0
- package/dist-server/migrations/1786400000000-IndexEventCreatedAt.js.map +1 -0
- package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.d.ts +6 -0
- package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.js +84 -0
- package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.js.map +1 -0
- package/dist-server/migrations/index.js +7 -1
- package/dist-server/migrations/index.js.map +1 -1
- package/dist-server/routes.d.ts +1 -0
- package/dist-server/routes.js +69 -5
- package/dist-server/routes.js.map +1 -1
- package/dist-server/service/index.d.ts +2 -2
- package/dist-server/service/index.js +30 -24
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/reference/control-routing.d.ts +1 -1
- package/dist-server/service/reference/control-routing.js +1 -1
- package/dist-server/service/reference/control-routing.js.map +1 -1
- package/dist-server/service/reference/discovery-result.d.ts +2 -2
- package/dist-server/service/reference/discovery-result.js +3 -3
- package/dist-server/service/reference/discovery-result.js.map +1 -1
- package/dist-server/service/reference/hook-contract.d.ts +43 -0
- package/dist-server/service/reference/hook-contract.js +59 -0
- package/dist-server/service/reference/hook-contract.js.map +1 -0
- package/dist-server/service/reference/ingest-space.d.ts +1 -1
- package/dist-server/service/reference/ingest-space.js +4 -4
- package/dist-server/service/reference/ingest-space.js.map +1 -1
- package/dist-server/service/reference/knob-defaults.d.ts +1 -1
- package/dist-server/service/reference/knob-defaults.js +4 -4
- package/dist-server/service/reference/knob-defaults.js.map +1 -1
- package/dist-server/service/reference/reference-adapter.d.ts +116 -9
- package/dist-server/service/reference/reference-adapter.js +27 -0
- package/dist-server/service/reference/reference-adapter.js.map +1 -1
- package/dist-server/service/reference/reference-hook.d.ts +20 -0
- package/dist-server/service/reference/reference-hook.js +133 -0
- package/dist-server/service/reference/reference-hook.js.map +1 -0
- package/dist-server/service/reference/reference-live.d.ts +2 -2
- package/dist-server/service/reference/reference-live.js +76 -13
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-master.d.ts +25 -16
- package/dist-server/service/reference/reference-master.js +35 -13
- package/dist-server/service/reference/reference-master.js.map +1 -1
- package/dist-server/service/reference/reference-resolver.d.ts +1 -1
- package/dist-server/service/reference/reference-resolver.js +12 -12
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/reference/template-registry.d.ts +1 -1
- package/dist-server/service/reference/template-registry.js.map +1 -1
- package/dist-server/service/reference/twin-reference.d.ts +3 -3
- package/dist-server/service/reference/twin-reference.js.map +1 -1
- package/dist-server/service/twin-attention/twin-attention-query.d.ts +1 -1
- package/dist-server/service/twin-attention/twin-attention-query.js +1 -1
- package/dist-server/service/twin-attention/twin-attention-query.js.map +1 -1
- package/dist-server/service/twin-audit/command-audit.d.ts +1 -1
- package/dist-server/service/twin-audit/command-audit.js.map +1 -1
- package/dist-server/service/twin-audit/twin-audit-event.d.ts +1 -1
- package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -1
- package/dist-server/service/twin-audit/twin-audit-query.js +1 -1
- package/dist-server/service/twin-audit/twin-audit-query.js.map +1 -1
- package/dist-server/service/twin-backfill/backfill-period-facts.d.ts +24 -0
- package/dist-server/service/twin-backfill/backfill-period-facts.js +96 -0
- package/dist-server/service/twin-backfill/backfill-period-facts.js.map +1 -0
- package/dist-server/service/twin-backfill/backfill-shape.d.ts +34 -0
- package/dist-server/service/twin-backfill/backfill-shape.js +75 -0
- package/dist-server/service/twin-backfill/backfill-shape.js.map +1 -0
- package/dist-server/service/twin-backfill/index.d.ts +2 -0
- package/dist-server/service/twin-backfill/index.js +6 -0
- package/dist-server/service/twin-backfill/index.js.map +1 -0
- package/dist-server/service/twin-backfill/twin-backfill-resolver.d.ts +10 -0
- package/dist-server/service/twin-backfill/twin-backfill-resolver.js +44 -0
- package/dist-server/service/twin-backfill/twin-backfill-resolver.js.map +1 -0
- package/dist-server/service/twin-control/twin-control-mutation.js +3 -3
- package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
- package/dist-server/service/twin-event/twin-event-keys.d.ts +1 -1
- package/dist-server/service/twin-event/twin-event-keys.js +2 -2
- package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
- package/dist-server/service/twin-event/twin-event-type.d.ts +1 -0
- package/dist-server/service/twin-event/twin-event-type.js +8 -1
- package/dist-server/service/twin-event/twin-event-type.js.map +1 -1
- package/dist-server/service/twin-event/twin-event.d.ts +2 -2
- package/dist-server/service/twin-event/twin-event.js +32 -4
- package/dist-server/service/twin-event/twin-event.js.map +1 -1
- package/dist-server/service/twin-forecast/forecast-metrics.js +1 -1
- package/dist-server/service/twin-forecast/forecast-metrics.js.map +1 -1
- package/dist-server/service/twin-forecast/gap-analytics.d.ts +1 -1
- package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
- package/dist-server/service/twin-forecast/twin-forecast-query.js +29 -11
- package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
- package/dist-server/service/twin-ingest-window/twin-ingest-window-query.d.ts +1 -1
- package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js +1 -1
- package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js.map +1 -1
- package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js +1 -1
- package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js.map +1 -1
- package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
- package/dist-server/service/twin-journal/event-type-split-shape.d.ts +50 -0
- package/dist-server/service/twin-journal/event-type-split-shape.js +111 -0
- package/dist-server/service/twin-journal/event-type-split-shape.js.map +1 -0
- package/dist-server/service/twin-journal/event-type-split.d.ts +15 -0
- package/dist-server/service/twin-journal/event-type-split.js +102 -0
- package/dist-server/service/twin-journal/event-type-split.js.map +1 -0
- package/dist-server/service/twin-journal/journal-order.d.ts +15 -0
- package/dist-server/service/twin-journal/journal-order.js +17 -0
- package/dist-server/service/twin-journal/journal-order.js.map +1 -0
- package/dist-server/service/twin-journal/twin-journal-query.d.ts +5 -5
- package/dist-server/service/twin-journal/twin-journal-query.js +232 -21
- package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
- package/dist-server/service/twin-lifecycle/domain-catalog.js +2 -1
- package/dist-server/service/twin-lifecycle/domain-catalog.js.map +1 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +27 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +59 -3
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
- package/dist-server/service/twin-metrics/twin-metrics-query.js +14 -1
- package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
- package/dist-server/service/twin-model/axis-journal-evidence.js +1 -1
- package/dist-server/service/twin-model/axis-journal-evidence.js.map +1 -1
- package/dist-server/service/twin-model/epcis-coverage.js +34 -3
- package/dist-server/service/twin-model/epcis-coverage.js.map +1 -1
- package/dist-server/service/twin-model/iec61850-coverage.js +1 -1
- package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -1
- package/dist-server/service/twin-model/isa95-coverage.d.ts +12 -9
- package/dist-server/service/twin-model/isa95-coverage.js +33 -9
- package/dist-server/service/twin-model/isa95-coverage.js.map +1 -1
- package/dist-server/service/twin-model/item-ref.js +2 -2
- package/dist-server/service/twin-model/item-ref.js.map +1 -1
- package/dist-server/service/twin-model/name-index.d.ts +1 -1
- package/dist-server/service/twin-model/name-index.js +2 -2
- package/dist-server/service/twin-model/name-index.js.map +1 -1
- package/dist-server/service/twin-model/project-structure.d.ts +1 -1
- package/dist-server/service/twin-model/project-structure.js +7 -6
- package/dist-server/service/twin-model/project-structure.js.map +1 -1
- package/dist-server/service/twin-model/twin-equipment.d.ts +2 -2
- package/dist-server/service/twin-model/twin-equipment.js +3 -3
- package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
- package/dist-server/service/twin-model/twin-lineage-query.js +16 -5
- package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-location.d.ts +1 -1
- package/dist-server/service/twin-model/twin-location.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-item-query.d.ts +3 -3
- package/dist-server/service/twin-model/twin-model-item-query.js +15 -15
- package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-mutation.d.ts +2 -2
- package/dist-server/service/twin-model/twin-model-mutation.js +4 -4
- package/dist-server/service/twin-model/twin-model-mutation.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-query.js +51 -30
- package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-tree-query.js +23 -1
- package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-operation.d.ts +1 -1
- package/dist-server/service/twin-model/twin-operation.js +1 -1
- package/dist-server/service/twin-model/twin-operation.js.map +1 -1
- package/dist-server/service/twin-space/move-space.js +2 -2
- package/dist-server/service/twin-space/move-space.js.map +1 -1
- package/dist-server/service/twin-space/representation-role.d.ts +2 -0
- package/dist-server/service/twin-space/representation-role.js +18 -0
- package/dist-server/service/twin-space/representation-role.js.map +1 -0
- package/dist-server/service/twin-space/space-integrity.d.ts +1 -1
- package/dist-server/service/twin-space/space-integrity.js.map +1 -1
- package/dist-server/service/twin-space/twin-space-representation.d.ts +2 -0
- package/dist-server/service/twin-space/twin-space-representation.js +16 -0
- package/dist-server/service/twin-space/twin-space-representation.js.map +1 -1
- package/dist-server/service/twin-space/twin-space-resolver.d.ts +2 -2
- package/dist-server/service/twin-space/twin-space-resolver.js +37 -8
- package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
- package/dist-server/service/twin-structure/twin-structure.js +3 -3
- package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
- package/dist-server/service/twin-subject/event-subjects.d.ts +43 -0
- package/dist-server/service/twin-subject/event-subjects.js +137 -0
- package/dist-server/service/twin-subject/event-subjects.js.map +1 -0
- package/dist-server/service/twin-subject/index.d.ts +9 -0
- package/dist-server/service/twin-subject/index.js +16 -0
- package/dist-server/service/twin-subject/index.js.map +1 -0
- package/dist-server/service/twin-subject/item-resolver.d.ts +10 -0
- package/dist-server/service/twin-subject/item-resolver.js +93 -0
- package/dist-server/service/twin-subject/item-resolver.js.map +1 -0
- package/dist-server/service/twin-subject/subject-query-shape.d.ts +22 -0
- package/dist-server/service/twin-subject/subject-query-shape.js +88 -0
- package/dist-server/service/twin-subject/subject-query-shape.js.map +1 -0
- package/dist-server/service/twin-subject/twin-subject-backfill.d.ts +23 -0
- package/dist-server/service/twin-subject/twin-subject-backfill.js +159 -0
- package/dist-server/service/twin-subject/twin-subject-backfill.js.map +1 -0
- package/dist-server/service/twin-subject/twin-subject-event.d.ts +14 -0
- package/dist-server/service/twin-subject/twin-subject-event.js +111 -0
- package/dist-server/service/twin-subject/twin-subject-event.js.map +1 -0
- package/dist-server/service/twin-subject/twin-subject-fast-path.d.ts +39 -0
- package/dist-server/service/twin-subject/twin-subject-fast-path.js +295 -0
- package/dist-server/service/twin-subject/twin-subject-fast-path.js.map +1 -0
- package/dist-server/service/twin-subject/twin-subject-query.d.ts +27 -0
- package/dist-server/service/twin-subject/twin-subject-query.js +232 -0
- package/dist-server/service/twin-subject/twin-subject-query.js.map +1 -0
- package/dist-server/service/twin-subject/twin-subject-writer.d.ts +27 -0
- package/dist-server/service/twin-subject/twin-subject-writer.js +166 -0
- package/dist-server/service/twin-subject/twin-subject-writer.js.map +1 -0
- package/dist-server/service/twin-target/twin-target-resolver.js +1 -1
- package/dist-server/service/twin-target/twin-target-resolver.js.map +1 -1
- package/dist-shared/entity-delta.d.ts +1 -1
- package/dist-shared/entity-delta.js +4 -4
- package/dist-shared/entity-delta.js.map +1 -1
- package/dist-shared/touched-items.js +2 -2
- package/dist-shared/touched-items.js.map +1 -1
- package/dist-shared/twin-level.js +1 -1
- package/dist-shared/twin-level.js.map +1 -1
- package/package.json +7 -6
- package/server/engine/attention-digest.ts +9 -9
- package/server/engine/canonical-ingest.ts +202 -87
- package/server/engine/command-routing.ts +1 -1
- package/server/engine/declared-stimulus.ts +1 -1
- package/server/engine/energy-topology.ts +2 -2
- package/server/engine/ingest-dedupe.ts +149 -0
- package/server/engine/ingest-health.ts +171 -22
- package/server/engine/integration-probes.ts +7 -7
- package/server/engine/kpi-baseline.ts +4 -4
- package/server/engine/kpi-fold.ts +592 -41
- package/server/engine/kpi-query.ts +307 -48
- package/server/engine/live-feed-registry.ts +1 -1
- package/server/engine/load-meter.ts +1 -1
- package/server/engine/local-declarations.ts +15 -14
- package/server/engine/log.ts +1 -1
- package/server/engine/measured-estimator.ts +1 -1
- package/server/engine/measured-yield.ts +1 -1
- package/server/engine/model-basis.ts +2 -2
- package/server/engine/model-gap.ts +3 -3
- package/server/engine/model-vocabulary.ts +2 -2
- package/server/engine/oee-accumulator.ts +8 -6
- package/server/engine/property-effects.ts +2 -2
- package/server/engine/read-time.ts +77 -0
- package/server/engine/restart-policy.ts +1 -1
- package/server/engine/runtime-key.ts +1 -1
- package/server/engine/spec-coverage.ts +3 -3
- package/server/engine/stage-path.ts +5 -5
- package/server/engine/state-axes.ts +2 -2
- package/server/engine/travel-estimator.ts +2 -2
- package/server/engine/twin-engine.ts +906 -205
- package/server/engine/warm-start.ts +6 -6
- package/server/index.ts +1 -1
- package/server/migrations/1786000000000-RenameTwinInstanceBoardToModel.ts +1 -1
- package/server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.ts +3 -3
- package/server/migrations/1786200000000-CarryLiveFeedCursor.ts +2 -2
- package/server/migrations/1786300000000-IndexEventTypeByTime.ts +48 -0
- package/server/migrations/1786400000000-IndexEventCreatedAt.ts +38 -0
- package/server/migrations/1786500000000-CreateTwinSubjectEvents.ts +93 -0
- package/server/migrations/index.ts +7 -1
- package/server/routes.ts +73 -5
- package/server/service/index.ts +7 -1
- package/server/service/reference/control-routing.ts +1 -1
- package/server/service/reference/discovery-result.ts +3 -3
- package/server/service/reference/hook-contract.ts +103 -0
- package/server/service/reference/ingest-space.ts +4 -4
- package/server/service/reference/knob-defaults.ts +4 -4
- package/server/service/reference/reference-adapter.ts +145 -10
- package/server/service/reference/reference-hook.ts +145 -0
- package/server/service/reference/reference-live.ts +87 -15
- package/server/service/reference/reference-master.ts +51 -20
- package/server/service/reference/reference-resolver.ts +11 -11
- package/server/service/reference/template-registry.ts +1 -1
- package/server/service/reference/twin-reference.ts +3 -3
- package/server/service/twin-attention/twin-attention-query.ts +1 -1
- package/server/service/twin-audit/command-audit.ts +1 -1
- package/server/service/twin-audit/twin-audit-event.ts +2 -2
- package/server/service/twin-audit/twin-audit-query.ts +1 -1
- package/server/service/twin-backfill/backfill-period-facts.ts +168 -0
- package/server/service/twin-backfill/backfill-shape.ts +86 -0
- package/server/service/twin-backfill/index.ts +3 -0
- package/server/service/twin-backfill/twin-backfill-resolver.ts +35 -0
- package/server/service/twin-control/twin-control-mutation.ts +2 -2
- package/server/service/twin-event/twin-event-keys.ts +2 -2
- package/server/service/twin-event/twin-event-type.ts +18 -1
- package/server/service/twin-event/twin-event.ts +32 -6
- package/server/service/twin-forecast/forecast-metrics.ts +1 -1
- package/server/service/twin-forecast/gap-analytics.ts +1 -1
- package/server/service/twin-forecast/twin-forecast-query.ts +30 -11
- package/server/service/twin-ingest-window/twin-ingest-window-query.ts +1 -1
- package/server/service/twin-ingest-window/twin-ingest-window-writer.ts +1 -1
- package/server/service/twin-instance/twin-instance.ts +4 -4
- package/server/service/twin-journal/event-type-split-shape.ts +112 -0
- package/server/service/twin-journal/event-type-split.ts +144 -0
- package/server/service/twin-journal/journal-order.ts +20 -0
- package/server/service/twin-journal/twin-journal-query.ts +224 -20
- package/server/service/twin-lifecycle/domain-catalog.ts +2 -1
- package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +58 -4
- package/server/service/twin-metrics/twin-metrics-query.ts +14 -1
- package/server/service/twin-model/axis-journal-evidence.ts +1 -1
- package/server/service/twin-model/epcis-coverage.ts +34 -3
- package/server/service/twin-model/iec61850-coverage.ts +1 -1
- package/server/service/twin-model/isa95-coverage.ts +43 -16
- package/server/service/twin-model/item-ref.ts +2 -2
- package/server/service/twin-model/name-index.ts +2 -2
- package/server/service/twin-model/project-structure.ts +7 -6
- package/server/service/twin-model/twin-equipment.ts +16 -4
- package/server/service/twin-model/twin-lineage-query.ts +16 -5
- package/server/service/twin-model/twin-location.ts +1 -1
- package/server/service/twin-model/twin-model-item-query.ts +15 -15
- package/server/service/twin-model/twin-model-mutation.ts +4 -4
- package/server/service/twin-model/twin-model-query.ts +49 -28
- package/server/service/twin-model/twin-model-tree-query.ts +26 -2
- package/server/service/twin-model/twin-operation.ts +2 -2
- package/server/service/twin-space/move-space.ts +3 -3
- package/server/service/twin-space/representation-role.ts +15 -0
- package/server/service/twin-space/space-integrity.ts +1 -1
- package/server/service/twin-space/twin-space-representation.ts +37 -0
- package/server/service/twin-space/twin-space-resolver.ts +40 -8
- package/server/service/twin-structure/twin-structure.ts +3 -3
- package/server/service/twin-subject/event-subjects.ts +155 -0
- package/server/service/twin-subject/index.ts +10 -0
- package/server/service/twin-subject/item-resolver.ts +92 -0
- package/server/service/twin-subject/subject-query-shape.ts +84 -0
- package/server/service/twin-subject/twin-subject-backfill.ts +196 -0
- package/server/service/twin-subject/twin-subject-event.ts +101 -0
- package/server/service/twin-subject/twin-subject-fast-path.ts +346 -0
- package/server/service/twin-subject/twin-subject-query.ts +210 -0
- package/server/service/twin-subject/twin-subject-writer.ts +189 -0
- package/server/service/twin-target/twin-target-resolver.ts +1 -1
- package/shared/entity-delta.ts +4 -4
- package/shared/touched-items.ts +2 -2
- package/shared/twin-level.ts +1 -1
- package/test/adopt-structure-live.test.ts +3 -3
- package/test/aggregate-not-findone.test.ts +112 -0
- package/test/attention-digest.test.ts +6 -6
- package/test/axis-read.test.ts +6 -5
- package/test/backfill-shape.test.ts +89 -0
- package/test/boot-resume.test.ts +20 -7
- package/test/broadcast-cost-baseline.test.ts +2 -1
- package/test/broadcast-period.test.ts +2 -2
- package/test/broadcast-queue-overflow.test.ts +244 -0
- package/test/canonical-ingest-vocabularies.test.ts +259 -8
- package/test/canonical-quantity-door.test.ts +2 -2
- package/test/capability-mapping.test.ts +1 -1
- package/test/checkpoint-refuses-empty.test.ts +2 -2
- package/test/connector-capability-declaration.test.ts +88 -0
- package/test/contract-layer-guard.test.ts +48 -0
- package/test/control-capability.test.ts +1 -1
- package/test/cursor-stall-not-read-failure.test.ts +2 -2
- package/test/declaration-reaches-model.test.ts +7 -5
- package/test/declared-stimulus.test.ts +4 -4
- package/test/discovery-result.test.ts +2 -2
- package/test/entity-delta.test.ts +4 -4
- package/test/equipment-identity-reaches.test.ts +57 -0
- package/test/event-subjects.test.ts +147 -0
- package/test/event-time-column.test.ts +3 -3
- package/test/event-type-split.test.ts +155 -0
- package/test/fact-scope-wiring.test.ts +96 -0
- package/test/fold-gap-speaks.test.ts +53 -0
- package/test/forecast-metrics.test.ts +1 -1
- package/test/forecast-tuning.test.ts +1 -1
- package/test/generated-not-counted.test.ts +99 -0
- package/test/ingest-bench.test.ts +2 -1
- package/test/ingest-health-engine.test.ts +2 -2
- package/test/ingest-health-wiring.test.ts +7 -5
- package/test/ingest-health.test.ts +6 -6
- package/test/ingest-history.test.ts +2 -2
- package/test/ingest-idempotent.test.ts +132 -0
- package/test/ingest-space.test.ts +3 -3
- package/test/ingest-wiring-guard.test.ts +197 -0
- package/test/integration-probes.test.ts +3 -3
- package/test/integration-runner.test.ts +1 -1
- package/test/item-ref.test.ts +1 -1
- package/test/journal-order-axis.test.ts +80 -0
- package/test/journal-read-discipline.test.ts +16 -6
- package/test/journal-retention.test.ts +171 -29
- package/test/journal-sort-axis.test.ts +1 -1
- package/test/journal-write-door.test.ts +3 -3
- package/test/journal-write-trend.test.ts +1 -1
- package/test/kernel-kind-guard.test.ts +3 -3
- package/test/knob-defaults.test.ts +1 -1
- package/test/kpi-baseline-db.test.ts +2 -2
- package/test/kpi-fold.test.ts +266 -14
- package/test/kpi-query-bench.test.ts +3 -3
- package/test/live-cursor-wiring.test.ts +6 -6
- package/test/live-kernel-facts.test.ts +11 -11
- package/test/live-mirror-parity.test.ts +9 -6
- package/test/load-meter.test.ts +1 -1
- package/test/local-declarations.test.ts +13 -12
- package/test/log-stamp.test.ts +1 -1
- package/test/master-to-twin.test.ts +2 -2
- package/test/measured-yield.test.ts +2 -2
- package/test/merge-streams.test.ts +70 -0
- package/test/mirror-resumes-from-checkpoint.test.ts +13 -13
- package/test/model-basis.test.ts +2 -2
- package/test/model-gap.test.ts +4 -4
- package/test/model-vocabulary.test.ts +2 -2
- package/test/move-space.test.ts +1 -1
- package/test/oee-accumulator.test.ts +20 -5
- package/test/operational-vocabulary.test.ts +2 -2
- package/test/project-structure-db.test.ts +2 -2
- package/test/projection-reaches-screen.test.ts +7 -2
- package/test/projection-reads-declared.test.ts +109 -0
- package/test/property-effects.test.ts +10 -6
- package/test/read-failure-ledger.test.ts +106 -0
- package/test/read-failure-visible.test.ts +7 -7
- package/test/read-time.test.ts +118 -0
- package/test/reference-grounding.test.ts +1 -1
- package/test/reference-hook.test.ts +86 -0
- package/test/registry-key-guard.test.ts +1 -1
- package/test/rename-twin.test.ts +81 -0
- package/test/restart-policy.test.ts +3 -3
- package/test/resync-origin-site.test.ts +3 -3
- package/test/revision-axis.test.ts +14 -8
- package/test/runtime-key.test.ts +3 -3
- package/test/scale-twin-bench.test.ts +2 -1
- package/test/snapshot-freshness.test.ts +4 -4
- package/test/source-outcome-audit.test.ts +1 -1
- package/test/space-integrity.test.ts +1 -1
- package/test/space-representation.test.ts +66 -0
- package/test/space-scope-includes-stopped.test.ts +55 -0
- package/test/spec-coverage.test.ts +1 -1
- package/test/standard-coverage.test.ts +19 -21
- package/test/status-tally.test.ts +1 -1
- package/test/structure-revision-db.test.ts +7 -7
- package/test/subject-fast-path.test.ts +148 -0
- package/test/subject-read-model.test.ts +175 -0
- package/test/touched-items.test.ts +2 -2
- package/test/twin-audit.test.ts +1 -1
- package/test/twin-event-keys.test.ts +1 -1
- package/test/twin-model-item-db.test.ts +5 -5
- package/test/twin-model-tree-db.test.ts +35 -2
- package/test/twin-origin-resync.test.ts +1 -1
- package/test/vocabulary-guard.test.ts +1 -1
- package/test/warm-start-seam.test.ts +1 -1
- package/test/warm-start.test.ts +6 -6
- package/test/withheld-door.test.ts +2 -2
- package/test/yield-loop.test.ts +4 -4
- package/tsconfig.shared.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { getRepository } from '@things-factory/shell'
|
|
2
|
+
|
|
3
|
+
import { twinError, twinWarn } from '../../engine/log.js'
|
|
4
|
+
import { absoluteTime, hydratedDate } from '../../engine/read-time.js'
|
|
5
|
+
import { TwinSubjectEvent } from './twin-subject-event.js'
|
|
6
|
+
import { subjectsOf, type ItemResolver } from './event-subjects.js'
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* **대상별 사건 목록에 적는 쪽** — 저널을 쓸 때 함께 적는다.
|
|
10
|
+
*
|
|
11
|
+
* ── 왜 저널 쓰기와 같은 자리인가 ────────────────────────────────────────────
|
|
12
|
+
* 저널을 쓰는 문은 하나다(`flushJournal` → `persistBatch`·`persistCarried`). 그 자리에서 함께 적으면
|
|
13
|
+
* 두 표가 어긋나지 않는다. 다른 자리에서 나중에 적으면 「저널에는 있고 이 표에는 없는 사건」이 생기고,
|
|
14
|
+
* 그것은 화면에서 오류 없이 빠진 사건이 된다.
|
|
15
|
+
*
|
|
16
|
+
* ── 저널이 먼저다 ──────────────────────────────────────────────────────────
|
|
17
|
+
* 이 표에 적다가 실패해도 저널 쓰기를 되돌리지 않는다. 사실의 원본은 저널이고, 이 표는 다시 만들 수
|
|
18
|
+
* 있다. 반대로 두면 조회를 빠르게 하려고 만든 표가 사실을 잃는 원인이 된다.
|
|
19
|
+
*
|
|
20
|
+
* **그러나 알리지 않고 넘기지 않는다.** 실패를 세어 창마다 한 줄 남긴다 — 알리지 않고 비면 화면이 「그 대상에는
|
|
21
|
+
* 사건이 없다」고 말하게 된다.
|
|
22
|
+
*
|
|
23
|
+
* ── 앞으로만 채운다 ────────────────────────────────────────────────────────
|
|
24
|
+
* 이미 저장된 사건을 소급해서 채우지 않는다. 이 표가 만들어진 뒤 들어오는 사건만 적힌다. 그래서 새
|
|
25
|
+
* 조회 축이 필요할 때 3,128만 줄을 다시 만들지 않는다. 준비되지 않은 구간은 조회하는 쪽이 말한다
|
|
26
|
+
* (§`twin-subject-query` 의 `coveredFrom`).
|
|
27
|
+
*
|
|
28
|
+
* 설계: `operato-twin/design/plans/read-models.md`
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** 한 번에 넣는 줄 수 — 저널 쓰기와 같은 규율(드라이버 파라미터 상한). */
|
|
32
|
+
const CHUNK = 500
|
|
33
|
+
|
|
34
|
+
/** 적다가 실패한 것을 센다 — 트윈마다. 알리지 않고 비는 것을 막는 유일한 신호다. */
|
|
35
|
+
const failures = new Map<string, { count: number; lastLogMs: number; lastError: string }>()
|
|
36
|
+
|
|
37
|
+
/** 같은 실패를 매번 적지 않는다 — 로그가 그 자체로 부하가 된다. */
|
|
38
|
+
const FAIL_LOG_MS = 60_000
|
|
39
|
+
|
|
40
|
+
export interface SubjectWriteEntry {
|
|
41
|
+
envelope: any
|
|
42
|
+
revision: number
|
|
43
|
+
/** 저널 줄의 id — 알면 함께 둔다. 벌크 저장에서는 모를 수 있다(그때는 비운다). */
|
|
44
|
+
eventId?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 이 배치의 사건들이 가리키는 대상을 적는다 — 적은 줄 수를 돌려준다.
|
|
49
|
+
*
|
|
50
|
+
* `itemOf` 는 개체 식별자에서 선언된 품목을 찾아 주는 함수다. 없으면 품목 줄을 만들지 않는다 —
|
|
51
|
+
* 짐작으로 이으면 남의 품목 이력에 이 사건이 섞인다(§`event-subjects`).
|
|
52
|
+
*/
|
|
53
|
+
export async function writeSubjectRows(
|
|
54
|
+
domainId: string,
|
|
55
|
+
instanceId: string,
|
|
56
|
+
entries: readonly SubjectWriteEntry[],
|
|
57
|
+
itemOf?: ItemResolver
|
|
58
|
+
): Promise<number> {
|
|
59
|
+
if (!entries.length) return 0
|
|
60
|
+
|
|
61
|
+
const rows: any[] = []
|
|
62
|
+
for (const { envelope, revision, eventId } of entries) {
|
|
63
|
+
const eventTime = timeOf(envelope)
|
|
64
|
+
/*
|
|
65
|
+
* 시각을 모르는 사건은 이 표에 넣지 않는다. 이 표의 유일한 쓸모가 「시각순 목록」이고, 시각이 없는
|
|
66
|
+
* 줄은 그 목록의 어디에도 놓을 수 없다. 넣고 「지금」으로 메우면 그 사건이 최근으로 보인다.
|
|
67
|
+
*/
|
|
68
|
+
if (!eventTime) continue
|
|
69
|
+
const eventType = String(envelope?.eventType ?? '').trim()
|
|
70
|
+
if (!eventType) continue
|
|
71
|
+
|
|
72
|
+
for (const s of subjectsOf(envelope, { itemOf })) {
|
|
73
|
+
rows.push({
|
|
74
|
+
domain: { id: domainId },
|
|
75
|
+
instanceId,
|
|
76
|
+
subjectKind: s.kind,
|
|
77
|
+
subjectId: s.id,
|
|
78
|
+
eventTime,
|
|
79
|
+
revision,
|
|
80
|
+
eventType,
|
|
81
|
+
...(eventId ? { eventId } : {})
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!rows.length) return 0
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const repo = getRepository(TwinSubjectEvent)
|
|
89
|
+
for (let i = 0; i < rows.length; i += CHUNK) await repo.insert(rows.slice(i, i + CHUNK))
|
|
90
|
+
return rows.length
|
|
91
|
+
} catch (err: any) {
|
|
92
|
+
noteFailure(domainId, instanceId, rows.length, err?.message ?? String(err))
|
|
93
|
+
return 0
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 사건의 시각 — 봉투가 말하는 값만 쓴다. 없으면 `null`(지금으로 메우지 않는다). */
|
|
98
|
+
function timeOf(envelope: any): Date | null {
|
|
99
|
+
/* 봉투는 컬럼이 아니다 — 정규 계약이 절대시각을 요구하므로 그 규칙으로 읽는다(§`absoluteTime`). */
|
|
100
|
+
return absoluteTime(envelope?.eventTime ?? envelope?.data?.at)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function noteFailure(domainId: string, instanceId: string, rows: number, message: string): void {
|
|
104
|
+
const key = `${domainId}:${instanceId}`
|
|
105
|
+
const f = failures.get(key) ?? { count: 0, lastLogMs: 0, lastError: '' }
|
|
106
|
+
f.count += rows
|
|
107
|
+
f.lastError = message
|
|
108
|
+
const now = Date.now()
|
|
109
|
+
if (now - f.lastLogMs >= FAIL_LOG_MS) {
|
|
110
|
+
f.lastLogMs = now
|
|
111
|
+
twinWarn(
|
|
112
|
+
`[twin-subject] "${instanceId}" 대상 목록에 적지 못했다 — 지금까지 ${f.count}줄. ` +
|
|
113
|
+
`저널에는 그 사건이 있으므로 사실은 잃지 않았지만, 이 트윈의 이력 화면에 그만큼이 비어 있다: ${message}`
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
failures.set(key, f)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 적지 못한 줄 수 — 조회하는 쪽이 「이 표가 온전한가」를 말할 수 있게. */
|
|
120
|
+
export function subjectWriteFailures(domainId: string, instanceId: string): number {
|
|
121
|
+
return failures.get(`${domainId}:${instanceId}`)?.count ?? 0
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 시험이 쓰는 초기화 — 프로세스 안의 셈이므로 시험마다 비운다. */
|
|
125
|
+
export function resetSubjectWriteFailures(): void {
|
|
126
|
+
failures.clear()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 보관 상한 — 이 트윈에서 이 시각보다 오래된 줄을 지운다.
|
|
131
|
+
*
|
|
132
|
+
* 이 표는 **버릴 수 있는 값**이다. 지우면 그 구간의 이력 조회가 느려지고(저널로 답한다) 사실은 사라지지
|
|
133
|
+
* 않는다. 그래서 저널보다 짧게 둘 수 있고, 그러는 편이 낫다 — 화면은 최근을 본다.
|
|
134
|
+
*
|
|
135
|
+
* 저널 정리와 같은 창에서 돌린다(§`TwinEngine.pruneJournal`). 나눠서 지우는 규율도 같다.
|
|
136
|
+
*/
|
|
137
|
+
export async function pruneSubjectRows(
|
|
138
|
+
domainId: string,
|
|
139
|
+
instanceId: string,
|
|
140
|
+
cutoff: Date,
|
|
141
|
+
budgetMs: number,
|
|
142
|
+
batch: number
|
|
143
|
+
): Promise<number> {
|
|
144
|
+
let deleted = 0
|
|
145
|
+
const startedMs = Date.now()
|
|
146
|
+
for (;;) {
|
|
147
|
+
/*
|
|
148
|
+
* 경계를 시각으로 찾는다 — 목차가 `(domain, instance, eventTime)` 이므로 그 순서로 물어야 쓴다.
|
|
149
|
+
* 저널 정리에서 같은 것을 실측으로 배웠다(리비전 순으로 물으면 목차를 못 쓴다).
|
|
150
|
+
*/
|
|
151
|
+
const edge = await getRepository(TwinSubjectEvent)
|
|
152
|
+
.createQueryBuilder('s')
|
|
153
|
+
.select('s.eventTime', 'eventTime')
|
|
154
|
+
.where('s.domain = :domainId', { domainId })
|
|
155
|
+
.andWhere('s.instanceId = :instanceId', { instanceId })
|
|
156
|
+
.andWhere('s.eventTime < :cutoff', { cutoff })
|
|
157
|
+
.orderBy('s.eventTime', 'ASC')
|
|
158
|
+
.offset(batch - 1)
|
|
159
|
+
.limit(1)
|
|
160
|
+
.getRawOne<{ eventTime: unknown }>()
|
|
161
|
+
.catch(() => null)
|
|
162
|
+
|
|
163
|
+
/* 원시 결과의 컬럼 값은 드라이버에게 해석시킨다(§`hydratedDate`). */
|
|
164
|
+
const upTo = hydratedDate(getRepository(TwinSubjectEvent), 'eventTime', edge?.eventTime)
|
|
165
|
+
|
|
166
|
+
const del = getRepository(TwinSubjectEvent)
|
|
167
|
+
.createQueryBuilder()
|
|
168
|
+
.delete()
|
|
169
|
+
.from(TwinSubjectEvent)
|
|
170
|
+
.where('domain_id = :domainId', { domainId })
|
|
171
|
+
.andWhere('instance_id = :instanceId', { instanceId })
|
|
172
|
+
const res = await (upTo
|
|
173
|
+
? del.andWhere('event_time <= :upTo', { upTo })
|
|
174
|
+
: del.andWhere('event_time < :cutoff', { cutoff })
|
|
175
|
+
)
|
|
176
|
+
.execute()
|
|
177
|
+
.catch(err => {
|
|
178
|
+
twinError('[twin-subject] prune fail', err?.message ?? err)
|
|
179
|
+
return { affected: 0 }
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
const n = res.affected ?? 0
|
|
183
|
+
deleted += n
|
|
184
|
+
if (!n) break
|
|
185
|
+
if (Date.now() - startedMs >= budgetMs) break
|
|
186
|
+
await new Promise(resolve => setImmediate(resolve))
|
|
187
|
+
}
|
|
188
|
+
return deleted
|
|
189
|
+
}
|
|
@@ -72,7 +72,7 @@ export class TwinTargetResolver {
|
|
|
72
72
|
if (!isTwinMetric(metric)) throw new Error(`unknown twin metric "${metric}" — see twinTargetMetrics`)
|
|
73
73
|
if (!Number.isFinite(target)) throw new Error('target must be a finite number')
|
|
74
74
|
if (!spaceId && !instanceId) throw new Error('a target must be scoped to a space or an instance')
|
|
75
|
-
/* 축만 있고 값이 없으면 **어디에 거는 목표인지 알 수 없다** —
|
|
75
|
+
/* 축만 있고 값이 없으면 **어디에 거는 목표인지 알 수 없다** — 알리지 않고 전체 목표로 만들지 않는다
|
|
76
76
|
(그러면 사용자는 구역 목표를 걸었다고 믿는데 전체가 바뀐다). */
|
|
77
77
|
if (axis && !axisKey) throw new Error('axisKey is required when axis is given')
|
|
78
78
|
if (!axis && axisKey) throw new Error('axis is required when axisKey is given')
|
package/shared/entity-delta.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 트윈에서 사소한 속성은 없다. 소속 구역·소속 팔레트·수량·만료처럼 곁가지로 보이는 값이 실제로는
|
|
7
7
|
* 구역 롤업·적재 표현·로트 추적의 전제다. 그래서 여기서 **필드를 손으로 골라 담지 않는다.**
|
|
8
8
|
*
|
|
9
|
-
* 예전에는 엔티티마다 필요할 것 같은 필드만 열거했고, 그 결과 계약에 있는 것이
|
|
9
|
+
* 예전에는 엔티티마다 필요할 것 같은 필드만 열거했고, 그 결과 계약에 있는 것이 오류 없이 빠졌다
|
|
10
10
|
* (2026-08-01 감사: 노드 parentId 누락 · 작업 전량 누락 · 물품 전량 누락). 화이트리스트 방식은
|
|
11
11
|
* **계약이 자라도 화면이 따라오지 못한다** — 커널이 속성을 늘릴 때마다 이 파일을 고쳐야 하고, 고치는
|
|
12
12
|
* 것을 잊으면 아무 신호 없이 정보가 사라진다.
|
|
@@ -50,7 +50,7 @@ export const ENERGY_TAG_PREFIX = '__energy__'
|
|
|
50
50
|
|
|
51
51
|
/** 이 트윈의 집약 채널 태그. 트윈 식별자가 없으면 만들지 않는다 — 덮어쓰기의 원인이었다. */
|
|
52
52
|
export function aggregateTag(prefix: string, instanceId: string): string {
|
|
53
|
-
if (!instanceId) throw new Error('aggregateTag: instanceId required —
|
|
53
|
+
if (!instanceId) throw new Error('aggregateTag: instanceId required — tags without instanceId overwrite other twin states across tenants')
|
|
54
54
|
|
|
55
55
|
return `${prefix}:${instanceId}`
|
|
56
56
|
}
|
|
@@ -121,7 +121,7 @@ function aggregateItem(entity: any, idKey: 'id' | 'epc'): any {
|
|
|
121
121
|
/**
|
|
122
122
|
* 물품 집약의 상한 — 큰 현장은 물품이 수만 개라, 전량을 매 브로드캐스팅에 실으면 전달이 현장을 느리게 한다.
|
|
123
123
|
*
|
|
124
|
-
*
|
|
124
|
+
* **알리지 않고 자르지 않는다**: 넘치면 `total`(전체 수)과 `truncated`(잘린 수)를 함께 실어, 소비처가
|
|
125
125
|
* "이게 전부" 라고 오해하지 않게 한다. 개별 물품은 태그별 delta 로 따로 나가고 그쪽은 시그니처 dedup
|
|
126
126
|
* 이 걸려 **변한 것만** 재브로드캐스팅된다.
|
|
127
127
|
*/
|
|
@@ -299,7 +299,7 @@ export function buildEntityDeltas(state: any, instanceId: string, scope?: Entity
|
|
|
299
299
|
...(energy.open ? { open: energy.open } : {}),
|
|
300
300
|
...(energy.peakSince ? { peakSince: energy.peakSince } : {}),
|
|
301
301
|
/* 마감된 구간은 **최근 것만** 낸다 — 하루치를 매 브로드캐스팅에 실으면 채널이 그것으로 찬다.
|
|
302
|
-
전체 수는 함께 낸다(
|
|
302
|
+
전체 수는 함께 낸다(알리지 않고 자르지 않는다). */
|
|
303
303
|
recent: (energy.closed ?? []).slice(-8),
|
|
304
304
|
closedTotal: energy.closedTotal ?? 0
|
|
305
305
|
}
|
package/shared/touched-items.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* ── 모르면 전부다 ───────────────────────────────────────────────────────────
|
|
14
14
|
* 이 함수가 `null` 을 돌려주면 「이 사건이 어느 물품을 건드렸는지 말할 수 없다」는 뜻이고, 그때 호출자는
|
|
15
|
-
* **전부 다시 만든다**(지금까지의 거동). 낯선 어휘가 오면
|
|
15
|
+
* **전부 다시 만든다**(지금까지의 거동). 낯선 어휘가 오면 알리지 않고 빠뜨리는 것이 아니라 비싸게 안전한
|
|
16
16
|
* 쪽으로 떨어진다 — 화면이 낡는 것보다 계산을 한 번 더 하는 것이 낫다.
|
|
17
17
|
*/
|
|
18
18
|
|
|
@@ -25,7 +25,7 @@ function push(out: Set<string>, v: unknown): void {
|
|
|
25
25
|
* 물품을 건드리지 않는다고 **선언된** 사건들 — 커널의 운영 델타와 에너지 사건.
|
|
26
26
|
*
|
|
27
27
|
* 이 목록은 커널 계약(`OP_EVENT`·`ENERGY_EVENT`)에서 왔다. 여기 없는 낱말이 오면 「모른다」로 떨어져
|
|
28
|
-
* 전부 다시 만든다 — 커널이 사건을 늘렸을 때
|
|
28
|
+
* 전부 다시 만든다 — 커널이 사건을 늘렸을 때 알리지 않고 빠뜨리는 대신 비싸게 안전한 쪽을 고른다.
|
|
29
29
|
*
|
|
30
30
|
* 근거: 미러의 리듀서에서 이 사건들은 작업·설비·인원·자산·오더·계량만 바꾼다(물품 축을 만지지 않는다).
|
|
31
31
|
* 자리 점유는 이 사건들도 바꾸지만, 자리는 **어차피 매번 전부 다시 만든다**(위 주석).
|
package/shared/twin-level.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* ── 왜 여기 있나 ───────────────────────────────────────────────────────────
|
|
5
5
|
* 이 목록을 세 곳이 쓴다: 씬 표현이 "나는 어느 단계에 붙는다" 고 선언할 때(클라이언트), 성과 카드가
|
|
6
6
|
* "이 단계로 나눠 달라" 고 물을 때(클라이언트), AI 가 "구역별로 보여 줘" 를 제안할 때(서버).
|
|
7
|
-
* 세 곳이 각자 목록을 적으면 하나가 늘 때 나머지가
|
|
7
|
+
* 세 곳이 각자 목록을 적으면 하나가 늘 때 나머지가 알리지 않고 뒤처진다 — 집약 태그에서 이미 겪었다.
|
|
8
8
|
*
|
|
9
9
|
* 그래서 **한 곳**에 두고 셋이 가져다 쓴다. 이 파일은 import 가 없어 클라이언트 번들에 서버 코드를
|
|
10
10
|
* 끌고 들어가지 않는다.
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* 미러가 비추는 현실은 24시간 돈다. 설비 한 대가 늘었다고 트윈을 멈췄다 세우면 그 사이의
|
|
5
5
|
* 사실을 잃고, 라이브에서 유실은 곧 거짓이다. 그래서 미러는 **돌면서** 갈아탄다.
|
|
6
6
|
*
|
|
7
|
-
* 갈아타기가 옳으려면 **세 층이 함께** 움직여야 한다. 하나라도 빠지면
|
|
7
|
+
* 갈아타기가 옳으려면 **세 층이 함께** 움직여야 한다. 하나라도 빠지면 알리지 않고 어긋난다:
|
|
8
8
|
* ① 리비전 — 이후의 이벤트가 새 번호를 달고 다닌다(재생이 마디를 나눌 수 있게)
|
|
9
9
|
* ② 저장 model·구조 행 — 조회가 새 구조를 본다
|
|
10
|
-
* ③ 커널 — 지금
|
|
10
|
+
* ③ 커널 — 지금 계산에 들어가는 이벤트가 새 구조 위에서 계산된다
|
|
11
11
|
*
|
|
12
12
|
* 커널 단위 거동(무엇을 지키고 무엇을 버리나)은 커널 쪽 `adopt-structure.test.ts` 가 지킨다.
|
|
13
13
|
* 여기서 지키는 것은 **호스트 배선** — 세 층이 같은 인제스트에서 함께 움직이는가.
|
|
@@ -93,7 +93,7 @@ test('돌고 있는 미러는 멈추지 않고 갈아탄다 — 세 층이 함
|
|
|
93
93
|
const snap = TwinEngine.snapshot(domainId, MIRROR)
|
|
94
94
|
assert.equal(snap.locations.filter((n: any) => n.type === 'storage').length, 7, '커널도 새 자리를 들고 있다')
|
|
95
95
|
|
|
96
|
-
/* 무엇이 달라졌는지 **말한다** — 멈췄다 세우는 마디가 없으므로 말하지 않으면
|
|
96
|
+
/* 무엇이 달라졌는지 **말한다** — 멈췄다 세우는 마디가 없으므로 말하지 않으면 알리지 않고 지나간다 */
|
|
97
97
|
const adopted = res.warnings.find((w: any) => w.code === 'structure-adopted')
|
|
98
98
|
assert.ok(adopted, '갈아타기는 말없이 성공하지 않는다')
|
|
99
99
|
assert.equal(adopted.params.locationsAdded, 4, '자리 4개가 늘었다(빈 3→7)')
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ 수 하나가 필요하면 **집계로 묻는다** — `findOne(order: …)` 으로 흉내내지 않는다.
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 이 검사가 필요한가 (2026-08-28 전수 조사) ────────────────────────────
|
|
5
|
+
* 「가장 늦은 사건」·「저널 머리」·「첫 사건」처럼 **한 줄만** 필요한 자리에서 이런 모양을 썼다.
|
|
6
|
+
*
|
|
7
|
+
* findOne({ where: { domain: { id }, … }, order: { revision: 'DESC' } })
|
|
8
|
+
*
|
|
9
|
+
* 관계를 가진 엔티티에서 `find`·`findOne` 은 두 단계 질의가 되고 **안쪽에 상한이 없다** — 조건에 맞는
|
|
10
|
+
* 행을 전부 만든 뒤 밖에서 한 줄을 고른다. 이 파일이 지키는 저장소에서 실측한 값이다.
|
|
11
|
+
*
|
|
12
|
+
* twinEvents 의 같은 모양 0.024초 대 12초 (저널 375만 행)
|
|
13
|
+
* 현장 성과(`latestEventTime`) 7.2초 — 업무 사실 97만 행을 payload 까지 만든 뒤 한 줄
|
|
14
|
+
* 백테스트 구간(`gapWindow`) 같은 모양을 두 번(첫 사건·마지막 사건)
|
|
15
|
+
*
|
|
16
|
+
* **답은 맞고 비용만 틀리다.** 그래서 사람 리뷰가 잡지 못한다 — 기계에 맡긴다.
|
|
17
|
+
* (같은 이유로 만든 검사가 하나 더 있다: `journal-read-discipline`. 그것은 「통째로 읽는 자리」를 보고,
|
|
18
|
+
* 이 검사는 「한 줄만 필요한데 통째로 읽는 자리」를 본다.)
|
|
19
|
+
*
|
|
20
|
+
* ── 규율 ────────────────────────────────────────────────────────────────────
|
|
21
|
+
* 큰 표에서 한 줄·한 수가 필요하면 질의 빌더로 `MIN`·`MAX`·`COUNT` 를 묻는다. 색인의 끝을 집으므로
|
|
22
|
+
* 행을 만들지 않는다.
|
|
23
|
+
*/
|
|
24
|
+
import { test } from 'node:test'
|
|
25
|
+
import assert from 'node:assert/strict'
|
|
26
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs'
|
|
27
|
+
import { join } from 'node:path'
|
|
28
|
+
|
|
29
|
+
const ROOT = join(import.meta.dirname, '..', 'server')
|
|
30
|
+
|
|
31
|
+
/** 이 검사가 보는 큰 표 — 행이 수백만이 될 수 있는 것들. */
|
|
32
|
+
const BIG_TABLES = ['TwinEvent', 'TwinSubjectEvent']
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 예외 — **이유와 함께** 적는다. 이유가 없으면 예외가 아니다.
|
|
36
|
+
*
|
|
37
|
+
* 지금은 비어 있다. 예외를 더할 때는 왜 한 줄이 아니라 여러 행이 필요한지, 그리고 없어질 조건을 함께 적는다.
|
|
38
|
+
*/
|
|
39
|
+
const ALLOWED: { file: string; why: string }[] = []
|
|
40
|
+
|
|
41
|
+
function sites(): { file: string; line: number; snippet: string }[] {
|
|
42
|
+
const out: { file: string; line: number; snippet: string }[] = []
|
|
43
|
+
const walk = (dir: string) => {
|
|
44
|
+
for (const name of readdirSync(dir)) {
|
|
45
|
+
const p = join(dir, name)
|
|
46
|
+
if (statSync(p).isDirectory()) { walk(p); continue }
|
|
47
|
+
if (!name.endsWith('.ts')) continue
|
|
48
|
+
const src = readFileSync(p, 'utf8')
|
|
49
|
+
/*
|
|
50
|
+
* 큰 표의 저장소에서 `findOne` 을 부르면서 `order:` 를 주는 모양을 찾는다. 정렬을 준다는 것은
|
|
51
|
+
* 「끝에서 한 줄」을 원한다는 뜻이고, 그것이 곧 집계다.
|
|
52
|
+
*
|
|
53
|
+
* 변수에 담아 부르는 형태도 본다 — 그러지 않으면 이름만 바꿔 이 검사를 지나칠 수 있다.
|
|
54
|
+
*/
|
|
55
|
+
for (const table of BIG_TABLES) {
|
|
56
|
+
const vars = new Set<string>()
|
|
57
|
+
for (const m of src.matchAll(new RegExp(`const\\s+(\\w+)\\s*=\\s*getRe(?:ad)?Repository\\(${table}\\)`, 'g'))) {
|
|
58
|
+
vars.add(m[1])
|
|
59
|
+
}
|
|
60
|
+
const receivers = [`getRepository(${table})`, `getReadRepository(${table})`, ...[...vars]]
|
|
61
|
+
for (const recv of receivers) {
|
|
62
|
+
let at = src.indexOf(recv)
|
|
63
|
+
while (at >= 0) {
|
|
64
|
+
const after = src.slice(at + recv.length, at + recv.length + 400)
|
|
65
|
+
const call = /^\s*\n?\s*\.findOne\(\s*\{([\s\S]{0,320})/.exec(after)
|
|
66
|
+
if (call && /order\s*:/.test(call[1])) {
|
|
67
|
+
out.push({
|
|
68
|
+
file: p.slice(ROOT.length + 1),
|
|
69
|
+
line: src.slice(0, at).split('\n').length,
|
|
70
|
+
snippet: call[1].replace(/\s+/g, ' ').slice(0, 90)
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
at = src.indexOf(recv, at + 1)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
walk(ROOT)
|
|
80
|
+
return out
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
test('★ 큰 표에서 한 줄을 원하면 집계로 묻는다 — findOne(order) 으로 흉내내지 않는다', () => {
|
|
84
|
+
const found = sites().filter(s => !ALLOWED.some(a => a.file === s.file))
|
|
85
|
+
assert.deepEqual(
|
|
86
|
+
found.map(s => `${s.file}:${s.line} — ${s.snippet}`),
|
|
87
|
+
[],
|
|
88
|
+
'이 모양은 조건에 맞는 행을 전부 만든 뒤 한 줄을 고른다. 질의 빌더로 MIN·MAX·COUNT 를 묻는다.\n' +
|
|
89
|
+
'정말 여러 행이 필요하면 ALLOWED 에 이유와 없어질 조건을 함께 적는다.'
|
|
90
|
+
)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('예외 목록에는 반드시 이유가 붙는다 — 이름만 적어 지나가지 않게', () => {
|
|
94
|
+
for (const a of ALLOWED) assert.ok(a.why.trim().length > 20, `${a.file} 의 예외에 이유가 없다`)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('★ 고친 자리들이 실제로 집계를 쓴다 — 되돌아가지 않게 이름으로 못박는다', () => {
|
|
98
|
+
/*
|
|
99
|
+
* 이름으로 못박는 이유: 되돌아가도 **답은 맞기 때문에** 결과를 보는 시험으로는 잡히지 않는다.
|
|
100
|
+
*/
|
|
101
|
+
const kpi = readFileSync(join(ROOT, 'engine', 'kpi-query.ts'), 'utf8')
|
|
102
|
+
assert.match(kpi, /select\('MAX\(e\.eventTime\)', 'at'\)/, '마지막 업무 기록 시각은 집계로 묻는다')
|
|
103
|
+
|
|
104
|
+
const engine = readFileSync(join(ROOT, 'engine', 'twin-engine.ts'), 'utf8')
|
|
105
|
+
assert.match(engine, /select\('MAX\(e\.revision\)', 'max'\)/, '저널 머리는 집계로 묻는다')
|
|
106
|
+
|
|
107
|
+
const forecast = readFileSync(join(ROOT, 'service', 'twin-forecast', 'twin-forecast-query.ts'), 'utf8')
|
|
108
|
+
assert.match(forecast, /\$\{agg\}\(e\.eventTime\)/, '백테스트 구간의 양 끝은 집계로 묻는다')
|
|
109
|
+
|
|
110
|
+
const model = readFileSync(join(ROOT, 'service', 'twin-model', 'twin-model-query.ts'), 'utf8')
|
|
111
|
+
assert.match(model, /select\('MAX\(e\.revision\)', 'max'\)/, '모델 요약의 저널 끝도 집계로 묻는다')
|
|
112
|
+
})
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* 둘뿐이다 — 레일에 20건만 그리고, 자리 7곳의 색을 뽑는다.
|
|
8
8
|
*
|
|
9
9
|
* ── 이 테스트가 지키는 것 ───────────────────────────────────────────────────
|
|
10
|
-
* ① **자리 색은 전량을 근거로**
|
|
11
|
-
* ②
|
|
10
|
+
* ① **자리 색은 전량을 근거로** 계산된다. 상위 N 으로 줄이면 20위 밖의 신호를 가진 자리가 색을 잃는다.
|
|
11
|
+
* ② **알리지 않고 자르지 않는다.** 전체 개수를 함께 보내야 레일이 "N건 더" 를 말할 수 있다.
|
|
12
12
|
* ③ **순서가 화면과 같다.** 서버가 고른 20건이 화면이 고를 20건과 달라지면 아무 데서도 안 드러난다.
|
|
13
13
|
*/
|
|
14
14
|
import { test } from 'node:test'
|
|
@@ -18,15 +18,15 @@ import { digestAttentions, compareAttentions, mergeLensAttentions } from '../ser
|
|
|
18
18
|
|
|
19
19
|
const attn = (id: string, severity: string, locationId?: string, state?: string) => ({ id, severity, state, anchor: locationId ? { locationId } : undefined })
|
|
20
20
|
|
|
21
|
-
test('상위 N 만 남기고 전체 개수를 함께 낸다 —
|
|
21
|
+
test('상위 N 만 남기고 전체 개수를 함께 낸다 — 알리지 않고 자르지 않는다', () => {
|
|
22
22
|
const all = Array.from({ length: 50 }, (_, i) => attn(`a${i}`, 'low', `loc-${i % 3}`))
|
|
23
23
|
const d = digestAttentions(all, 20)
|
|
24
24
|
assert.equal(d.attentions.length, 20)
|
|
25
25
|
assert.equal(d.attentionTotal, 50, '전체 개수가 없으면 화면이 "괜찮아졌다" 로 읽는다')
|
|
26
26
|
})
|
|
27
27
|
|
|
28
|
-
test('자리 색은 전량을 근거로
|
|
29
|
-
/* 앞 20건은 loc-hot 에, 21번째 한 건만 loc-cold 에 — 잘린 뒤에
|
|
28
|
+
test('자리 색은 전량을 근거로 처리한다 — 상위 N 밖의 자리도 색을 갖는다', () => {
|
|
29
|
+
/* 앞 20건은 loc-hot 에, 21번째 한 건만 loc-cold 에 — 잘린 뒤에 계산하면 loc-cold 가 색을 잃는다. */
|
|
30
30
|
const all = [...Array.from({ length: 20 }, (_, i) => attn(`hot${i}`, 'high', 'loc-hot')), attn('cold', 'critical', 'loc-cold')]
|
|
31
31
|
const d = digestAttentions(all, 20)
|
|
32
32
|
assert.equal(d.attentions.length, 20)
|
|
@@ -81,7 +81,7 @@ test('정렬 함수는 원본을 바꾸지 않는다 — 스냅샷을 제자리
|
|
|
81
81
|
* 한 공장에 WMS·MES·YMS 가 함께 있으므로 "이 현장에서 봐야 할 것" 은 렌즈 하나로 답할 수 없다.
|
|
82
82
|
* 예전에는 화면이 인스턴스마다 물어 손으로 합쳤고, 그러면 정렬 규칙이 화면마다 갈린다.
|
|
83
83
|
*
|
|
84
|
-
* 모으면서
|
|
84
|
+
* 모으면서 알리지 않고 틀릴 수 있는 자리 셋을 여기서 못 박는다.
|
|
85
85
|
*/
|
|
86
86
|
test('★ 어느 트윈에서 온 신호인지 붙는다 — 조치는 결국 그 트윈에 보내야 한다', () => {
|
|
87
87
|
const d = mergeLensAttentions([
|
package/test/axis-read.test.ts
CHANGED
|
@@ -20,7 +20,7 @@ test('경로가 없는 축은 이름으로 읽는다 — 지금까지의 거동'
|
|
|
20
20
|
assert.deepEqual(axisValueOf(state, { axis: 'tasks' }), [], '빈 배열은 「없다」다 — 모른다가 아니다')
|
|
21
21
|
})
|
|
22
22
|
|
|
23
|
-
test('경로가 있으면 그 경로로 읽는다 — 중첩된 축이
|
|
23
|
+
test('경로가 있으면 그 경로로 읽는다 — 중첩된 축이 오류 없이 사라지지 않게', () => {
|
|
24
24
|
const state = { energy: { closed: [{ startMs: 1 }, { startMs: 2 }] } }
|
|
25
25
|
const value = axisValueOf(state, { axis: 'demandWindows', path: 'energy.closed' })
|
|
26
26
|
assert.equal(Array.isArray(value), true)
|
|
@@ -48,10 +48,11 @@ test('경로 따라가기는 중간 부재에서 멈춘다 — 오류를 내지
|
|
|
48
48
|
|
|
49
49
|
test('커널 선언과 상태의 모양이 맞는다 — 선언한 경로에 정말 값이 있다', async () => {
|
|
50
50
|
/*
|
|
51
|
-
* 선언(`path`)과 실제 상태의 모양이 어긋나면 이 배선은
|
|
51
|
+
* 선언(`path`)과 실제 상태의 모양이 어긋나면 이 배선은 알리지 않고 빈 카드를 만든다. 그래서 **커널이 낸
|
|
52
52
|
* 상태로** 확인한다 — 선언을 읽는 쪽과 값을 만드는 쪽이 같은 모양을 쓰는지 보는 것이 요점이다.
|
|
53
53
|
*/
|
|
54
|
-
const { EmsKernel,
|
|
54
|
+
const { EmsKernel, DEMAND_WINDOW_MS } = await import('@operato/twin-kernel')
|
|
55
|
+
const { axisInfo, ENERGY_EVENT } = await import('@operato/ops-contract')
|
|
55
56
|
const info = axisInfo('demandWindows')!
|
|
56
57
|
assert.ok(info, '커널이 그 축을 선언해야 한다')
|
|
57
58
|
assert.equal(info.path, 'energy.closed')
|
|
@@ -134,7 +135,7 @@ test('축을 이름으로 읽는 자리가 없다 — 선언된 경로를 지나
|
|
|
134
135
|
test('수요 구간은 실제로 중첩된 축이다 — 이 가드가 지키는 대상이 있다', () => {
|
|
135
136
|
/* 가드가 지킬 것이 없으면 그 가드는 초록으로 거짓을 말한다. 대상이 실재하는지 함께 본다. */
|
|
136
137
|
const req = createRequire(import.meta.url)
|
|
137
|
-
const { TWIN_AXES } = req('@operato/
|
|
138
|
+
const { TWIN_AXES } = req('@operato/ops-contract')
|
|
138
139
|
const nested = (TWIN_AXES as any[]).filter(a => a.path)
|
|
139
140
|
|
|
140
141
|
assert.ok(nested.length > 0, '경로를 가진 축이 하나도 없으면 이 가드는 의미가 없다')
|
|
@@ -156,7 +157,7 @@ test('수요 구간은 실제로 중첩된 축이다 — 이 가드가 지키는
|
|
|
156
157
|
*/
|
|
157
158
|
test('id 가 없는 상태 축은 무엇이 항목을 가리키는지 선언한다', () => {
|
|
158
159
|
const req2 = createRequire(import.meta.url)
|
|
159
|
-
const { TWIN_AXES } = req2('@operato/
|
|
160
|
+
const { TWIN_AXES } = req2('@operato/ops-contract')
|
|
160
161
|
const dw = (TWIN_AXES as any[]).find(a => a.axis === 'demandWindows')
|
|
161
162
|
|
|
162
163
|
assert.ok(dw, '수요 구간 축이 있어야 한다')
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 지난 기록 채우기의 판단 — **무엇을 받고 무엇을 거절하나.**
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 저널에만 쓰나 (2026-08-30) ──────────────────────────────────────────
|
|
5
|
+
* 커널의 상태는 「지금」이다. 늦게 온 옛 표본은 가드가 막는다(그렇게 만들었다). 지난 기록을 라이브
|
|
6
|
+
* 문으로 넣으면 가드에 막히거나 트윈의 시계가 과거로 끌린다. 그래서 이 길은 저널에만 쓰고,
|
|
7
|
+
* **상태를 바꾸는 사실은 아예 받지 않는다.**
|
|
8
|
+
*
|
|
9
|
+
* ── 보존 안에서만 (사용자 결정) ────────────────────────────────────────────
|
|
10
|
+
* 보존보다 오래된 것을 채우면 다음 정리에서 지워진다. 조용히 넣고 지워지면 아무도 모르므로
|
|
11
|
+
* 거절하고 이유를 말한다.
|
|
12
|
+
*/
|
|
13
|
+
import { test } from 'node:test'
|
|
14
|
+
import assert from 'node:assert/strict'
|
|
15
|
+
|
|
16
|
+
import { judgeBackfill, splitAlreadyThere, PERIOD_FACTS } from '../server/service/twin-backfill/backfill-shape.ts'
|
|
17
|
+
|
|
18
|
+
const NOW = Date.parse('2026-08-30T00:00:00.000Z')
|
|
19
|
+
const env = (eventType: string, eventTime: string, eventId = `${eventType}|${eventTime}`) => ({ eventType, eventTime, eventId })
|
|
20
|
+
|
|
21
|
+
test('★ 마감된 구간 사실을 받는다', () => {
|
|
22
|
+
const r = judgeBackfill([env('energy.usage.period', '2026-08-29T00:00:00.000Z')], { nowMs: NOW, retentionDays: 7 })
|
|
23
|
+
assert.equal(r.keep.length, 1)
|
|
24
|
+
assert.equal(r.refused.length, 0)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('★ 상태를 바꾸는 사실은 받지 않는다 — 저널에만 넣으면 상태와 저널이 갈라진다', () => {
|
|
28
|
+
const r = judgeBackfill(
|
|
29
|
+
[env('energy.measured', '2026-08-29T00:00:00.000Z'), env('energy.generated', '2026-08-29T00:00:00.000Z')],
|
|
30
|
+
{ nowMs: NOW, retentionDays: 7 }
|
|
31
|
+
)
|
|
32
|
+
assert.equal(r.keep.length, 0)
|
|
33
|
+
assert.equal(r.refused.length, 2)
|
|
34
|
+
assert.match(r.refused[0].reason, /라이브 문으로 보내라/)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('★ 보존보다 오래된 것은 거절하고 이유를 말한다 — 넣어도 지워진다', () => {
|
|
38
|
+
const r = judgeBackfill([env('energy.usage.period', '2026-08-01T00:00:00.000Z')], { nowMs: NOW, retentionDays: 7 })
|
|
39
|
+
assert.equal(r.keep.length, 0)
|
|
40
|
+
assert.match(r.refused[0].reason, /보존 기간\(7일\)보다 오래됐다/)
|
|
41
|
+
assert.match(r.refused[0].reason, /보존 기간을 늘려야 한다/, '무엇을 해야 하는지 함께 말한다')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('보존 안쪽 경계는 받는다', () => {
|
|
45
|
+
const justInside = new Date(NOW - 7 * 86_400_000 + 1000).toISOString()
|
|
46
|
+
const r = judgeBackfill([env('energy.usage.period', justInside)], { nowMs: NOW, retentionDays: 7 })
|
|
47
|
+
assert.equal(r.keep.length, 1)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('보존 정책이 없으면 오래됨을 판정하지 않는다 — 없는 기준으로 거절하지 않는다', () => {
|
|
51
|
+
const r = judgeBackfill([env('energy.usage.period', '2020-01-01T00:00:00.000Z')], { nowMs: NOW })
|
|
52
|
+
assert.equal(r.keep.length, 1)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('★ 아직 오지 않은 시각은 거절한다 — 지난 기록이 아니다', () => {
|
|
56
|
+
const r = judgeBackfill([env('energy.usage.period', '2026-09-01T00:00:00.000Z')], { nowMs: NOW, retentionDays: 7 })
|
|
57
|
+
assert.equal(r.keep.length, 0)
|
|
58
|
+
assert.match(r.refused[0].reason, /아직 오지 않은 시각/)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('시각을 읽을 수 없으면 거절한다', () => {
|
|
62
|
+
const r = judgeBackfill([env('energy.usage.period', 'n/a')], { nowMs: NOW, retentionDays: 7 })
|
|
63
|
+
assert.equal(r.keep.length, 0)
|
|
64
|
+
assert.match(r.refused[0].reason, /읽을 수 없다/)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('받는 종류 다섯이 모두 마감된 구간 사실이다', () => {
|
|
68
|
+
for (const t of PERIOD_FACTS) {
|
|
69
|
+
const r = judgeBackfill([env(t, '2026-08-29T00:00:00.000Z')], { nowMs: NOW, retentionDays: 7 })
|
|
70
|
+
assert.equal(r.keep.length, 1, `${t} 를 받아야 한다`)
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
/* ── 같은 기간을 두 번 넣지 않는다 ────────────────────────────────────────── */
|
|
75
|
+
|
|
76
|
+
test('★ 이미 있는 것은 건너뛴다 — 다시 읽어도 중복이 쌓이지 않는다', () => {
|
|
77
|
+
const a = env('energy.usage.period', '2026-08-29T00:00:00.000Z')
|
|
78
|
+
const b = env('energy.usage.period', '2026-08-29T01:00:00.000Z')
|
|
79
|
+
const r = splitAlreadyThere([a, b], new Set([a.eventId]))
|
|
80
|
+
assert.equal(r.fresh.length, 1)
|
|
81
|
+
assert.equal(r.fresh[0].eventId, b.eventId)
|
|
82
|
+
assert.equal(r.skipped, 1)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('id 가 없으면 넣지 않는다 — 같은 사실인지 판단할 근거가 없다', () => {
|
|
86
|
+
const r = splitAlreadyThere([{ eventType: 'energy.usage.period', eventTime: '2026-08-29T00:00:00.000Z' }], new Set())
|
|
87
|
+
assert.equal(r.fresh.length, 0)
|
|
88
|
+
assert.equal(r.skipped, 1)
|
|
89
|
+
})
|
package/test/boot-resume.test.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* 서버를 한 번 재기동하면 등록부는 `running` 이라 말하는데 **아무 커널도 돌지 않았다**.
|
|
6
6
|
* `bootstrap()` 은 상태만 되찾아 `recovered` 에 담았고, 커널을 세우는 것은 명시 mutation 뿐이었다.
|
|
7
7
|
* 미러 트윈에서는 계측까지 끊겼다 — `startReferenceLiveFeed` 를 부르는 곳이 `importTwinReference`
|
|
8
|
-
* 하나뿐이어서, 붙어 있던 커넥터가
|
|
8
|
+
* 하나뿐이어서, 붙어 있던 커넥터가 알리지 않고 떨어졌다(오류도 로그도 없이).
|
|
9
9
|
*
|
|
10
10
|
* 이 시험이 지키는 것 넷:
|
|
11
11
|
* ① 부팅이 런타임까지 되살린다(상태 복구만으로는 도는 트윈이 아니다)
|
|
@@ -106,9 +106,9 @@ test('정책을 다시 선언하는 문이 있다 — 없으면 선언과 쓰임
|
|
|
106
106
|
'utf-8'
|
|
107
107
|
)
|
|
108
108
|
assert.match(src, /async setTwinRestartPolicy\(/, '만든 뒤에 고칠 길이 있어야 한다')
|
|
109
|
-
/* 값은 엄격히 읽는다 — 모르는 말을 기본값으로 메우면 그 트윈의 이력이 남을지가
|
|
109
|
+
/* 값은 엄격히 읽는다 — 모르는 말을 기본값으로 메우면 그 트윈의 이력이 남을지가 알리지 않고 결정된다. */
|
|
110
110
|
assert.match(src, /readRestartPolicy\(restartPolicy, /)
|
|
111
|
-
/* 도는 런타임을
|
|
111
|
+
/* 도는 런타임을 알리지 않고 갈아 끼우지 않는다 — 다르다는 사실을 답에 담는다. */
|
|
112
112
|
assert.match(src, /appliesAtNextStart/)
|
|
113
113
|
})
|
|
114
114
|
|
|
@@ -130,7 +130,7 @@ test('피드 재개는 등록부가 「도는 미러」라고 말하는 것만
|
|
|
130
130
|
assert.match(live, /export async function resumeReferenceLiveFeeds/, '재개 함수가 있어야 한다')
|
|
131
131
|
assert.match(live, /r\.status === 'running' && r\.restartPolicy === 'resync'/, '도는 미러만 골라야 한다')
|
|
132
132
|
assert.match(live, /discoverSites/, '사이트 서술자는 다시 찾아서 얻는다(hint 를 남기지 않았다)')
|
|
133
|
-
/* 붙지 않은 미러는 「도는데 아무것도 오지 않는」 트윈이다 —
|
|
133
|
+
/* 붙지 않은 미러는 「도는데 아무것도 오지 않는」 트윈이다 — 알리지 않고 넘기지 않는다. */
|
|
134
134
|
assert.match(live, /NOT reattached/, '못 붙인 것은 이유와 함께 남겨야 한다')
|
|
135
135
|
})
|
|
136
136
|
|
|
@@ -159,7 +159,11 @@ test('시뮬 브로드캐스팅도 모아서 한 번 — delta 마다 전 상태
|
|
|
159
159
|
})
|
|
160
160
|
|
|
161
161
|
test('flush 가 시뮬도 본다 — 브로드캐스팅과 저널은 둘 다, state 채널은 라이브만', () => {
|
|
162
|
-
|
|
162
|
+
/*
|
|
163
|
+
* 창의 본문은 `flushBroadcastWindow` 로 옮겼다(2026-08-27). `flushLiveBroadcasts` 는 겹쳐 도는 것을
|
|
164
|
+
* 막는 문만 남았다 — 발행이 청크로 나뉘어 한 창이 주기를 넘길 수 있게 됐기 때문이다.
|
|
165
|
+
*/
|
|
166
|
+
const body = methodOf(engine, 'flushBroadcastWindow')
|
|
163
167
|
assert.equal(/mode !== 'live'\) continue/.test(body), false, '시뮬을 건너뛰는 모양으로 되돌아갔다')
|
|
164
168
|
assert.match(body, /const isLive = inst\.mode === 'live'/, '모드는 가르되 둘 다 브로드캐스팅해야 한다')
|
|
165
169
|
/*
|
|
@@ -181,7 +185,16 @@ test('구독자 하나가 호스트를 죽이지 못한다 — 그리고 못 보
|
|
|
181
185
|
const guardBody = methodOf(engine, 'publishGuarded')
|
|
182
186
|
assert.equal((engine.match(/pubsub\.publish\(/g) ?? []).length, 1, '가드를 거치지 않는 직접 브로드캐스팅이 남아 있다')
|
|
183
187
|
assert.match(guardBody, /pubsub\.publish\(/, '그 하나는 가드 안에 있어야 한다')
|
|
184
|
-
|
|
188
|
+
/*
|
|
189
|
+
* ── 이 가드가 무엇을 못 하는지 (2026-08-27 측정으로 확인) ───────────────────
|
|
190
|
+
* 이 try/catch 는 **대기열 넘침을 막지 못한다.** 그 예외는 `EventTarget` 이 부른 리스너 안에서 나고
|
|
191
|
+
* 런타임은 그것을 호출자에게 주지 않는다(uncaughtException 으로 간다). 그것을 막는 것은 한 차례에
|
|
192
|
+
* 발행하는 수를 상한 아래로 두는 것이고, 그 규율은 `broadcast-queue-overflow.test.ts` 가 지킨다.
|
|
193
|
+
*
|
|
194
|
+
* 그래도 남긴다: 직렬화 실패처럼 `publish` 가 직접 던지는 경우를 잡고, 못 보낸 태그의 시그니처를
|
|
195
|
+
* 되돌린다(남겨 두면 그 태그가 오류 없이 영원히 낡는다).
|
|
196
|
+
*/
|
|
197
|
+
assert.match(guardBody, /catch/, 'publish 가 직접 던지는 경우는 잡아야 한다')
|
|
185
198
|
assert.match(guardBody, /twinWarn/, '못 보낸 사실은 남아야 한다(삼키면 「보냈는데 낡았다」가 된다)')
|
|
186
199
|
/* 시그니처를 남겨 두면 다음 주기에 「무변화」로 건너뛰고 그 태그는 영원히 낡는다. */
|
|
187
200
|
assert.match(engine, /if \(!this\.publishGuarded\('data'[\s\S]{0,80}sigs\.delete\(tag\)/, '실패한 태그의 시그니처를 되돌려야 한다')
|
|
@@ -204,7 +217,7 @@ test('굶김은 예산과 다른 문턱으로 판정한다 — 조금 느린 트
|
|
|
204
217
|
test('한 번으로 내리지 않고, 한 번 내려오면 연속이 끊긴다', () => {
|
|
205
218
|
const body = methodOf(engine, 'guardStarvation')
|
|
206
219
|
assert.match(body, /streak < this\.STARVE_STREAK\) return/, '연속이 차기 전에는 멈추지 않는다')
|
|
207
|
-
/* 웜스타트 직후 첫 틱은 원래 무겁다(복구한 상태를 처음
|
|
220
|
+
/* 웜스타트 직후 첫 틱은 원래 무겁다(복구한 상태를 처음 계산한다) — 그것으로 내리면 재개가 무의미해진다. */
|
|
208
221
|
assert.match(body, /this\.starveStreak\.delete\(key\)[\s\S]{0,40}return/, '문턱 아래면 연속을 끊어야 한다')
|
|
209
222
|
})
|
|
210
223
|
|