@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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* ── 왜 이 시험이 있나 (2026-08-18) ──────────────────────────────────────────
|
|
5
5
|
* 화면은 속성마다 「무엇을 바꾸나」를 말한다. 등록되지 않은 속성에는 「표시만 됩니다」라고 한다 — 그것이
|
|
6
6
|
* 정직한 기본값이다. 그런데 에너지 값들(계약전력·정격·요금)은 **오래전부터 읽히고 있었는데** 등록부에
|
|
7
|
-
* 없었다: 사용자는 계약전력을 넣고도 「이 값은 아무것도 바꾸지 않는다」는 문장을 읽었다.
|
|
7
|
+
* 없었다: 사용자는 계약전력을 넣고도 「이 값은 아무것도 바꾸지 않는다」는 문장을 읽었다. 들어온 것이 없는 거짓말이다.
|
|
8
8
|
*
|
|
9
9
|
* 반대 방향의 위험도 있다: 효과를 등록해 두고 **읽는 코드가 없으면** 화면이 「쓰입니다」라고 말하는데
|
|
10
10
|
* 아무 일도 일어나지 않는다. 그래서 등록부의 각 줄이 가리키는 자리를 이 시험이 확인한다.
|
|
@@ -19,10 +19,14 @@ import { PROPERTY_EFFECTS, propertyEffectsOf, declarationTargetsOf } from '../se
|
|
|
19
19
|
|
|
20
20
|
const req = createRequire(import.meta.url)
|
|
21
21
|
/*
|
|
22
|
-
*
|
|
23
|
-
* 그래서 모듈마다 읽지 않고
|
|
22
|
+
* 커널과 계약은 각각 **한 벌로 묶인 산출물**을 내보낸다(`dist-cjs/index.cjs`) — 서브패스는 exports 로
|
|
23
|
+
* 막혀 있다. 그래서 모듈마다 읽지 않고 묶음을 읽는다.
|
|
24
|
+
*
|
|
25
|
+
* **둘 다 읽는다** — 어휘와 판별은 계약에, 그것을 쓰는 엔진은 커널에 있다(2026-08-30 층 분리).
|
|
26
|
+
* 한쪽만 읽으면 「읽는 코드가 없다」는 거짓 판정이 난다.
|
|
24
27
|
*/
|
|
25
|
-
const kernelBundle =
|
|
28
|
+
const kernelBundle =
|
|
29
|
+
readFileSync(req.resolve('@operato/twin-kernel'), 'utf-8') + '\n' + readFileSync(req.resolve('@operato/ops-contract'), 'utf-8')
|
|
26
30
|
const hostSrc = (file: string): string => readFileSync(fileURLToPath(new URL(`../server/${file}`, import.meta.url)), 'utf-8')
|
|
27
31
|
|
|
28
32
|
test('등록부의 모든 줄이 자기 자리를 가리킨다 — 빈 항목이 없다', () => {
|
|
@@ -65,7 +69,7 @@ test('축으로 갈라 낸다 — 그 축의 항목에만 효과를 말한다',
|
|
|
65
69
|
test('요금은 커널이 아니라 호스트가 읽는다 — 그 사실을 readBy 가 말한다', () => {
|
|
66
70
|
const tariff = PROPERTY_EFFECTS.filter(e => e.id.startsWith('tariff.'))
|
|
67
71
|
assert.equal(tariff.length, 3)
|
|
68
|
-
for (const e of tariff) assert.match(e.readBy, /kpi-query/, '금액은 저널을
|
|
72
|
+
for (const e of tariff) assert.match(e.readBy, /kpi-query/, '금액은 저널을 계산하는 쪽이 만든다')
|
|
69
73
|
})
|
|
70
74
|
|
|
71
75
|
/*
|
|
@@ -110,7 +114,7 @@ test('수율·셋업이 커널 창구까지 배선돼 있다 — 등록만 하
|
|
|
110
114
|
const divisions = layer.match(/pct \/ 100/g) ?? []
|
|
111
115
|
assert.equal(divisions.length, 1, `퍼센트 환산은 한 자리에서만 (발견 ${divisions.length})`)
|
|
112
116
|
assert.match(hostSrc('engine/twin-engine.ts'), /declareParameters\(params\)/, '기동·구조전환 때 커널에 싣는다')
|
|
113
|
-
/* 커널이 그 창구를 갖지 않은 버전이면 **말한다** —
|
|
117
|
+
/* 커널이 그 창구를 갖지 않은 버전이면 **말한다** — 알리지 않고 상수로 돌지 않게. */
|
|
114
118
|
assert.match(hostSrc('engine/twin-engine.ts'), /declareParameters missing/, '버전 어긋남을 경고로 드러낸다')
|
|
115
119
|
})
|
|
116
120
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ 연결된 시스템에 닿지 못한 것과 다시 닿은 것을 인제스트 레저가 기록한다.
|
|
3
|
+
*
|
|
4
|
+
* ── 무엇이 없었나 (2026-08-27) ──────────────────────────────────────────────
|
|
5
|
+
* 커넥터는 폴링이 실패할 때 세 가지를 안다: 연달아 몇 번인지 · 언제부터인지 · 다음에 언제 다시 묻는지.
|
|
6
|
+
* 그런데 어댑터 계약에는 이유 한 줄만 받는 자리가 있었다. 그래서 커넥터가 세 가지를 **한국어 문장으로
|
|
7
|
+
* 합쳐** 이유 칸에 넣고 있었다(`ppms-sunsage.ts`).
|
|
8
|
+
*
|
|
9
|
+
* 두 가지가 틀어진다.
|
|
10
|
+
* ① 연달아 횟수와 시작 시각은 이 레저가 **이미 센다**. 두 곳에서 세면 갈라진다.
|
|
11
|
+
* ② 그 칸은 화면에 그려지고 화면은 다섯 언어로 그린다. 한 언어의 문장은 번역되지 않는다.
|
|
12
|
+
*
|
|
13
|
+
* 그리고 회복을 받는 자리가 아예 없었다. 읽기가 다시 성공하면 실패 기록만 지워졌고, 그러면 밤새 두 시간
|
|
14
|
+
* 끊겼던 연결과 한 번도 끊기지 않은 연결이 아침에 똑같이 보인다.
|
|
15
|
+
*/
|
|
16
|
+
import { test } from 'node:test'
|
|
17
|
+
import assert from 'node:assert/strict'
|
|
18
|
+
import {
|
|
19
|
+
newIngestLedger,
|
|
20
|
+
recordReadFailure,
|
|
21
|
+
recordRecovered,
|
|
22
|
+
clearReadFailure,
|
|
23
|
+
ingestHealth
|
|
24
|
+
} from '../server/engine/ingest-health.js'
|
|
25
|
+
|
|
26
|
+
const t0 = Date.UTC(2026, 7, 27, 3, 0, 0)
|
|
27
|
+
|
|
28
|
+
test('★ 연달아 실패한 횟수와 시작 시각은 레저가 센다 — 커넥터가 보내지 않는다', () => {
|
|
29
|
+
const l = newIngestLedger()
|
|
30
|
+
recordReadFailure(l, 'connect timeout', t0, 'inverters')
|
|
31
|
+
recordReadFailure(l, 'connect timeout', t0 + 60_000, 'inverters')
|
|
32
|
+
recordReadFailure(l, 'connect timeout', t0 + 120_000, 'inverters')
|
|
33
|
+
assert.equal(l.readFailure?.consecutive, 3)
|
|
34
|
+
assert.equal(l.readFailure?.sinceMs, t0, '처음 실패한 시각을 붙잡는다')
|
|
35
|
+
assert.equal(l.readFailure?.lastAtMs, t0 + 120_000)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('★ 이유는 이유만 담는다 — 세는 값을 문장으로 합쳐 넣지 않는다', () => {
|
|
39
|
+
/*
|
|
40
|
+
* 이 테스트는 사람이 읽는 문장이 이유 칸에 들어가는 것을 막지 못한다(문자열은 무엇이든 담긴다).
|
|
41
|
+
* 막을 수 있는 것은 **그 값들을 담을 다른 자리가 있다**는 사실뿐이다. 그것을 테스트로 고정한다.
|
|
42
|
+
*/
|
|
43
|
+
const l = newIngestLedger()
|
|
44
|
+
recordReadFailure(l, 'connect timeout', t0, 'inverters', 480_000)
|
|
45
|
+
assert.equal(l.readFailure?.reason, 'connect timeout')
|
|
46
|
+
assert.equal(l.readFailure?.nextRetryMs, 480_000)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('다음 시도까지의 시간은 있을 때만 적는다 — 0 이나 없는 값을 「곧 다시 묻는다」로 만들지 않는다', () => {
|
|
50
|
+
const l = newIngestLedger()
|
|
51
|
+
recordReadFailure(l, 'connect timeout', t0)
|
|
52
|
+
assert.equal('nextRetryMs' in (l.readFailure as any), false)
|
|
53
|
+
recordReadFailure(l, 'connect timeout', t0 + 1000, undefined, 0)
|
|
54
|
+
assert.equal('nextRetryMs' in (l.readFailure as any), false)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('★ 돌아오면 실패 기록이 사라지고 돌아온 사실이 남는다', () => {
|
|
58
|
+
const l = newIngestLedger()
|
|
59
|
+
recordReadFailure(l, 'connect timeout', t0)
|
|
60
|
+
recordReadFailure(l, 'connect timeout', t0 + 60_000)
|
|
61
|
+
recordRecovered(l, 2, 120_000, t0 + 120_000)
|
|
62
|
+
assert.equal(l.readFailure, undefined, '지금은 닿는다')
|
|
63
|
+
assert.deepEqual(l.lastRecovery, { atMs: t0 + 120_000, afterFailures: 2, downMs: 120_000 })
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('★ 그냥 지우면 끊긴 적이 있었다는 사실이 사라진다 — 그래서 두 함수를 나눈다', () => {
|
|
67
|
+
const l = newIngestLedger()
|
|
68
|
+
recordReadFailure(l, 'connect timeout', t0)
|
|
69
|
+
clearReadFailure(l)
|
|
70
|
+
assert.equal(l.readFailure, undefined)
|
|
71
|
+
assert.equal(l.lastRecovery, undefined, '지우기만 하면 남지 않는다')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('★ 다시 끊기면 새 단절이다 — 지난번 회복 기록이 그것을 가리지 않는다', () => {
|
|
75
|
+
const l = newIngestLedger()
|
|
76
|
+
recordReadFailure(l, 'connect timeout', t0)
|
|
77
|
+
recordRecovered(l, 1, 60_000, t0 + 60_000)
|
|
78
|
+
recordReadFailure(l, '401 unauthorized', t0 + 600_000)
|
|
79
|
+
assert.equal(l.readFailure?.consecutive, 1, '연달아 횟수가 1 부터 다시 센다')
|
|
80
|
+
assert.equal(l.readFailure?.sinceMs, t0 + 600_000)
|
|
81
|
+
assert.equal(l.lastRecovery?.atMs, t0 + 60_000, '지난 회복도 함께 보인다')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('★ 밖으로 내는 모양에 두 사실이 다 있다 — 화면이 옮기지 못하면 아무 도움이 되지 않는다', () => {
|
|
85
|
+
const l = newIngestLedger()
|
|
86
|
+
recordReadFailure(l, 'connect timeout', t0, 'inverters', 480_000)
|
|
87
|
+
const v: any = ingestHealth(l, 'attached')
|
|
88
|
+
assert.equal(v.readFailure.consecutive, 1)
|
|
89
|
+
assert.equal(v.readFailure.reason, 'connect timeout')
|
|
90
|
+
assert.equal(v.readFailure.stream, 'inverters')
|
|
91
|
+
assert.equal(v.readFailure.nextRetryMs, 480_000)
|
|
92
|
+
assert.equal(typeof v.readFailure.since, 'string')
|
|
93
|
+
|
|
94
|
+
recordRecovered(l, 1, 480_000, t0 + 480_000)
|
|
95
|
+
const w: any = ingestHealth(l, 'attached')
|
|
96
|
+
assert.equal(w.readFailure, undefined)
|
|
97
|
+
assert.equal(w.lastRecovery.at, new Date(t0 + 480_000).toISOString())
|
|
98
|
+
assert.equal(w.lastRecovery.afterFailures, 1)
|
|
99
|
+
assert.equal(w.lastRecovery.downMs, 480_000)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('닿은 적도 끊긴 적도 없으면 두 칸이 다 없다 — 없는 칸을 「끊겼다」로 읽지 않게', () => {
|
|
103
|
+
const v: any = ingestHealth(newIngestLedger(), 'attached')
|
|
104
|
+
assert.equal('readFailure' in v, false)
|
|
105
|
+
assert.equal('lastRecovery' in v, false)
|
|
106
|
+
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* ★ 「원본에 닿지 못한다」와
|
|
2
|
+
* ★ 「원본에 닿지 못한다」와 「연결된 시스템에서 들어온 것이 없다」는 **다른 사실**이다 — 상태가 그것을 가려야 한다.
|
|
3
3
|
*
|
|
4
|
-
* ── 무엇이
|
|
4
|
+
* ── 무엇이 아무 신호도 내지 않았나 (2026-08-23 실측) ───────────────────────────────────────
|
|
5
5
|
* 실 원본이 끊겼다(`jejoup.hatiolab.com:4000` 접속 시간 초과, HTTP 000 / 8초). 그런데 트윈의
|
|
6
6
|
* **조회 가능한 상태 어디에도 그 사실이 없었다.**
|
|
7
7
|
*
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* ── 이 시험이 지키는 것 ─────────────────────────────────────────────────────
|
|
16
16
|
* ① 실패가 상태에 남는다 ② 「몇 번」과 「언제부터」를 함께 든다(한 번의 흔들림과 20분째 단절은 다르다)
|
|
17
|
-
* ③ **빈 읽기는 성공이다** —
|
|
17
|
+
* ③ **빈 읽기는 성공이다** — 들어온 것이 없는 연결을 끊긴 원본으로 만들지 않는다
|
|
18
18
|
* ④ 조회 뷰로 나간다 — 상태에만 있고 안 나가면 화면이 못 본다
|
|
19
19
|
*/
|
|
20
20
|
import { test } from 'node:test'
|
|
@@ -53,7 +53,7 @@ test('한 번의 흔들림과 20분째 단절을 가른다 — 시작 시각을
|
|
|
53
53
|
assert.equal(l.readFailure?.lastAtMs, T0 + 1_200_000, '마지막 실패는 갱신된다')
|
|
54
54
|
})
|
|
55
55
|
|
|
56
|
-
test('빈 읽기는 성공이다 —
|
|
56
|
+
test('빈 읽기는 성공이다 — 들어온 것이 없는 연결을 끊긴 원본으로 만들지 않는다', () => {
|
|
57
57
|
/*
|
|
58
58
|
* ★ 이 단정이 고치려던 것의 **반대 방향으로 틀리는 것**을 막는다. 원본이 「새 것이 없다」고 답한 것은
|
|
59
59
|
* 닿았다는 뜻이다.
|
|
@@ -90,7 +90,7 @@ test('닿고 있으면 그 필드를 만들지 않는다 — 빈 값이 「닿
|
|
|
90
90
|
|
|
91
91
|
test('「유입 없음」과 「닿지 못함」은 다른 판정이다', () => {
|
|
92
92
|
/*
|
|
93
|
-
* 유입 없음은
|
|
93
|
+
* 유입 없음은 연결된 시스템에서 들어온 것이 없는 것일 수 있고, 닿지 못함은 우리가 묻지도 못한 것이다. 두 상태가 같은
|
|
94
94
|
* 값으로 뭉개지면 화면이 두 문장을 다르게 말할 수 없다.
|
|
95
95
|
*/
|
|
96
96
|
const quiet = newIngestLedger()
|
|
@@ -99,7 +99,7 @@ test('「유입 없음」과 「닿지 못함」은 다른 판정이다', () =>
|
|
|
99
99
|
|
|
100
100
|
const vq = ingestHealth(quiet, 'attached', undefined, T0)
|
|
101
101
|
const vd = ingestHealth(dark, 'attached', undefined, T0)
|
|
102
|
-
assert.equal(vq.lastAt, null, '
|
|
102
|
+
assert.equal(vq.lastAt, null, '들어온 것이 없는 쪽도 받은 것이 없다')
|
|
103
103
|
assert.equal(vd.lastAt, null, '끊긴 쪽도 받은 것이 없다 — 그 값만으로는 갈리지 않는다')
|
|
104
104
|
assert.notEqual(Boolean(vq.readFailure), Boolean(vd.readFailure), '이 축이 그 둘을 가른다')
|
|
105
105
|
})
|
|
@@ -117,7 +117,7 @@ test('「유입 없음」과 「닿지 못함」은 다른 판정이다', () =>
|
|
|
117
117
|
* status **연결 설정이 성립하나** — 접속 시험·마스터 동기가 정한다. 재부착의 **자격**이다
|
|
118
118
|
* 읽기 실패 **지금 닿나** — 오갈 수 있는 사실이고 유입 장부가 그 축을 갖는다
|
|
119
119
|
*
|
|
120
|
-
* 「사실을 말하게」 하려던 것이 「사실을 못 듣게」 만들었다. 그 방향의 실패는
|
|
120
|
+
* 「사실을 말하게」 하려던 것이 「사실을 못 듣게」 만들었다. 그 방향의 실패는 들어온 것이 없다 — 화면은
|
|
121
121
|
* 「원본에 닿지 못한다」를 정직하게 말하면서, 정작 다시 닿을 기회를 잃는다.
|
|
122
122
|
*/
|
|
123
123
|
test('읽기 실패가 레퍼런스의 status 를 건드리지 않는다 — 그것이 재부착의 자격이다', () => {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ 저장된 값을 시각으로 읽는 규칙 — 드라이버의 문을 지난다.
|
|
3
|
+
*
|
|
4
|
+
* ── 무엇이 어긋났나 (2026-08-28 실측) ───────────────────────────────────────
|
|
5
|
+
* `getRawOne` 은 드라이버가 준 값을 그대로 준다. sqlite 가 주는 그 값에는 시간대가 없다
|
|
6
|
+
* (`2026-08-27 14:37:20.049`). 그것을 `new Date()` 로 읽으면 서버의 지역 시간으로 해석해 한국에서
|
|
7
|
+
* 9시간이 밀린다. 네 자리에서 났고, 둘은 **지우는 경계**였다(경계가 이르게 밀리면 정리가 0건이 되어
|
|
8
|
+
* 멈춘다).
|
|
9
|
+
*
|
|
10
|
+
* ── 처음에 편법을 썼다 ──────────────────────────────────────────────────────
|
|
11
|
+
* 「시간대가 없으면 UTC 로 읽는다」를 응용 코드에 직접 적었다. 그것은 드라이버가 어떤 문자열로
|
|
12
|
+
* 저장하는지를 우리가 아는 것이고, 다섯 드라이버 규칙에 어긋난다.
|
|
13
|
+
*
|
|
14
|
+
* 표준 문은 `Driver.prepareHydratedValue` 다 — 엔티티 변환이 지나는 그 함수. 이 시험은 컬럼 값을
|
|
15
|
+
* 읽을 때 **우리가 해석하지 않고 그 문에 물어보는지**를 지킨다.
|
|
16
|
+
*/
|
|
17
|
+
import { test } from 'node:test'
|
|
18
|
+
import assert from 'node:assert/strict'
|
|
19
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs'
|
|
20
|
+
import { join } from 'node:path'
|
|
21
|
+
|
|
22
|
+
import { absoluteTime, hydratedDate } from '../server/engine/read-time.js'
|
|
23
|
+
|
|
24
|
+
/** 드라이버를 흉내낸 저장소 — 이 시험이 확인하는 것은 「누가 해석하나」다. */
|
|
25
|
+
const fakeRepository = (opts: { hasColumn?: boolean; hydrate?: (v: unknown) => unknown } = {}) => {
|
|
26
|
+
const calls: unknown[] = []
|
|
27
|
+
const repo = {
|
|
28
|
+
metadata: {
|
|
29
|
+
name: 'TwinEvent',
|
|
30
|
+
findColumnWithPropertyName: (name: string) => (opts.hasColumn === false ? undefined : { propertyName: name, type: Date })
|
|
31
|
+
},
|
|
32
|
+
manager: {
|
|
33
|
+
connection: {
|
|
34
|
+
driver: {
|
|
35
|
+
prepareHydratedValue: (value: unknown) => {
|
|
36
|
+
calls.push(value)
|
|
37
|
+
return opts.hydrate ? opts.hydrate(value) : new Date('2026-08-27T14:37:20.049Z')
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { repo: repo as any, calls }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
test('★ 컬럼 값은 우리가 해석하지 않고 드라이버에게 물어본다', () => {
|
|
47
|
+
const { repo, calls } = fakeRepository()
|
|
48
|
+
const at = hydratedDate(repo, 'createdAt', '2026-08-27 14:37:20.049')
|
|
49
|
+
assert.equal(at?.toISOString(), '2026-08-27T14:37:20.049Z')
|
|
50
|
+
assert.deepEqual(calls, ['2026-08-27 14:37:20.049'], '드라이버의 문을 그 값으로 한 번 지나야 한다')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('빈 값은 드라이버를 부르지 않고 null — 없는 것은 없는 것이다', () => {
|
|
54
|
+
const { repo, calls } = fakeRepository()
|
|
55
|
+
assert.equal(hydratedDate(repo, 'createdAt', null), null)
|
|
56
|
+
assert.equal(hydratedDate(repo, 'createdAt', undefined), null)
|
|
57
|
+
assert.deepEqual(calls, [])
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('드라이버가 날짜를 내주지 못하면 null — 「지금」으로 메우지 않는다', () => {
|
|
61
|
+
const { repo } = fakeRepository({ hydrate: () => 'still a string' })
|
|
62
|
+
assert.equal(hydratedDate(repo, 'createdAt', 'x'), null)
|
|
63
|
+
const bad = fakeRepository({ hydrate: () => new Date('nope') })
|
|
64
|
+
assert.equal(hydratedDate(bad.repo, 'createdAt', 'x'), null)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('★ 컬럼 이름을 못 찾으면 명시 실패한다 — 이름이 바뀐 것을 값으로 덮지 않는다', () => {
|
|
68
|
+
const { repo } = fakeRepository({ hasColumn: false })
|
|
69
|
+
assert.throws(() => hydratedDate(repo, 'createdAt', '2026-08-27 14:37:20.049'), /no column for property/)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('★ 계약이 절대시각인 값 — 시간대가 없으면 UTC 다(지역 시간으로 읽을 근거가 없다)', () => {
|
|
73
|
+
assert.equal(absoluteTime('2026-08-27 14:37:20.049')?.toISOString(), '2026-08-27T14:37:20.049Z')
|
|
74
|
+
/* 지역 시간으로 읽었다면 한국에서 9시간이 밀린다. */
|
|
75
|
+
assert.notEqual(absoluteTime('2026-08-27 14:37:20.049')?.toISOString(), '2026-08-27T05:37:20.049Z')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('시간대가 적혀 있으면 그 말을 믿는다', () => {
|
|
79
|
+
assert.equal(absoluteTime('2026-08-27T14:37:20.049Z')?.toISOString(), '2026-08-27T14:37:20.049Z')
|
|
80
|
+
assert.equal(absoluteTime('2026-08-27T23:37:20.049+09:00')?.toISOString(), '2026-08-27T14:37:20.049Z')
|
|
81
|
+
assert.equal(absoluteTime('2026-08-27T09:37:20.049-05:00')?.toISOString(), '2026-08-27T14:37:20.049Z')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('날짜 객체와 밀리초도 받는다', () => {
|
|
85
|
+
const d = new Date('2026-08-27T14:37:20.049Z')
|
|
86
|
+
assert.equal(absoluteTime(d), d)
|
|
87
|
+
assert.equal(absoluteTime(1_756_305_440_049)?.getTime(), 1_756_305_440_049)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('★ 읽을 수 없는 값은 null — 경계를 정하는 자리가 이 함수를 쓴다', () => {
|
|
91
|
+
for (const bad of [null, undefined, '', ' ', '어제', 'not-a-date', {}, [], NaN, new Date('x')]) {
|
|
92
|
+
assert.equal(absoluteTime(bad), null, `${String(bad)} 는 null 이어야 한다`)
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('★ 컬럼 값을 직접 날짜로 만드는 자리가 남아 있지 않다', () => {
|
|
97
|
+
/*
|
|
98
|
+
* 규칙이 있어도 새로 쓰는 코드가 그것을 지나지 않으면 같은 결함이 다시 난다. 그래서 자리를 세어
|
|
99
|
+
* 둔다 — 원시 결과의 컬럼을 `new Date(...)` 로 감싸는 모양을 찾는다.
|
|
100
|
+
*
|
|
101
|
+
* 밀리초 숫자(`new Date(someMs)`)는 시간대가 없으므로 이 그물에 걸리지 않는다.
|
|
102
|
+
*/
|
|
103
|
+
const root = join(import.meta.dirname, '..', 'server')
|
|
104
|
+
const offenders: string[] = []
|
|
105
|
+
const walk = (dir: string) => {
|
|
106
|
+
for (const name of readdirSync(dir)) {
|
|
107
|
+
const p = join(dir, name)
|
|
108
|
+
if (statSync(p).isDirectory()) { walk(p); continue }
|
|
109
|
+
if (!name.endsWith('.ts')) continue
|
|
110
|
+
const text = readFileSync(p, 'utf8')
|
|
111
|
+
for (const m of text.matchAll(/new Date\(\s*[A-Za-z_$][\w$?.]*\.(eventTime|createdAt|updatedAt|at)\b[^)]*\)/g)) {
|
|
112
|
+
offenders.push(`${name}: ${m[0]}`)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
walk(root)
|
|
117
|
+
assert.deepEqual(offenders, [], '컬럼 값을 직접 날짜로 만드는 자리가 있다 — hydratedDate() 를 지나야 한다')
|
|
118
|
+
})
|
|
@@ -59,7 +59,7 @@ test('목과 가상은 대상 없이도 선다 — 그 자체가 사실이다',
|
|
|
59
59
|
test('instance 와 product 를 가른다 — 그 구분이 이 어휘의 요점이다', () => {
|
|
60
60
|
/*
|
|
61
61
|
* 첫 실 연동에서 그 배포만의 성질이 넷 드러났는데(관계 리솔버가 다른 테넌트의 행을 답한다 · 모르는
|
|
62
|
-
* 필터를
|
|
62
|
+
* 필터를 알리지 않고 무시한다 · 지워진 품목의 BOM 이 남아 있다 · 사실상 무한을 뜻하는 표식) **그것이
|
|
63
63
|
* 제품의 성질인지 그 인스턴스의 성질인지 아무도 모른다.** 두 번째 배포가 붙는 날 갈린다.
|
|
64
64
|
*
|
|
65
65
|
* 그래서 하나로 확인한 것을 `product` 라 부르면 안 되고, 어휘가 그 구분을 담는다.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ 연결된 시스템이 밀어 주는 사실을 받는 자리 — 규약을 시험이 지킨다.
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 만들었나 (2026-08-26 · 사용자 지시) ──────────────────────────────────
|
|
5
|
+
* 지금 모든 커넥터가 주기적으로 물어봅니다. 그러면 연결된 시스템이 값을 갱신하는 주기보다 자주 묻게
|
|
6
|
+
* 되고, 그쪽 서버에 그만큼 부담을 줍니다. 태양광 발전소에 10초마다 물으면서 10분마다 바뀌는 값을
|
|
7
|
+
* 받아 왔고, 그날 그쪽 서버가 부하로 멈췄습니다.
|
|
8
|
+
*
|
|
9
|
+
* ── 무엇을 지키나 ───────────────────────────────────────────────────────────
|
|
10
|
+
* 응답 코드가 규약입니다. 밀어 주는 쪽은 그 코드로 재시도를 정하므로, 우리가 아무렇게나 주면 고칠 수
|
|
11
|
+
* 없는 것을 영원히 다시 보내거나, 잠깐의 문제로 사라진 사실을 아무도 모릅니다.
|
|
12
|
+
*/
|
|
13
|
+
import { test } from 'node:test'
|
|
14
|
+
import assert from 'node:assert/strict'
|
|
15
|
+
import { HOOK_STATUS, secretFromHeaders, secretMatches, producedInstance } from '../server/service/reference/hook-contract.js'
|
|
16
|
+
|
|
17
|
+
test('★ 비밀값을 두 가지 방식으로 받는다 — 밀어 주는 쪽의 관습이 갈린다', () => {
|
|
18
|
+
assert.equal(secretFromHeaders({ 'x-twin-hook-secret': 'abc' }), 'abc')
|
|
19
|
+
assert.equal(secretFromHeaders({ authorization: 'Bearer abc' }), 'abc')
|
|
20
|
+
assert.equal(secretFromHeaders({ authorization: 'bearer abc ' }), 'abc')
|
|
21
|
+
assert.equal(secretFromHeaders({}), '')
|
|
22
|
+
assert.equal(secretFromHeaders(undefined), '')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('★ 전용 헤더가 먼저다 — 둘 다 오면 헷갈리지 않는다', () => {
|
|
26
|
+
assert.equal(secretFromHeaders({ 'x-twin-hook-secret': 'first', authorization: 'Bearer second' }), 'first')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('★ 빈 비밀값은 어떤 것과도 맞지 않는다 — 선언하지 않은 연결은 훅을 받지 않는다', () => {
|
|
30
|
+
assert.equal(secretMatches('', ''), false)
|
|
31
|
+
assert.equal(secretMatches('anything', ''), false)
|
|
32
|
+
assert.equal(secretMatches('', 'expected'), false)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('★ 비밀값 비교는 길이가 같으면 전부를 본다 — 응답 시간으로 알아내지 못하게', () => {
|
|
36
|
+
assert.equal(secretMatches('secret-value', 'secret-value'), true)
|
|
37
|
+
assert.equal(secretMatches('secret-valuX', 'secret-value'), false)
|
|
38
|
+
assert.equal(secretMatches('Xecret-value', 'secret-value'), false)
|
|
39
|
+
assert.equal(secretMatches('short', 'secret-value'), false)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('★ 그 연결이 만든 트윈만 받는다 — 남의 트윈에 밀어 넣지 못한다', () => {
|
|
43
|
+
const ref = {
|
|
44
|
+
scopeSpec: {
|
|
45
|
+
produced: [
|
|
46
|
+
{ siteId: 'site-a', instanceId: 'twin-a' },
|
|
47
|
+
{ siteId: 'site-b', instanceId: 'twin-b' }
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
assert.deepEqual(producedInstance(ref, 'twin-b'), { siteId: 'site-b' })
|
|
52
|
+
assert.equal(producedInstance(ref, 'twin-c'), undefined)
|
|
53
|
+
assert.equal(producedInstance(null, 'twin-a'), undefined)
|
|
54
|
+
assert.equal(producedInstance({}, 'twin-a'), undefined)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('★ 응답 코드가 재시도로 풀리는 것과 그렇지 않은 것을 가른다', () => {
|
|
58
|
+
/*
|
|
59
|
+
* 재시도해도 같은 것: 비밀값이 다르다 · 모르는 대상 · 옮길 수 없는 본문 · 커넥터가 훅을 모른다
|
|
60
|
+
* 사람이 손대야 하는 것: 트윈이 돌지 않는다
|
|
61
|
+
* 재시도하면 될 수 있는 것: 우리 쪽 오류
|
|
62
|
+
*/
|
|
63
|
+
assert.equal(HOOK_STATUS.ok, 200)
|
|
64
|
+
assert.equal(HOOK_STATUS.badPayload, 400)
|
|
65
|
+
assert.equal(HOOK_STATUS.badSecret, 401)
|
|
66
|
+
assert.equal(HOOK_STATUS.unknownTarget, 404)
|
|
67
|
+
assert.equal(HOOK_STATUS.notLive, 409)
|
|
68
|
+
assert.equal(HOOK_STATUS.failed, 500)
|
|
69
|
+
assert.equal(HOOK_STATUS.unsupported, 501)
|
|
70
|
+
|
|
71
|
+
/* 재시도해도 같은 것에 5xx 를 주면 안 된다 — 그쪽이 영원히 다시 보낸다. */
|
|
72
|
+
for (const code of [HOOK_STATUS.badPayload, HOOK_STATUS.badSecret, HOOK_STATUS.unknownTarget, HOOK_STATUS.notLive]) {
|
|
73
|
+
assert.ok(code < 500, `${code} 는 4xx 여야 한다`)
|
|
74
|
+
}
|
|
75
|
+
/* 우리 쪽 문제에는 5xx 를 준다 — 그쪽이 다시 보내야 그 사실이 살아남는다. */
|
|
76
|
+
assert.ok(HOOK_STATUS.failed >= 500)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('★ 트윈이 돌지 않으면 200 을 주지 않는다 — 그쪽이 그 사실을 버린다', () => {
|
|
80
|
+
/*
|
|
81
|
+
* 200 은 「받아서 반영했다」는 뜻입니다. 트윈이 실시간으로 돌지 않아 한 건도 반영되지 않았는데
|
|
82
|
+
* 200 을 주면, 밀어 주는 쪽은 전달된 줄 알고 그 사실을 버립니다. 다시 보내 주지도 않습니다.
|
|
83
|
+
*/
|
|
84
|
+
assert.notEqual(HOOK_STATUS.notLive, HOOK_STATUS.ok)
|
|
85
|
+
assert.equal(HOOK_STATUS.notLive, 409)
|
|
86
|
+
})
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 그래서 도메인 확인이 자리마다 제각각이었고 한 곳은 아예 없었다.
|
|
7
7
|
*
|
|
8
8
|
* 키를 `(domainId, instanceId)` 로 고쳤지만, **직접 색인이 남아 있으면 같은 부류가 다시 자란다** — 밖에서
|
|
9
|
-
* `instances[id]` 를 쓰면 이제는 항상 `undefined` 가 나와
|
|
9
|
+
* `instances[id]` 를 쓰면 이제는 항상 `undefined` 가 나와 알리지 않고 "안 돌고 있다" 로 읽힌다(더 나쁘다:
|
|
10
10
|
* 오류가 아니라 거짓 사실이 된다). 그래서 접근자(`owns`·`runtime`·`kernel`·`metrics`·`snapshot`)만 쓴다.
|
|
11
11
|
*
|
|
12
12
|
* 이 부류는 이미 한 번 물었다 — `twin-lifecycle-mutation.ts` 주석이 증언한다("과거엔
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ 트윈이 첫날의 이름에 갇혀 있었다 — 바꿀 문이 없었다.
|
|
3
|
+
*
|
|
4
|
+
* 이름은 처음 세울 때 한 번 정해지고(`provision`) 다시 읽기는 건드리지 않는다. 그 자체는 안전하다 —
|
|
5
|
+
* 사람이 붙인 이름을 연결이 덮어쓰면 안 된다. 그런데 그래서 첫날 이름에서 벗어날 길이 없었다.
|
|
6
|
+
*
|
|
7
|
+
* 실제로 그런 일이 났다(2026-08-25 · 태양광 발전소). 커넥터가 처음 아는 이름이 연결된 시스템의
|
|
8
|
+
* 코드뿐이었다.
|
|
9
|
+
*
|
|
10
|
+
* 현장 이름 호현에너지
|
|
11
|
+
* 트윈 이름 ES202307202202 ← 화면의 트윈 목록에 이렇게 보였다
|
|
12
|
+
*
|
|
13
|
+
* 뒤에 사람이 부르는 이름이 어디 있는지 알아내 커넥터를 고쳤는데, 트윈 이름은 그대로였다. 남은
|
|
14
|
+
* 유일한 길이 트윈을 지우고 다시 세우는 것이었고, 그러면 그날 실제 발전소에서 받은 246건이
|
|
15
|
+
* 사라진다 — **이름 하나 때문에 사실을 버리는 선택**이다.
|
|
16
|
+
*
|
|
17
|
+
* 연결된 시스템을 따라가는 트윈에서는 이름이 나중에 좋아진다. 커넥터가 나아질 때마다 나는 일이다.
|
|
18
|
+
*
|
|
19
|
+
* 이 테스트는 소스를 전수 조회해 규율을 지킨다(실제 뮤테이션은 DB 와 도메인 맥락이 필요하다).
|
|
20
|
+
*/
|
|
21
|
+
import { test } from 'node:test'
|
|
22
|
+
import assert from 'node:assert/strict'
|
|
23
|
+
import { readFileSync } from 'node:fs'
|
|
24
|
+
import { join } from 'node:path'
|
|
25
|
+
|
|
26
|
+
const FILE = join(import.meta.dirname, '..', 'server', 'service', 'twin-lifecycle', 'twin-lifecycle-mutation.ts')
|
|
27
|
+
const src = readFileSync(FILE, 'utf8')
|
|
28
|
+
const body = (() => {
|
|
29
|
+
const at = src.indexOf('async renameTwinInstance')
|
|
30
|
+
assert.ok(at > 0, '이름을 바꾸는 문이 있어야 한다')
|
|
31
|
+
return src.slice(at, src.indexOf('async deleteTwinInstance'))
|
|
32
|
+
})()
|
|
33
|
+
|
|
34
|
+
test('★ 이름을 바꾸는 문이 있다 — 없어서 지우고 다시 세우는 것이 유일한 길이었다', () => {
|
|
35
|
+
assert.ok(/@Mutation/.test(src.slice(0, src.indexOf('async renameTwinInstance'))))
|
|
36
|
+
assert.ok(/@Arg\('instanceId'\)/.test(body))
|
|
37
|
+
assert.ok(/@Arg\('name'\)/.test(body))
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('★ 빈 이름은 받지 않는다 — 지우는 것과 바꾸는 것은 다른 일이다', () => {
|
|
41
|
+
assert.ok(/\.trim\(\)/.test(body), '앞뒤 공백은 이름이 아니다')
|
|
42
|
+
assert.ok(/if \(!next\) throw/.test(body), '빈 값이면 거절한다')
|
|
43
|
+
/* 빈 이름을 받아 두면 그 트윈은 목록에서 식별자만 남는데, 그것을 「바꿨다」로 보고하면 거짓이다. */
|
|
44
|
+
assert.ok(/left as it was/.test(body), '무엇도 바뀌지 않았다는 것을 말한다')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('★ 남의 도메인 트윈은 바꾸지 못한다 — 테넌트 격리', () => {
|
|
48
|
+
assert.ok(/where: \{ domain: \{ id: domainId \}, instanceId \}/.test(body), '도메인으로 좁혀 찾는다')
|
|
49
|
+
assert.ok(/unknown twin/.test(body), '없으면 이유를 말한다')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('바꾸지 않은 경우를 바꿨다고 말하지 않는다', () => {
|
|
53
|
+
assert.ok(/changed: false/.test(body), '같은 이름이면 changed 를 false 로 낸다')
|
|
54
|
+
assert.ok(/previous/.test(body), '무엇에서 무엇으로 바뀌었는지 함께 낸다')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('바꾼 사람을 남긴다 — 이름은 사람이 하는 선언이다', () => {
|
|
58
|
+
assert.ok(/updater: context\.state\.user/.test(body))
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('이름만 바꾼다 — 트윈을 멈추거나 다시 읽지 않는다', () => {
|
|
62
|
+
/*
|
|
63
|
+
* 도는 런타임은 식별자로 동작하고 이름을 쓰지 않는다. 정지·기동을 끼우면 그 사이의 사실이 어디로
|
|
64
|
+
* 갔는지 설명해야 하는데, 이름 때문에 그 값을 치를 이유가 없다.
|
|
65
|
+
*/
|
|
66
|
+
assert.ok(!/TwinEngine\.(stop|start|remove|resync)/.test(body), '런타임을 건드리지 않는다')
|
|
67
|
+
assert.ok(!/adoptStructure|reprovision/.test(body), '구조를 다시 읽지 않는다')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('연결된 시스템이 이름을 덮어쓰지는 않는다 — 다시 읽기는 그대로다', () => {
|
|
71
|
+
/*
|
|
72
|
+
* 「사람이 손대지 않았으면 따라간다」가 옳아 보이지만, 그러려면 「손댔다」는 표시를 새로 만들어야
|
|
73
|
+
* 한다. 이름 하나 때문에 지기에 무거운 빚이므로 사람이 바꾸는 길만 낸다. 그 결정을 여기서 고정한다.
|
|
74
|
+
*
|
|
75
|
+
* 이름을 정하는 자리는 **처음 세울 때 하나**다. 그 수가 늘면 다시 읽기가 이름을 다시 정하기
|
|
76
|
+
* 시작했다는 뜻이고, 그때는 이 결정을 다시 해야 한다.
|
|
77
|
+
*/
|
|
78
|
+
const engine = readFileSync(join(import.meta.dirname, '..', 'server', 'engine', 'twin-engine.ts'), 'utf8')
|
|
79
|
+
const places = engine.match(/name: master\.siteName/g) ?? []
|
|
80
|
+
assert.equal(places.length, 1, `이름을 정하는 자리가 ${places.length}곳이다 — 처음 세울 때 하나여야 한다`)
|
|
81
|
+
})
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* 이름이 바뀌었다.
|
|
9
9
|
*
|
|
10
10
|
* ── 지키는 규율 둘 (계획서 §6) ──────────────────────────────────────────────
|
|
11
|
-
* ① **모르는 값은 오류를 낸다.** 예전에는 미선언이
|
|
11
|
+
* ① **모르는 값은 오류를 낸다.** 예전에는 미선언이 알리지 않고 `sim-experiment`(=저널 초기화)로 떨어졌다 — 값이
|
|
12
12
|
* 하나 어긋나는 순간 이력을 지우는 길이다. 기본값을 없앴다.
|
|
13
13
|
* ② **옛 어휘를 번역하지 않는다.** 번역을 두면 옛 값이 살아남고 다시 저장된다. 저장된 값은 한 번
|
|
14
14
|
* 옮기고(데이터 이관), 코드는 새 어휘만 안다 — 다만 무엇으로 옮겨야 하는지는 오류 문장이 말한다.
|
|
@@ -67,7 +67,7 @@ test('묻는 자리도 있다 — 화면·검사는 오류를 내지 않고 판
|
|
|
67
67
|
/*
|
|
68
68
|
* ── 옛 어휘가 코드로 되돌아오지 않게 (계획서 §6 의 검사) ────────────────────
|
|
69
69
|
* ADR-0034 의 `local-declarations.test.ts` 가 `site*` 에 대해 하는 것과 같은 방식이다. 개명은 저장값·타입·
|
|
70
|
-
* 화면에 걸쳐 있어서, 한 곳만 되돌아와도 그 자리가
|
|
70
|
+
* 화면에 걸쳐 있어서, 한 곳만 되돌아와도 그 자리가 알리지 않고 다르게 재기동한다.
|
|
71
71
|
*/
|
|
72
72
|
const OLD_TOKENS = [/\brealityMode\b/, /\bRealityMode\b/, /'sim-world'/, /'sim-experiment'/, /DEFAULT_REALITY_MODE/]
|
|
73
73
|
|
|
@@ -106,6 +106,6 @@ test('기본값 상수가 없다 — 미선언을 우리가 고르지 않는다'
|
|
|
106
106
|
.split('\n')
|
|
107
107
|
.filter(line => !/^\s*(\*|\/\/|\/\*)/.test(line))
|
|
108
108
|
.join('\n')
|
|
109
|
-
assert.equal(/DEFAULT_RESTART_POLICY/.test(code), false, '기본값 상수가 되살아나면 미선언이 다시
|
|
109
|
+
assert.equal(/DEFAULT_RESTART_POLICY/.test(code), false, '기본값 상수가 되살아나면 미선언이 다시 들어온 것이 없어진다')
|
|
110
110
|
assert.match(code, /readRestartPolicy\(/, '읽는 자리는 엄격한 리더를 지난다')
|
|
111
111
|
})
|
|
@@ -35,14 +35,14 @@ test('기억이 없으면 그렇게 말한다 — 첫 사이트로 때우지 않
|
|
|
35
35
|
test('기억한 사이트가 사라졌으면 다른 사이트로 덮지 않는다', () => {
|
|
36
36
|
const { site, reason } = pickOriginSite(sites, { siteId: 'plant-9' })
|
|
37
37
|
assert.equal(site, undefined)
|
|
38
|
-
assert.equal(reason, 'site-gone', '다른 현장의 구조를 이 트윈에 덮는 것은
|
|
38
|
+
assert.equal(reason, 'site-gone', '다른 현장의 구조를 이 트윈에 덮는 것은 오류 없이 틀린 공장이 된다')
|
|
39
39
|
})
|
|
40
40
|
|
|
41
41
|
test('빈 문자열은 기억이 아니다', () => {
|
|
42
42
|
assert.equal(pickOriginSite(sites, { siteId: ' ' }).reason, 'no-remembered-site')
|
|
43
43
|
})
|
|
44
44
|
|
|
45
|
-
/* 배선이 사라지면
|
|
45
|
+
/* 배선이 사라지면 알리지 않고 예전으로 돌아간다 — 소스 모양으로 못 박는다(리졸버는 DB·어댑터를 물고 있다). */
|
|
46
46
|
test('다시 읽기는 인스턴스 정체로 견주고, 가져오기는 사이트 정체를 남긴다', () => {
|
|
47
47
|
const src = readFileSync(fileURLToPath(new URL('../server/service/reference/reference-resolver.ts', import.meta.url)), 'utf-8')
|
|
48
48
|
/* 옛 비교식은 **설명문에 남아 있어야 한다**(왜 고쳤는지가 사라지면 되돌아간다) — 그래서 코드 줄만 본다. */
|
|
@@ -60,7 +60,7 @@ test('다시 읽기는 인스턴스 정체로 견주고, 가져오기는 사이
|
|
|
60
60
|
/*
|
|
61
61
|
* ── 다시 읽기가 **현장 소속**을 지킨다 (2026-08-18 실측) ─────────────────────
|
|
62
62
|
* 한 현장에 합쳐 둔 EMS 트윈 여덟을 재동기했더니 각자 공간으로 흩어졌다 — 템플릿이 낸 기본 공간으로
|
|
63
|
-
* 돌아갔기 때문이다.
|
|
63
|
+
* 돌아갔기 때문이다. 들어온 것이 없다: 오류도 경고도 없고, 트윈은 멀쩡히 돌아간다. 그런데 그 순간 **그 현장의
|
|
64
64
|
* 값(요금·통화)을 함께 잃는다** — 현장 겹은 소속으로 얹히기 때문이다.
|
|
65
65
|
*
|
|
66
66
|
* 원인은 origin 이 「어느 현장에 합쳤나」를 기억하지 않은 것이다(구조만 기억했다).
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* ── 왜 알려 주어야 하나 ─────────────────────────────────────────────────────
|
|
18
18
|
* 커널의 계약이 그 이유를 적어 두었다(`resumeRevision`): 「저널이 비어 있지 않은데 0부터 다시 세면 같은
|
|
19
19
|
* 트윈에 같은 리비전이 둘 생기고, 재생 순서가 뒤섞이며, 시간여행이 엉뚱한 시점을 답한다 — 오류는 안 난다.」
|
|
20
|
-
*
|
|
20
|
+
* 알리지 않고 깨지는 종류이므로 시험이 지킨다.
|
|
21
21
|
*
|
|
22
22
|
* ── 이 시험이 무엇을 재는가 ─────────────────────────────────────────────────
|
|
23
23
|
* DB 도 커널도 띄우지 않는다. 이 규율은 **어느 경로가 무엇을 부르나**에 있고 그것은 소스에서 확인된다.
|
|
@@ -33,7 +33,7 @@ const src = readFileSync(ENGINE, 'utf8')
|
|
|
33
33
|
/**
|
|
34
34
|
* 그 함수 본문만 — 다른 경로의 호출이 섞여 판정이 흐려지지 않게.
|
|
35
35
|
*
|
|
36
|
-
* 끝은 **다음 멤버 선언**으로 자른다. 글자 수로 자르면 주석이 늘어난 날
|
|
36
|
+
* 끝은 **다음 멤버 선언**으로 자른다. 글자 수로 자르면 주석이 늘어난 날 알리지 않고 짧아져서, 있는 것을
|
|
37
37
|
* 없다고 말한다(거짓 빨강을 내는 가드는 없는 것보다 나쁘다).
|
|
38
38
|
*/
|
|
39
39
|
function bodyOf(signature: string): string {
|
|
@@ -49,8 +49,14 @@ function bodyOf(signature: string): string {
|
|
|
49
49
|
|
|
50
50
|
test('미러를 세우는 경로도 커널에 저널 머리를 알려 준다', () => {
|
|
51
51
|
const body = bodyOf('static startLive(')
|
|
52
|
-
/*
|
|
53
|
-
|
|
52
|
+
/*
|
|
53
|
+
* 저널 머리를 읽는다 — 이 조회는 예전부터 있었다(inst.revision 시드용).
|
|
54
|
+
*
|
|
55
|
+
* 2026-08-28: 모양을 `findOne(order: revision DESC)` 에서 `MAX(e.revision)` 으로 바꿨다. 앞의 것은
|
|
56
|
+
* 관계로 물어 두 단계 질의가 되고 안쪽에 상한이 없어 그 트윈의 저널을 전부 만든 뒤 한 줄을 고른다.
|
|
57
|
+
* **묻는 것은 같다** — 저널 머리다. 그래서 이 단정은 「머리를 읽는가」로 남고 모양만 따라간다.
|
|
58
|
+
*/
|
|
59
|
+
assert.match(body, /select\('MAX\(e\.revision\)'/, '저널 머리를 읽어야 한다')
|
|
54
60
|
/* 그리고 **커널에도** 알려 준다 — 이 한 줄이 없어서 두 수가 갈렸다. */
|
|
55
61
|
assert.match(body, /resumeRevision\(head\)/, '커널에도 그 번호를 알려 준다')
|
|
56
62
|
})
|
|
@@ -72,7 +78,7 @@ test('저널이 비어 있으면 알려 주지 않는다 — 새 트윈은 0 부
|
|
|
72
78
|
test('뒤로 되돌리려는 거절을 삼키지 않는다', () => {
|
|
73
79
|
/*
|
|
74
80
|
* 조회가 비동기라 그 사이에 이벤트가 들어와 있을 수 있다. 그때 커널은 뒤로 가기를 거절한다(겹치는
|
|
75
|
-
* 번호를 막는 그 판정이다). 그 거절을
|
|
81
|
+
* 번호를 막는 그 판정이다). 그 거절을 알리지 않고 버리면 「왜 두 수가 아직 다르지」를 답할 계기가 없다.
|
|
76
82
|
*/
|
|
77
83
|
const body = bodyOf('static startLive(')
|
|
78
84
|
assert.match(body, /catch \(err[^)]*\)[\s\S]{0,200}twinWarn/, '거절을 로그에 남긴다')
|
|
@@ -81,7 +87,7 @@ test('뒤로 되돌리려는 거절을 삼키지 않는다', () => {
|
|
|
81
87
|
|
|
82
88
|
test('체크포인트는 두 수를 나란히 적는다 — 그 사실 자체는 옳다', () => {
|
|
83
89
|
/*
|
|
84
|
-
* 바깥은 저널 줄 번호(이어 쓸 자리), 안쪽은 커널이
|
|
90
|
+
* 바깥은 저널 줄 번호(이어 쓸 자리), 안쪽은 커널이 계산한 봉투 수다. **둘을 하나로 합치지 않는다** —
|
|
85
91
|
* 합치면 「어디까지 적었나」와 「어디까지 반영했나」를 구별할 수 없고, 그 구별이 미러의 정직성이다.
|
|
86
92
|
* 두 경로가 대칭이면 값이 같아지지만, **자리는 둘이어야 한다.**
|
|
87
93
|
*/
|
|
@@ -89,7 +95,7 @@ test('체크포인트는 두 수를 나란히 적는다 — 그 사실 자체는
|
|
|
89
95
|
assert.ok(at > 0, 'persistSnapshot 이 있어야 한다')
|
|
90
96
|
/*
|
|
91
97
|
* 창을 넓혀 왔다 — 이 함수 앞쪽에 「빈 상태로 좋은 스냅샷을 덮지 않는다」 가드가 들어오고(§
|
|
92
|
-
* `checkpoint-refuses-empty`), 뒤쪽에 「이어
|
|
98
|
+
* `checkpoint-refuses-empty`), 뒤쪽에 「이어 계산할 씨앗을 함께 적는다」가 들어왔다(§
|
|
93
99
|
* `mirror-resumes-from-checkpoint`). 검사하는 **사실은 그대로**다.
|
|
94
100
|
*
|
|
95
101
|
* 그리고 **줄 모양이 아니라 두 자리를 검사한다**: 예전에는 `{ revision, state` 라는 한 줄 모양을
|
|
@@ -100,5 +106,5 @@ test('체크포인트는 두 수를 나란히 적는다 — 그 사실 자체는
|
|
|
100
106
|
assert.match(body, /inst\.revision \?\? state\.revision/, '바깥은 저널 줄 번호를 먼저 쓴다')
|
|
101
107
|
const call = body.slice(body.indexOf('setInCache'))
|
|
102
108
|
assert.match(call, /revision/, '저널 줄 번호를 싣는다')
|
|
103
|
-
assert.match(call, /state/, '커널이
|
|
109
|
+
assert.match(call, /state/, '커널이 계산한 상태를 함께 싣는다 — 두 자리를 하나로 합치지 않는다')
|
|
104
110
|
})
|