@things-factory/headless-twin 10.0.9 → 10.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-server/engine/canonical-ingest.d.ts +2 -2
- package/dist-server/engine/canonical-ingest.js +24 -2
- package/dist-server/engine/canonical-ingest.js.map +1 -1
- package/dist-server/engine/command-routing.js +1 -1
- package/dist-server/engine/command-routing.js.map +1 -1
- package/dist-server/engine/energy-topology.d.ts +86 -0
- package/dist-server/engine/energy-topology.js +144 -0
- package/dist-server/engine/energy-topology.js.map +1 -0
- package/dist-server/engine/index.d.ts +8 -3
- package/dist-server/engine/index.js +10 -3
- package/dist-server/engine/index.js.map +1 -1
- package/dist-server/engine/kpi-baseline.d.ts +1 -1
- package/dist-server/engine/kpi-baseline.js.map +1 -1
- package/dist-server/engine/kpi-fold.d.ts +137 -1
- package/dist-server/engine/kpi-fold.js +225 -0
- package/dist-server/engine/kpi-fold.js.map +1 -1
- package/dist-server/engine/kpi-query.d.ts +1 -1
- package/dist-server/engine/kpi-query.js +212 -18
- package/dist-server/engine/kpi-query.js.map +1 -1
- package/dist-server/engine/kpi-target.d.ts +17 -0
- package/dist-server/engine/kpi-target.js +24 -2
- package/dist-server/engine/kpi-target.js.map +1 -1
- package/dist-server/engine/live-feed-registry.d.ts +25 -0
- package/dist-server/engine/live-feed-registry.js +51 -0
- package/dist-server/engine/live-feed-registry.js.map +1 -0
- package/dist-server/engine/load-meter.d.ts +181 -0
- package/dist-server/engine/load-meter.js +267 -0
- package/dist-server/engine/load-meter.js.map +1 -0
- package/dist-server/engine/local-declarations.d.ts +262 -0
- package/dist-server/engine/local-declarations.js +528 -0
- package/dist-server/engine/local-declarations.js.map +1 -0
- package/dist-server/engine/model-basis.d.ts +39 -6
- package/dist-server/engine/model-basis.js +69 -9
- package/dist-server/engine/model-basis.js.map +1 -1
- package/dist-server/engine/{board-vocabulary.d.ts → model-vocabulary.d.ts} +4 -4
- package/dist-server/engine/{board-vocabulary.js → model-vocabulary.js} +37 -19
- package/dist-server/engine/model-vocabulary.js.map +1 -0
- package/dist-server/engine/oee-accumulator.d.ts +28 -0
- package/dist-server/engine/oee-accumulator.js +26 -1
- package/dist-server/engine/oee-accumulator.js.map +1 -1
- package/dist-server/engine/operation-basis.d.ts +32 -0
- package/dist-server/engine/operation-basis.js +87 -0
- package/dist-server/engine/operation-basis.js.map +1 -0
- package/dist-server/engine/property-effects.d.ts +30 -0
- package/dist-server/engine/property-effects.js +192 -0
- package/dist-server/engine/property-effects.js.map +1 -0
- package/dist-server/engine/runtime-key.d.ts +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.js +3 -3
- package/dist-server/engine/spec-coverage.js.map +1 -1
- package/dist-server/engine/state-axes.d.ts +7 -2
- package/dist-server/engine/state-axes.js +8 -3
- package/dist-server/engine/state-axes.js.map +1 -1
- package/dist-server/engine/structure-diff.js +1 -1
- package/dist-server/engine/structure-diff.js.map +1 -1
- package/dist-server/engine/travel-estimator.d.ts +9 -2
- package/dist-server/engine/travel-estimator.js +11 -6
- package/dist-server/engine/travel-estimator.js.map +1 -1
- package/dist-server/engine/twin-engine.d.ts +332 -47
- package/dist-server/engine/twin-engine.js +1314 -279
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/engine/warm-start.d.ts +38 -4
- package/dist-server/engine/warm-start.js +38 -9
- package/dist-server/engine/warm-start.js.map +1 -1
- package/dist-server/index.d.ts +1 -0
- package/dist-server/index.js +17 -11
- package/dist-server/index.js.map +1 -1
- package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.d.ts +5 -0
- package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js +48 -0
- package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js.map +1 -0
- package/dist-server/migrations/index.d.ts +2 -0
- package/dist-server/migrations/index.js +10 -0
- package/dist-server/migrations/index.js.map +1 -0
- package/dist-server/service/index.d.ts +3 -2
- package/dist-server/service/index.js +25 -17
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/reference/discovery-result.d.ts +1 -1
- package/dist-server/service/reference/discovery-result.js +2 -2
- package/dist-server/service/reference/discovery-result.js.map +1 -1
- package/dist-server/service/reference/ingest-space.d.ts +24 -1
- package/dist-server/service/reference/ingest-space.js +17 -1
- package/dist-server/service/reference/ingest-space.js.map +1 -1
- package/dist-server/service/reference/reference-live.d.ts +23 -0
- package/dist-server/service/reference/reference-live.js +121 -4
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-master.d.ts +194 -13
- package/dist-server/service/reference/reference-master.js +159 -28
- package/dist-server/service/reference/reference-master.js.map +1 -1
- package/dist-server/service/reference/reference-resolver.d.ts +31 -1
- package/dist-server/service/reference/reference-resolver.js +223 -12
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/reference/template-registry.d.ts +19 -3
- package/dist-server/service/reference/template-registry.js.map +1 -1
- package/dist-server/service/twin-audit/command-audit.d.ts +37 -0
- package/dist-server/service/twin-audit/command-audit.js +53 -0
- package/dist-server/service/twin-audit/command-audit.js.map +1 -0
- package/dist-server/service/twin-audit/index.d.ts +4 -0
- package/dist-server/service/twin-audit/index.js +8 -0
- package/dist-server/service/twin-audit/index.js.map +1 -0
- package/dist-server/service/twin-audit/twin-audit-event.d.ts +24 -0
- package/dist-server/service/twin-audit/twin-audit-event.js +125 -0
- package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -0
- package/dist-server/service/twin-audit/twin-audit-query.d.ts +4 -0
- package/dist-server/service/twin-audit/twin-audit-query.js +76 -0
- package/dist-server/service/twin-audit/twin-audit-query.js.map +1 -0
- package/dist-server/service/twin-control/twin-control-mutation.js +21 -1
- package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
- package/dist-server/service/twin-event/twin-event-keys.d.ts +1 -1
- package/dist-server/service/twin-event/twin-event-keys.js +1 -1
- package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
- package/dist-server/service/twin-event/twin-event.d.ts +14 -5
- package/dist-server/service/twin-event/twin-event.js +46 -14
- package/dist-server/service/twin-event/twin-event.js.map +1 -1
- package/dist-server/service/twin-forecast/forecast-metrics.d.ts +13 -0
- package/dist-server/service/twin-forecast/forecast-metrics.js +57 -0
- package/dist-server/service/twin-forecast/forecast-metrics.js.map +1 -0
- package/dist-server/service/twin-forecast/forecast-tuning.d.ts +12 -0
- package/dist-server/service/twin-forecast/forecast-tuning.js +44 -0
- package/dist-server/service/twin-forecast/forecast-tuning.js.map +1 -0
- package/dist-server/service/twin-forecast/gap-analytics.d.ts +28 -2
- package/dist-server/service/twin-forecast/gap-analytics.js +43 -17
- package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
- package/dist-server/service/twin-forecast/twin-forecast-query.js +217 -38
- package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
- package/dist-server/service/twin-instance/twin-instance.d.ts +38 -5
- package/dist-server/service/twin-instance/twin-instance.js +66 -20
- package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
- package/dist-server/service/twin-journal/twin-journal-query.d.ts +5 -2
- package/dist-server/service/twin-journal/twin-journal-query.js +71 -9
- package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +3 -5
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +24 -27
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
- package/dist-server/service/twin-metrics/twin-metrics-query.d.ts +1 -0
- package/dist-server/service/twin-metrics/twin-metrics-query.js +62 -0
- package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
- package/dist-server/service/twin-model/epcis-coverage.d.ts +14 -0
- package/dist-server/service/twin-model/epcis-coverage.js +174 -0
- package/dist-server/service/twin-model/epcis-coverage.js.map +1 -0
- package/dist-server/service/twin-model/iec61850-coverage.d.ts +14 -0
- package/dist-server/service/twin-model/iec61850-coverage.js +98 -0
- package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -0
- package/dist-server/service/twin-model/index.d.ts +13 -0
- package/dist-server/service/twin-model/index.js +29 -0
- package/dist-server/service/twin-model/index.js.map +1 -0
- package/dist-server/service/twin-model/isa95-coverage.d.ts +48 -0
- package/dist-server/service/twin-model/isa95-coverage.js +155 -0
- package/dist-server/service/twin-model/isa95-coverage.js.map +1 -0
- package/dist-server/service/twin-model/project-structure.d.ts +16 -0
- package/dist-server/service/twin-model/project-structure.js +173 -0
- package/dist-server/service/twin-model/project-structure.js.map +1 -0
- package/dist-server/service/twin-model/standard-coverage.d.ts +9 -0
- package/dist-server/service/twin-model/standard-coverage.js +37 -0
- package/dist-server/service/twin-model/standard-coverage.js.map +1 -0
- package/dist-server/service/twin-model/twin-equipment.d.ts +64 -0
- package/dist-server/service/twin-model/twin-equipment.js +134 -0
- package/dist-server/service/twin-model/twin-equipment.js.map +1 -0
- package/dist-server/service/twin-model/twin-lineage-query.d.ts +3 -0
- package/dist-server/service/twin-model/twin-lineage-query.js +206 -0
- package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -0
- package/dist-server/service/twin-model/twin-location.d.ts +58 -0
- package/dist-server/service/twin-model/twin-location.js +130 -0
- package/dist-server/service/twin-model/twin-location.js.map +1 -0
- package/dist-server/service/twin-model/twin-model-item-query.d.ts +141 -0
- package/dist-server/service/twin-model/twin-model-item-query.js +852 -0
- package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -0
- package/dist-server/service/twin-model/twin-model-mutation.d.ts +57 -0
- package/dist-server/service/twin-model/twin-model-mutation.js +490 -0
- package/dist-server/service/twin-model/twin-model-mutation.js.map +1 -0
- package/dist-server/service/twin-model/twin-model-query.d.ts +43 -0
- package/dist-server/service/twin-model/twin-model-query.js +551 -0
- package/dist-server/service/twin-model/twin-model-query.js.map +1 -0
- package/dist-server/service/twin-model/twin-model-tree-query.d.ts +3 -0
- package/dist-server/service/twin-model/twin-model-tree-query.js +158 -0
- package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -0
- package/dist-server/service/twin-model/twin-operation.d.ts +61 -0
- package/dist-server/service/twin-model/twin-operation.js +137 -0
- package/dist-server/service/twin-model/twin-operation.js.map +1 -0
- package/dist-server/service/twin-space/move-space.d.ts +65 -0
- package/dist-server/service/twin-space/move-space.js +166 -0
- package/dist-server/service/twin-space/move-space.js.map +1 -0
- package/dist-server/service/twin-space/space-integrity.d.ts +97 -0
- package/dist-server/service/twin-space/space-integrity.js +182 -0
- package/dist-server/service/twin-space/space-integrity.js.map +1 -0
- package/dist-server/service/twin-space/twin-space-area.js +4 -4
- package/dist-server/service/twin-space/twin-space-area.js.map +1 -1
- package/dist-server/service/twin-space/twin-space-representation.js +6 -6
- package/dist-server/service/twin-space/twin-space-representation.js.map +1 -1
- package/dist-server/service/twin-space/twin-space-resolver.d.ts +63 -1
- package/dist-server/service/twin-space/twin-space-resolver.js +356 -18
- package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
- package/dist-server/service/twin-space/twin-space.d.ts +17 -1
- package/dist-server/service/twin-space/twin-space.js +15 -5
- package/dist-server/service/twin-space/twin-space.js.map +1 -1
- package/dist-server/service/twin-structure/twin-structure.d.ts +1 -1
- package/dist-server/service/twin-structure/twin-structure.js +8 -8
- package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
- package/dist-shared/axis-read.d.ts +12 -0
- package/dist-shared/axis-read.js +32 -0
- package/dist-shared/axis-read.js.map +1 -0
- package/{dist-server/engine → dist-shared}/entity-delta.d.ts +3 -0
- package/{dist-server/engine → dist-shared}/entity-delta.js +102 -2
- package/dist-shared/entity-delta.js.map +1 -0
- package/dist-shared/kpi-broadcast.js.map +1 -0
- package/dist-shared/twin-level.js.map +1 -0
- package/package.json +14 -12
- package/server/engine/canonical-ingest.ts +36 -4
- package/server/engine/command-routing.ts +1 -1
- package/server/engine/energy-topology.ts +189 -0
- package/server/engine/index.ts +10 -3
- package/server/engine/kpi-baseline.ts +1 -1
- package/server/engine/kpi-fold.ts +389 -1
- package/server/engine/kpi-query.ts +214 -19
- package/server/engine/kpi-target.ts +24 -2
- package/server/engine/live-feed-registry.ts +58 -0
- package/server/engine/load-meter.ts +384 -0
- package/server/engine/local-declarations.ts +700 -0
- package/server/engine/model-basis.ts +78 -10
- package/server/engine/{board-vocabulary.ts → model-vocabulary.ts} +36 -15
- package/server/engine/oee-accumulator.ts +34 -1
- package/server/engine/operation-basis.ts +100 -0
- package/server/engine/property-effects.ts +199 -0
- package/server/engine/runtime-key.ts +1 -1
- package/server/engine/spec-coverage.ts +3 -3
- package/server/engine/state-axes.ts +8 -3
- package/server/engine/structure-diff.ts +1 -1
- package/server/engine/travel-estimator.ts +11 -6
- package/server/engine/twin-engine.ts +1414 -274
- package/server/engine/warm-start.ts +83 -12
- package/server/index.ts +13 -10
- package/server/migrations/1786000000000-RenameTwinInstanceBoardToModel.ts +43 -0
- package/server/migrations/index.ts +7 -0
- package/server/service/index.ts +9 -1
- package/server/service/reference/discovery-result.ts +2 -2
- package/server/service/reference/ingest-space.ts +35 -1
- package/server/service/reference/reference-live.ts +119 -4
- package/server/service/reference/reference-master.ts +276 -35
- package/server/service/reference/reference-resolver.ts +232 -12
- package/server/service/reference/template-registry.ts +20 -5
- package/server/service/twin-audit/command-audit.ts +81 -0
- package/server/service/twin-audit/index.ts +5 -0
- package/server/service/twin-audit/twin-audit-event.ts +112 -0
- package/server/service/twin-audit/twin-audit-query.ts +72 -0
- package/server/service/twin-control/twin-control-mutation.ts +22 -1
- package/server/service/twin-event/twin-event-keys.ts +1 -1
- package/server/service/twin-event/twin-event.ts +52 -15
- package/server/service/twin-forecast/forecast-metrics.ts +60 -0
- package/server/service/twin-forecast/forecast-tuning.ts +40 -0
- package/server/service/twin-forecast/gap-analytics.ts +48 -9
- package/server/service/twin-forecast/twin-forecast-query.ts +208 -36
- package/server/service/twin-instance/twin-instance.ts +126 -25
- package/server/service/twin-journal/twin-journal-query.ts +74 -10
- package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +23 -22
- package/server/service/twin-metrics/twin-metrics-query.ts +59 -2
- package/server/service/twin-model/epcis-coverage.ts +195 -0
- package/server/service/twin-model/iec61850-coverage.ts +117 -0
- package/server/service/twin-model/index.ts +25 -0
- package/server/service/twin-model/isa95-coverage.ts +190 -0
- package/server/service/twin-model/project-structure.ts +199 -0
- package/server/service/twin-model/standard-coverage.ts +35 -0
- package/server/service/twin-model/twin-equipment.ts +150 -0
- package/server/service/twin-model/twin-lineage-query.ts +193 -0
- package/server/service/twin-model/twin-location.ts +140 -0
- package/server/service/twin-model/twin-model-item-query.ts +848 -0
- package/server/service/twin-model/twin-model-mutation.ts +503 -0
- package/server/service/twin-model/twin-model-query.ts +537 -0
- package/server/service/twin-model/twin-model-tree-query.ts +180 -0
- package/server/service/twin-model/twin-operation.ts +150 -0
- package/server/service/twin-space/move-space.ts +262 -0
- package/server/service/twin-space/space-integrity.ts +293 -0
- package/server/service/twin-space/twin-space-area.ts +4 -4
- package/server/service/twin-space/twin-space-representation.ts +6 -6
- package/server/service/twin-space/twin-space-resolver.ts +355 -21
- package/server/service/twin-space/twin-space.ts +40 -6
- package/server/service/twin-structure/twin-structure.ts +10 -10
- package/shared/axis-read.ts +28 -0
- package/{server/engine → shared}/entity-delta.ts +96 -1
- package/test/adopt-structure-live.test.ts +133 -0
- package/test/attention-digest.test.ts +3 -3
- package/test/axis-read.test.ts +87 -0
- package/test/boot-resume.test.ts +205 -0
- package/test/canonical-ingest-vocabularies.test.ts +118 -0
- package/test/capability-mapping.test.ts +1 -1
- package/test/command-routing.test.ts +1 -1
- package/test/declaration-reaches-model.test.ts +178 -0
- package/test/discovery-result.test.ts +1 -1
- package/test/energy-topology.test.ts +113 -0
- package/test/entity-delta.test.ts +171 -1
- package/test/event-time-column.test.ts +67 -0
- package/test/forecast-metrics.test.ts +71 -0
- package/test/forecast-tuning.test.ts +42 -0
- package/test/gap-analytics.test.ts +34 -7
- package/test/ingest-bench.test.ts +6 -6
- package/test/ingest-running-guard.test.ts +136 -0
- package/test/ingest-space.test.ts +29 -1
- package/test/instance-cache-lifecycle.test.ts +110 -0
- package/test/kernel-kind-guard.test.ts +96 -0
- package/test/kpi-baseline-db.test.ts +3 -1
- package/test/kpi-baseline.test.ts +3 -3
- package/test/kpi-fold.test.ts +393 -2
- package/test/kpi-query-bench.test.ts +5 -3
- package/test/lineage-survives-restart.test.ts +203 -0
- package/test/live-feed-registry.test.ts +67 -0
- package/test/live-kernel-facts.test.ts +161 -0
- package/test/live-mirror-parity.test.ts +22 -15
- package/test/load-meter.test.ts +314 -0
- package/test/local-declarations.test.ts +554 -0
- package/test/master-to-twin.test.ts +121 -33
- package/test/model-basis.test.ts +56 -1
- package/test/{board-vocabulary.test.ts → model-vocabulary.test.ts} +25 -25
- package/test/move-space.test.ts +149 -0
- package/test/mutation-gate.test.ts +19 -5
- package/test/oee-accumulator.test.ts +26 -7
- package/test/operation-basis.test.ts +100 -0
- package/test/operations-capability-db.test.ts +165 -0
- package/test/project-structure-db.test.ts +226 -0
- package/test/projection-reaches-screen.test.ts +272 -0
- package/test/property-effects.test.ts +96 -0
- package/test/resync-origin-site.test.ts +77 -0
- package/test/scale-twin-bench.test.ts +5 -5
- package/test/snapshot-freshness.test.ts +60 -0
- package/test/space-integrity.test.ts +223 -0
- package/test/standard-coverage.test.ts +94 -0
- package/test/state-axes.test.ts +1 -1
- package/test/streamline-e2e.test.ts +14 -14
- package/test/structure-revision-db.test.ts +11 -11
- package/test/tenant-registry-db.test.ts +5 -5
- package/test/twin-audit.test.ts +83 -0
- package/test/twin-model-item-db.test.ts +275 -0
- package/test/twin-model-tree-db.test.ts +176 -0
- package/test/twin-origin-resync.test.ts +114 -0
- package/test/warm-start-seam.test.ts +6 -6
- package/test/warm-start.test.ts +88 -3
- package/tsconfig.json +6 -1
- package/tsconfig.shared.json +23 -0
- package/tsconfig.shared.tsbuildinfo +1 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/dist-server/engine/board-vocabulary.js.map +0 -1
- package/dist-server/engine/entity-delta.js.map +0 -1
- package/dist-server/engine/kpi-broadcast.js.map +0 -1
- package/dist-server/engine/twin-level.js.map +0 -1
- package/dist-server/service/twin-event/backfill-keys.d.ts +0 -11
- package/dist-server/service/twin-event/backfill-keys.js +0 -63
- package/dist-server/service/twin-event/backfill-keys.js.map +0 -1
- package/dist-server/tsconfig.tsbuildinfo +0 -1
- package/server/service/twin-event/backfill-keys.ts +0 -72
- /package/{dist-server/engine → dist-shared}/kpi-broadcast.d.ts +0 -0
- /package/{dist-server/engine → dist-shared}/kpi-broadcast.js +0 -0
- /package/{dist-server/engine → dist-shared}/twin-level.d.ts +0 -0
- /package/{dist-server/engine → dist-shared}/twin-level.js +0 -0
- /package/{server/engine → shared}/kpi-broadcast.ts +0 -0
- /package/{server/engine → shared}/twin-level.ts +0 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* eventTime 이 **날짜 컬럼**이라는 사실을 지킨다.
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 있나 (2026-08-08) ────────────────────────────────────────────────────
|
|
5
|
+
* 예전에는 문자열 컬럼이었고, "UTC ISO 만 들어오니 사전식 비교가 맞는다" 는 전제 위에서 정렬·범위
|
|
6
|
+
* 질의가 돌았다. 그 전제는 컬럼 어디에도 적혀 있지 않았다.
|
|
7
|
+
*
|
|
8
|
+
* 날짜 컬럼으로 바꾸면서 **조용한 함정**이 하나 드러났다: sqlite 는 datetime 을 공백 구분 형식
|
|
9
|
+
* (`2026-08-07 11:00:00.000`)으로 저장하는데, 화면은 ISO 문자열(`…T11:00:00.000Z`)을 보낸다.
|
|
10
|
+
* 문자열 그대로 비교하면 사전식으로 어긋나 **오류 없이 0건**이 나온다 — 원장 시간창이 조용히
|
|
11
|
+
* 비어 버리고, 사용자는 "그 시간에 아무 일도 없었다" 로 읽는다.
|
|
12
|
+
*
|
|
13
|
+
* 그래서 두 가지를 못 박는다: 저장 형식이 실제로 그렇다는 것과, 경계에서 날짜로 옮기면 걸린다는 것.
|
|
14
|
+
* 이 검사가 깨지면 typeorm/드라이버의 거동이 바뀐 것이므로 변환 지점을 다시 봐야 한다.
|
|
15
|
+
*/
|
|
16
|
+
import { test } from 'node:test'
|
|
17
|
+
import assert from 'node:assert/strict'
|
|
18
|
+
import { createRequire } from 'node:module'
|
|
19
|
+
|
|
20
|
+
const req = createRequire(import.meta.url)
|
|
21
|
+
const { DataSource, EntitySchema, Between } = req('typeorm')
|
|
22
|
+
|
|
23
|
+
const Ev = new EntitySchema({
|
|
24
|
+
name: 'ev',
|
|
25
|
+
tableName: 'ev',
|
|
26
|
+
columns: {
|
|
27
|
+
id: { primary: true, type: 'int', generated: true },
|
|
28
|
+
eventTime: { type: 'datetime', nullable: true }
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
async function seeded() {
|
|
33
|
+
const ds = new DataSource({ type: 'sqlite', database: ':memory:', entities: [Ev], synchronize: true })
|
|
34
|
+
await ds.initialize()
|
|
35
|
+
const repo = ds.getRepository('ev')
|
|
36
|
+
await repo.save([
|
|
37
|
+
{ eventTime: new Date('2026-08-07T11:00:00.000Z') },
|
|
38
|
+
{ eventTime: new Date('2026-08-07T13:00:00.000Z') }
|
|
39
|
+
])
|
|
40
|
+
return { ds, repo }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
test('sqlite 는 datetime 을 공백 구분 형식으로 저장한다 — ISO 문자열과 사전식으로 어긋난다', async () => {
|
|
44
|
+
const { ds } = await seeded()
|
|
45
|
+
const raw = await ds.query('select eventTime from ev order by id')
|
|
46
|
+
assert.match(raw[0].eventTime, /^\d{4}-\d{2}-\d{2} /, '공백 구분이 아니면 이 검사의 전제가 바뀐 것이다')
|
|
47
|
+
assert.ok(!String(raw[0].eventTime).includes('T'), 'T 가 들어가면 ISO 와 같은 형식이므로 함정이 사라진 것이다')
|
|
48
|
+
await ds.destroy()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('ISO 문자열을 그대로 넘기면 **조용히 0건** — 오류가 아니라 빈 결과라 더 위험하다', async () => {
|
|
52
|
+
const { ds, repo } = await seeded()
|
|
53
|
+
const rows = await repo.find({
|
|
54
|
+
where: { eventTime: Between('2026-08-07T10:00:00.000Z', '2026-08-07T12:00:00.000Z') }
|
|
55
|
+
})
|
|
56
|
+
assert.equal(rows.length, 0, '이 검사가 깨졌다면 드라이버가 문자열을 받아 주게 된 것 — 변환 지점을 재검토할 것')
|
|
57
|
+
await ds.destroy()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('경계에서 날짜로 옮기면 제대로 걸린다 — 질의 전에 반드시 변환한다', async () => {
|
|
61
|
+
const { ds, repo } = await seeded()
|
|
62
|
+
const rows = await repo.find({
|
|
63
|
+
where: { eventTime: Between(new Date('2026-08-07T10:00:00.000Z'), new Date('2026-08-07T12:00:00.000Z')) }
|
|
64
|
+
})
|
|
65
|
+
assert.equal(rows.length, 1)
|
|
66
|
+
await ds.destroy()
|
|
67
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 예측 지표 — **스냅샷에서 무엇을 세는가.**
|
|
3
|
+
*
|
|
4
|
+
* 여기서 틀리면 예측 그래프가 조용히 다른 것을 그린다(오류 없이 곡선이 나온다). 특히 에너지 지표는
|
|
5
|
+
* 「없음」과 「0」이 같은 그림이 되기 쉬워서, 그 경계를 명시로 잡아 둔다.
|
|
6
|
+
*/
|
|
7
|
+
import { test } from 'node:test'
|
|
8
|
+
import assert from 'node:assert/strict'
|
|
9
|
+
|
|
10
|
+
import { FORECAST_METRICS, metricsForKind } from '../dist-server/service/twin-forecast/forecast-metrics.js'
|
|
11
|
+
|
|
12
|
+
test('물류 지표 — 기존 여섯은 그대로 센다', () => {
|
|
13
|
+
const s = {
|
|
14
|
+
items: [1, 2, 3],
|
|
15
|
+
locations: [{ occupancy: 2 }, { occupancy: 5 }],
|
|
16
|
+
tasks: [{}, {}],
|
|
17
|
+
orders: [{ status: 'shipped' }, { status: 'picking' }, { status: 'cancelled' }]
|
|
18
|
+
}
|
|
19
|
+
assert.equal(FORECAST_METRICS.items(s), 3)
|
|
20
|
+
assert.equal(FORECAST_METRICS.occupancy(s), 7)
|
|
21
|
+
assert.equal(FORECAST_METRICS.tasks(s), 2)
|
|
22
|
+
assert.equal(FORECAST_METRICS.orders(s), 3)
|
|
23
|
+
assert.equal(FORECAST_METRICS.shipped(s), 1)
|
|
24
|
+
/* 취소는 백로그가 아니다 — 대기와 취소를 같이 세면 「안 빠지는 일감」이 부풀려진다. */
|
|
25
|
+
assert.equal(FORECAST_METRICS.backlog(s), 1)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('에너지 지표 — 커널이 낸 값을 그대로 읽는다(다시 계산하지 않는다)', () => {
|
|
29
|
+
const s = {
|
|
30
|
+
energy: {
|
|
31
|
+
peakSince: { kW: 812 },
|
|
32
|
+
open: { meanKW: 300, projectedKW: 455, projectionBasis: 'mean-so-far' },
|
|
33
|
+
closed: [{ overContract: true }, { overContract: false }, { overContract: true }]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
assert.equal(FORECAST_METRICS.peakKW(s), 812)
|
|
37
|
+
assert.equal(FORECAST_METRICS.projectedKW(s), 455)
|
|
38
|
+
assert.equal(FORECAST_METRICS.overWindows(s), 2)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('투영이 아직 없으면 지금까지의 평균으로 — 0 으로 떨어뜨리지 않는다', () => {
|
|
42
|
+
assert.equal(FORECAST_METRICS.projectedKW({ energy: { open: { meanKW: 120 } } }), 120)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('에너지가 없는 스냅샷 — 세 지표 모두 0(그리지 않을 값이지, 관측한 0 이 아니다)', () => {
|
|
46
|
+
assert.equal(FORECAST_METRICS.peakKW({ items: [] }), 0)
|
|
47
|
+
assert.equal(FORECAST_METRICS.projectedKW({ items: [] }), 0)
|
|
48
|
+
assert.equal(FORECAST_METRICS.overWindows({ items: [] }), 0)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('계약을 모르면 초과 구간은 0 — overContract 가 없는 구간을 초과로 세지 않는다', () => {
|
|
52
|
+
const s = { energy: { closed: [{ maxKW: 900 }, { maxKW: 1200 }] } }
|
|
53
|
+
assert.equal(FORECAST_METRICS.overWindows(s), 0)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('종류별 지표 목록 — 에너지 트윈에는 물류 지표가 섞이지 않는다', () => {
|
|
57
|
+
const ems = metricsForKind('ems')
|
|
58
|
+
assert.deepEqual(ems, ['projectedKW', 'peakKW', 'overWindows'])
|
|
59
|
+
assert.equal(ems.includes('items' as any), false)
|
|
60
|
+
assert.equal(metricsForKind('wms').includes('items'), true)
|
|
61
|
+
/* 종류를 모르면 물류로 — 기존 트윈(kind 미선언)이 지표를 잃지 않는다. */
|
|
62
|
+
assert.equal(metricsForKind(undefined).includes('items'), true)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('목록의 모든 키가 실제로 계산 가능하다 — 화면에 뜨는데 서버가 모르는 지표는 없다', () => {
|
|
66
|
+
for (const kind of ['ems', 'wms', 'mes', 'yms']) {
|
|
67
|
+
for (const key of metricsForKind(kind)) {
|
|
68
|
+
assert.equal(typeof (FORECAST_METRICS as any)[key], 'function', `${kind}:${key}`)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 예측 틱 크기 — **지평선이 정하고, 양끝은 묶인다.**
|
|
3
|
+
*
|
|
4
|
+
* 이 규칙이 틀리면 예측이 조용히 비싸지거나(1초 고정 시절 9,000틱·28.5초) 조용히 성글어진다
|
|
5
|
+
* (짧은 작업이 한 틱에 삼켜진다). 둘 다 화면에는 그럴듯한 숫자로 나오므로 시험이 지킨다.
|
|
6
|
+
*/
|
|
7
|
+
import { test } from 'node:test'
|
|
8
|
+
import assert from 'node:assert/strict'
|
|
9
|
+
|
|
10
|
+
import { forecastStepMs, TARGET_TICKS_PER_RUN, MAX_STEP_MS, MIN_STEP_MS } from '../dist-server/service/twin-forecast/forecast-tuning.js'
|
|
11
|
+
|
|
12
|
+
test('기본 지평선(5분)은 5초 단위 — 실측으로 1초와 답이 같고 5배 싸다', () => {
|
|
13
|
+
assert.equal(forecastStepMs(300_000), 5_000)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('짧은 지평선은 더 잘게 — 1분이면 1초', () => {
|
|
17
|
+
assert.equal(forecastStepMs(60_000), 1_000)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('아주 짧아도 커널 시각 단위 아래로 내려가지 않는다', () => {
|
|
21
|
+
assert.equal(forecastStepMs(5_000), MIN_STEP_MS)
|
|
22
|
+
assert.equal(forecastStepMs(0), MIN_STEP_MS)
|
|
23
|
+
assert.equal(forecastStepMs(Number.NaN), MIN_STEP_MS)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('아무리 길어도 상한을 넘지 않는다 — 짧은 작업이 한 틱에 삼켜진다', () => {
|
|
27
|
+
assert.equal(forecastStepMs(2 * 60 * 60_000), MAX_STEP_MS)
|
|
28
|
+
assert.equal(forecastStepMs(24 * 60 * 60_000), MAX_STEP_MS)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('틱 크기는 초 단위로 떨어진다 — 3,333ms 는 사람도 화면도 설명할 수 없다', () => {
|
|
32
|
+
for (const h of [100_000, 200_000, 333_000, 777_000]) {
|
|
33
|
+
assert.equal(forecastStepMs(h) % 1000, 0, `${h}ms`)
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('상한에 걸리지 않는 구간에서는 목표 틱 수 근처로 돈다', () => {
|
|
38
|
+
for (const h of [60_000, 120_000, 300_000, 600_000]) {
|
|
39
|
+
const ticks = h / forecastStepMs(h)
|
|
40
|
+
assert.ok(ticks >= TARGET_TICKS_PER_RUN * 0.8 && ticks <= TARGET_TICKS_PER_RUN * 1.25, `${h}ms → ${ticks}틱`)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
@@ -52,11 +52,38 @@ test('learnedCalibration — biasShift=평균오차, 편향 제거로 정확도
|
|
|
52
52
|
assert.equal(learn.accAfter, 100, '일정 편향을 완전 제거 → 잔차 0 → 100%')
|
|
53
53
|
})
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
/* 같은 격자 위의 점 — vantage 를 명시한다(예전 헬퍼는 t 를 늘 0 으로 두어 시간축을 전혀 시험하지 않았다). */
|
|
56
|
+
const at = (t: number, error: number, actual = 100) => ({ ...pt(error, actual), t })
|
|
57
|
+
|
|
58
|
+
test('aggregatePoints — 공동배치 합산(공장 총량)', () => {
|
|
59
|
+
const a = [at(10, 2, 30), at(20, 4, 30)]
|
|
60
|
+
const b = [at(10, 1, 20), at(20, 2, 20)]
|
|
61
|
+
const { points, droppedVantages } = aggregatePoints([a, b])
|
|
62
|
+
assert.equal(points.length, 2)
|
|
63
|
+
assert.equal(droppedVantages, 0)
|
|
64
|
+
assert.equal(points[0].actual, 50, '실제 30+20')
|
|
65
|
+
assert.equal(points[0].t, 10, '합산점의 시각은 그 vantage 다')
|
|
66
|
+
assert.equal(points[0].error, points[0].actual - points[0].p50, 'error = 실제−예측(합산 후 재계산)')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('한 트윈이 못 잰 vantage 는 통째로 뺀다 — 빠진 트윈을 0 으로 세지 않는다', () => {
|
|
70
|
+
/*
|
|
71
|
+
* 예전에는 `max` 길이로 돌며 있는 것만 더했다. 그래서 이 경우 t=20 의 공장 총량이 20(b 뿐)으로
|
|
72
|
+
* 나왔다 — a 가 그 시각에 아무것도 안 했다는 뜻이 아니라, **못 쟀다**는 뜻인데도.
|
|
73
|
+
* 공장 합계 곡선이 꼬리에서 아래로 꺾이고, 그 곡선으로 예측 정확도를 판정했다.
|
|
74
|
+
*/
|
|
75
|
+
const a = [at(10, 2, 30), null]
|
|
76
|
+
const b = [at(10, 1, 20), at(20, 2, 20)]
|
|
77
|
+
const { points, droppedVantages } = aggregatePoints([a as any, b])
|
|
78
|
+
assert.equal(points.length, 1, '모두가 값을 가진 vantage 만 남는다')
|
|
79
|
+
assert.equal(points[0].t, 10)
|
|
80
|
+
assert.equal(droppedVantages, 1, '뺀 개수를 돌려줘야 화면이 "2점 중 1점" 이라고 말할 수 있다')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('길이가 다르면 뒤쪽은 뺀다 — 남은 트윈만의 합을 공장 총량이라 부르지 않는다', () => {
|
|
84
|
+
const a = [at(10, 2, 30)]
|
|
85
|
+
const b = [at(10, 1, 20), at(20, 2, 20)]
|
|
86
|
+
const { points, droppedVantages } = aggregatePoints([a, b])
|
|
87
|
+
assert.equal(points.length, 1)
|
|
88
|
+
assert.equal(droppedVantages, 1)
|
|
62
89
|
})
|
|
@@ -8,10 +8,10 @@ import { test } from 'node:test'
|
|
|
8
8
|
import assert from 'node:assert/strict'
|
|
9
9
|
|
|
10
10
|
import { StateProjector, objectEvent, BIZSTEP, DISP } from '@operato/twin-kernel'
|
|
11
|
-
import { buildEntityDeltas } from '../
|
|
11
|
+
import { buildEntityDeltas } from '../shared/entity-delta.ts'
|
|
12
12
|
|
|
13
13
|
const T0 = '2026-07-19T00:00:00.000Z'
|
|
14
|
-
const
|
|
14
|
+
const model = (nNodes: number) => ({
|
|
15
15
|
locations: Array.from({ length: nNodes }, (_, i) => ({ id: `bin-${i}`, type: 'storage', capacity: 100000 })),
|
|
16
16
|
equipment: []
|
|
17
17
|
})
|
|
@@ -23,7 +23,7 @@ function envelope(epc: string, location: string): any {
|
|
|
23
23
|
test('bench: apply 처리량(이벤트 흡수)', () => {
|
|
24
24
|
const nNodes = 200
|
|
25
25
|
const M = 200_000
|
|
26
|
-
const proj = new StateProjector(
|
|
26
|
+
const proj = new StateProjector(model(nNodes))
|
|
27
27
|
const t0 = performance.now()
|
|
28
28
|
for (let i = 0; i < M; i++) proj.apply(envelope(`urn:epc:id:sscc:0614141.${i}`, `bin-${i % nNodes}`))
|
|
29
29
|
const dt = performance.now() - t0
|
|
@@ -33,7 +33,7 @@ test('bench: apply 처리량(이벤트 흡수)', () => {
|
|
|
33
33
|
|
|
34
34
|
test('bench: snapshot 비용 vs item 수 (전체 O(N) 재구성)', () => {
|
|
35
35
|
for (const N of [1000, 10000, 50000]) {
|
|
36
|
-
const proj = new StateProjector(
|
|
36
|
+
const proj = new StateProjector(model(200))
|
|
37
37
|
for (let i = 0; i < N; i++) proj.apply(envelope(`urn:epc:id:sscc:0614141.${i}`, `bin-${i % 200}`))
|
|
38
38
|
const iters = 200
|
|
39
39
|
const t0 = performance.now()
|
|
@@ -45,7 +45,7 @@ test('bench: snapshot 비용 vs item 수 (전체 O(N) 재구성)', () => {
|
|
|
45
45
|
|
|
46
46
|
test('bench: buildEntityDeltas 비용 vs 엔티티 수 (전 엔티티 매번 방출)', () => {
|
|
47
47
|
for (const N of [100, 1000, 5000]) {
|
|
48
|
-
const proj = new StateProjector(
|
|
48
|
+
const proj = new StateProjector(model(N))
|
|
49
49
|
for (let i = 0; i < N * 5; i++) proj.apply(envelope(`urn:epc:id:sscc:0614141.${i}`, `bin-${i % N}`))
|
|
50
50
|
const snap = proj.snapshot()
|
|
51
51
|
const iters = 200
|
|
@@ -58,7 +58,7 @@ test('bench: buildEntityDeltas 비용 vs 엔티티 수 (전 엔티티 매번 방
|
|
|
58
58
|
|
|
59
59
|
test('bench: 방송 1회 총비용(snapshot + buildEntityDeltas) vs 상태 규모', () => {
|
|
60
60
|
for (const [locations, items] of [[200, 5000], [1000, 50000], [2000, 100000]]) {
|
|
61
|
-
const proj = new StateProjector(
|
|
61
|
+
const proj = new StateProjector(model(locations))
|
|
62
62
|
for (let i = 0; i < items; i++) proj.apply(envelope(`urn:epc:id:sscc:0614141.${i}`, `bin-${i % locations}`))
|
|
63
63
|
const iters = 100
|
|
64
64
|
const t0 = performance.now()
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ 가동 중 인제스트 — **거절할 것이면 아무것도 쓰기 전에 거절한다.**
|
|
3
|
+
*
|
|
4
|
+
* 가동 중 재프로비저닝은 `provision` 이 막는다. 그런데 그 호출은 `ingestMaster` 의 **맨 끝**이었고,
|
|
5
|
+
* 그때까지 공간·표현·구역(TwinArea)은 이미 저장된 뒤였다. 사용자 화면에는 "인제스트 실패" 가
|
|
6
|
+
* 뜨는데 실제로는 절반이 적용된 상태다 — **실패라고 말하면서 데이터를 바꾸는 것**이 가장 나쁘다.
|
|
7
|
+
*
|
|
8
|
+
* 이 검사는 그 경계 하나를 지킨다: 거절된 인제스트는 **흔적을 남기지 않는다.**
|
|
9
|
+
*
|
|
10
|
+
* 순수 함수로는 확인되지 않는다. 부분 쓰기는 정의상 "여러 표에 걸쳐 무엇이 남았나" 이므로 실
|
|
11
|
+
* DataSource 가 있어야 한다(멀티DB 규약상 sqlite 가 5개 드라이버 공통의 최소선이다).
|
|
12
|
+
*
|
|
13
|
+
* 이 제약 자체는 최종형이 아니다 — 커널에는 가동 중 구조 교체(`adoptStructure`)가 이미 있고
|
|
14
|
+
* 재생은 리비전 경계마다 그것을 쓴다. 라이브에 배선되면 이 거절은 사라지고 이 검사도 함께 간다.
|
|
15
|
+
*/
|
|
16
|
+
import { test, before, after } from 'node:test'
|
|
17
|
+
import assert from 'node:assert/strict'
|
|
18
|
+
import { createRequire } from 'node:module'
|
|
19
|
+
import type { DataSource } from 'typeorm'
|
|
20
|
+
|
|
21
|
+
const req = createRequire(import.meta.url)
|
|
22
|
+
const { NamingStrategy, addDataSource, entities: shellEntities, Domain } = req('@things-factory/shell')
|
|
23
|
+
const { entities: authEntities } = req('@things-factory/auth-base')
|
|
24
|
+
const { entities: twinEntities } = req('../dist-server/service/index.js')
|
|
25
|
+
const { TwinEngine } = req('../dist-server/engine/index.js')
|
|
26
|
+
const { TwinInstance } = req('../dist-server/service/twin-instance/twin-instance.js')
|
|
27
|
+
const { TwinStructure } = req('../dist-server/service/twin-structure/twin-structure.js')
|
|
28
|
+
const { TwinArea } = req('../dist-server/service/twin-space/twin-area.js')
|
|
29
|
+
const { TwinSpace } = req('../dist-server/service/twin-space/twin-space.js')
|
|
30
|
+
const { TwinLocation } = req('../dist-server/service/twin-model/twin-location.js')
|
|
31
|
+
const { DataSource: TypeOrmDataSource } = req('typeorm')
|
|
32
|
+
|
|
33
|
+
let ds: DataSource
|
|
34
|
+
let domainId: string
|
|
35
|
+
|
|
36
|
+
const INSTANCE = 'guard-probe'
|
|
37
|
+
|
|
38
|
+
/** WMS 마스터 — `bins` 로 규모를, `siteName`·구역 이름으로 "무엇이 바뀌려 했나" 를 다이얼한다. */
|
|
39
|
+
const master = (bins: number, siteName: string, areaName: string): any => ({
|
|
40
|
+
source: INSTANCE,
|
|
41
|
+
system: 'wms',
|
|
42
|
+
siteName,
|
|
43
|
+
space: { width: 60, depth: 30, unit: 'm' },
|
|
44
|
+
locations: [
|
|
45
|
+
{ id: 'a-dc', type: 'warehouse', role: 'area', name: areaName },
|
|
46
|
+
{ id: 'dock-recv', type: 'dock', role: 'location', name: '입고 도크', capacity: 100 },
|
|
47
|
+
...Array.from({ length: bins }, (_, i) => ({
|
|
48
|
+
id: `bin-${i + 1}`,
|
|
49
|
+
type: 'storage',
|
|
50
|
+
role: 'location',
|
|
51
|
+
name: `빈${i + 1}`,
|
|
52
|
+
parentId: 'a-dc',
|
|
53
|
+
capacity: 20
|
|
54
|
+
}))
|
|
55
|
+
],
|
|
56
|
+
equipment: [{ id: 'fk-1', kind: 'forklift', homeLocationId: 'dock-recv' }]
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
/** 지금 DB 에 남아 있는 모든 흔적 — 인제스트가 건드리는 표를 한 줌으로 뜬다. */
|
|
60
|
+
async function trace() {
|
|
61
|
+
const inst = await ds.getRepository(TwinInstance).findOne({ where: { instanceId: INSTANCE } })
|
|
62
|
+
const space = await ds.getRepository(TwinSpace).findOne({ where: { spaceId: `${INSTANCE}-site` } })
|
|
63
|
+
const areas = await ds.getRepository(TwinArea).find({ order: { areaId: 'ASC' } })
|
|
64
|
+
const revs = await ds.getRepository(TwinStructure).find({ order: { rev: 'ASC' } })
|
|
65
|
+
return {
|
|
66
|
+
boardLocations: (inst?.model?.locations ?? []).length,
|
|
67
|
+
spaceName: space?.name,
|
|
68
|
+
areaNames: areas.map((a: any) => a.name),
|
|
69
|
+
revs: revs.map((r: any) => r.rev),
|
|
70
|
+
rows: await ds.getRepository(TwinLocation).count()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
before(async () => {
|
|
75
|
+
ds = new TypeOrmDataSource({
|
|
76
|
+
type: 'sqlite',
|
|
77
|
+
database: ':memory:',
|
|
78
|
+
synchronize: true,
|
|
79
|
+
dropSchema: true,
|
|
80
|
+
namingStrategy: new NamingStrategy(),
|
|
81
|
+
entities: [...shellEntities, ...authEntities, ...twinEntities],
|
|
82
|
+
logging: false
|
|
83
|
+
})
|
|
84
|
+
await ds.initialize()
|
|
85
|
+
addDataSource('default', ds)
|
|
86
|
+
const d = await ds.getRepository(Domain).save({ name: 'guard-test', subdomain: 'guard-test' } as any)
|
|
87
|
+
domainId = d.id
|
|
88
|
+
|
|
89
|
+
await TwinEngine.ingestMaster(domainId, master(3, '서울 창고', '보관 구역'))
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
after(async () => {
|
|
93
|
+
await TwinEngine.stop(domainId, INSTANCE).catch(() => {})
|
|
94
|
+
await ds?.destroy()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('멈춰 있으면 재인제스트가 통한다 — 막는 것은 가동 중일 때뿐이다', async () => {
|
|
98
|
+
await TwinEngine.ingestMaster(domainId, master(5, '서울 창고', '보관 구역'))
|
|
99
|
+
const t = await trace()
|
|
100
|
+
assert.equal(t.boardLocations, 6, '자리 6개(도크 1 + 빈 5)로 갱신된다')
|
|
101
|
+
assert.deepEqual(t.revs, [1, 2], '구조가 달라졌으니 리비전이 하나 늘어난다')
|
|
102
|
+
assert.equal(t.rows, 6, '표도 함께 다시 그려진다')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('가동 중이면 거절하고 — **아무것도 바꾸지 않는다**', async () => {
|
|
106
|
+
await TwinEngine.startFromRegistry(domainId, INSTANCE, 'sim-experiment')
|
|
107
|
+
const before = await trace()
|
|
108
|
+
|
|
109
|
+
/*
|
|
110
|
+
* 바뀔 것을 **일부러 전부** 흔든다. 구조(빈 5→9)뿐 아니라 공간 이름·구역 이름까지 —
|
|
111
|
+
* 부분 쓰기는 앞쪽(공간·구역)에서 일어났으므로, 구조만 확인하면 예전 결함을 못 잡는다.
|
|
112
|
+
*/
|
|
113
|
+
await assert.rejects(
|
|
114
|
+
() => TwinEngine.ingestMaster(domainId, master(9, '부산 창고', '다른 구역')),
|
|
115
|
+
/is running/,
|
|
116
|
+
'가동 중에는 거절해야 한다'
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
assert.deepEqual(await trace(), before, '거절된 인제스트는 어느 표에도 흔적을 남기지 않는다')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('멈추면 다시 통한다 — 거절은 잠금이 아니라 순서다', async () => {
|
|
123
|
+
await TwinEngine.stop(domainId, INSTANCE)
|
|
124
|
+
await TwinEngine.ingestMaster(domainId, master(9, '부산 창고', '다른 구역'))
|
|
125
|
+
|
|
126
|
+
const t = await trace()
|
|
127
|
+
assert.equal(t.boardLocations, 10, '아까 거절당한 그 구조가 이제 들어간다')
|
|
128
|
+
assert.deepEqual(t.revs, [1, 2, 3])
|
|
129
|
+
assert.equal(t.rows, 10)
|
|
130
|
+
/*
|
|
131
|
+
* 공간 이름은 **안 바뀐다** — 이미 있는 공간의 이름은 인제스트가 덮지 않는다(공유 공간에서
|
|
132
|
+
* 먼저 쓴 값이 이긴다). 거절 검사가 "안 바뀌었다" 를 확인할 때 이 성질에 기대지 않도록,
|
|
133
|
+
* 여기서 그것이 별개의 규칙임을 못 박아 둔다.
|
|
134
|
+
*/
|
|
135
|
+
assert.equal(t.spaceName, '서울 창고', '기존 공간 이름은 재인제스트가 덮지 않는다(별개 규칙)')
|
|
136
|
+
})
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { test } from 'node:test'
|
|
8
8
|
import assert from 'node:assert/strict'
|
|
9
9
|
|
|
10
|
-
import { resolveIngestSpace, collidingIds, INGEST_SPACE_NOT_FOUND } from '../server/service/reference/ingest-space.ts'
|
|
10
|
+
import { resolveIngestSpace, refuseImportSpace, collidingIds, INGEST_SPACE_NOT_FOUND } from '../server/service/reference/ingest-space.ts'
|
|
11
11
|
|
|
12
12
|
test('고르지 않으면 마스터가 파생한 공간으로 — 지금까지의 거동을 바꾸지 않는다', () => {
|
|
13
13
|
for (const req of [undefined, null, '', ' ']) {
|
|
@@ -48,3 +48,31 @@ test('같은 id 를 쓰는 것들을 찾아낸다 — 합쳐지는 것을 모르
|
|
|
48
48
|
assert.deepEqual(collidingIds(undefined, undefined), [], '없는 쪽은 빈 것으로 본다')
|
|
49
49
|
assert.deepEqual(collidingIds([{}, { id: '' }], [{ id: '' }]), [], 'id 없는 항목은 세지 않는다')
|
|
50
50
|
})
|
|
51
|
+
|
|
52
|
+
/* ── 실 시스템 import 의 현장 선택 (2026-08-13) ───────────────────────────── */
|
|
53
|
+
|
|
54
|
+
test('현장을 고르지 않으면 사이트가 몇이든 진행한다 — 지금까지의 거동', () => {
|
|
55
|
+
for (const n of [0, 1, 5, 40]) {
|
|
56
|
+
assert.equal(refuseImportSpace(n, undefined), undefined)
|
|
57
|
+
assert.equal(refuseImportSpace(n, ''), undefined)
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('사이트 하나면 그 현장에 넣는다 — 렌즈를 더하는 정상 경로', () => {
|
|
62
|
+
assert.equal(refuseImportSpace(1, 'rosarito-site'), undefined)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('사이트 여럿을 한 현장에 넣는 것은 거절한다 — 서로 다른 현장이 조용히 합쳐진다', () => {
|
|
66
|
+
const r = refuseImportSpace(3, 'rosarito-site')
|
|
67
|
+
assert.ok(r, '거절해야 한다')
|
|
68
|
+
assert.equal(r.code, 'space-with-multiple-sites')
|
|
69
|
+
assert.equal(r.params?.count, 3)
|
|
70
|
+
assert.equal(r.params?.spaceId, 'rosarito-site')
|
|
71
|
+
assert.match(r.message, /pick one site/)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('거절 사유는 코드로 낸다 — 화면이 다섯 언어로 번역할 수 있게', () => {
|
|
75
|
+
const r = refuseImportSpace(2, ' rosarito-site ')
|
|
76
|
+
assert.equal(r?.code, 'space-with-multiple-sites')
|
|
77
|
+
assert.equal(r?.params?.spaceId, 'rosarito-site', '앞뒤 공백은 다듬어 낸다')
|
|
78
|
+
})
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ 인스턴스별 캐시는 **트윈과 함께 사라진다.**
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 이 검사가 필요한가 ─────────────────────────────────────────────────────
|
|
5
|
+
* 엔진은 트윈마다 메모리에 몇 가지를 들고 있다(실측 추정기·구조 리비전·복구 표식). 지움은 저널과
|
|
6
|
+
* 등록부까지 갔는데 **캐시는 남았다** — 삭제된 트윈의 항목이 서버 재기동까지 살아 있었다. 항목 하나가
|
|
7
|
+
* 하루치 KPI 그룹을 물고 있으므로, 트윈을 만들고 지우는 일이 반복되는 현장에서는 그것이 곧 누수다.
|
|
8
|
+
*
|
|
9
|
+
* TTL 은 이것을 막지 못한다: TTL 은 **값이 낡는 것**을 막을 뿐 **없어진 주인**을 지우지 않는다.
|
|
10
|
+
*
|
|
11
|
+
* 그래서 규율은 둘이다.
|
|
12
|
+
* ① 트윈이 사라지면 그 트윈에 대한 메모리를 **전부** 잊는다(정확한 쪽).
|
|
13
|
+
* ② 우리 API 를 지나지 않고 사라지는 길(도메인째 삭제·DB 직접 조작)에는 **상한**이 막는다(방어).
|
|
14
|
+
*
|
|
15
|
+
* 사설(private) 필드를 들여다보는 검사인 것을 안다. 그런데 여기서 지켜야 하는 것은 겉으로 드러나는
|
|
16
|
+
* 값이 아니라 **메모리가 자라지 않는다**는 사실이고, 그것은 안을 봐야 볼 수 있다.
|
|
17
|
+
*/
|
|
18
|
+
import { test, before, after } from 'node:test'
|
|
19
|
+
import assert from 'node:assert/strict'
|
|
20
|
+
import { createRequire } from 'node:module'
|
|
21
|
+
import type { DataSource } from 'typeorm'
|
|
22
|
+
|
|
23
|
+
const req = createRequire(import.meta.url)
|
|
24
|
+
const { NamingStrategy, addDataSource, entities: shellEntities, Domain } = req('@things-factory/shell')
|
|
25
|
+
const { entities: authEntities } = req('@things-factory/auth-base')
|
|
26
|
+
const { entities: twinEntities } = req('../dist-server/service/index.js')
|
|
27
|
+
const { TwinEngine } = req('../dist-server/engine/index.js')
|
|
28
|
+
const { DataSource: TypeOrmDataSource } = req('typeorm')
|
|
29
|
+
|
|
30
|
+
let ds: DataSource
|
|
31
|
+
let domainId: string
|
|
32
|
+
|
|
33
|
+
const INSTANCE = 'cache-probe'
|
|
34
|
+
|
|
35
|
+
const master = (): any => ({
|
|
36
|
+
source: INSTANCE,
|
|
37
|
+
system: 'wms',
|
|
38
|
+
siteName: '캐시 창고',
|
|
39
|
+
space: { spaceId: 'cache-site', width: 40, depth: 30, unit: 'm' },
|
|
40
|
+
locations: [
|
|
41
|
+
{ id: 'dock', type: 'dock', role: 'location', name: '도크', capacity: 50 },
|
|
42
|
+
{ id: 'bin-1', type: 'storage', role: 'location', name: '빈1', capacity: 20 }
|
|
43
|
+
],
|
|
44
|
+
equipment: [{ id: 'fk-1', kind: 'forklift', homeLocationId: 'dock' }]
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
/** 캐시 안을 본다 — 겉으로는 안 보이는 사실을 지키는 검사이므로. */
|
|
48
|
+
const caches = () => ({
|
|
49
|
+
measured: (TwinEngine as any).measuredCache as Map<string, unknown>,
|
|
50
|
+
structureRev: (TwinEngine as any).structureRevCache as Record<string, number>,
|
|
51
|
+
recovered: (TwinEngine as any).recovered as Record<string, unknown>
|
|
52
|
+
})
|
|
53
|
+
const keyOf = (id: string) => `${domainId}:${id}`
|
|
54
|
+
|
|
55
|
+
before(async () => {
|
|
56
|
+
ds = new TypeOrmDataSource({
|
|
57
|
+
type: 'sqlite',
|
|
58
|
+
database: ':memory:',
|
|
59
|
+
synchronize: true,
|
|
60
|
+
dropSchema: true,
|
|
61
|
+
namingStrategy: new NamingStrategy(),
|
|
62
|
+
entities: [...shellEntities, ...authEntities, ...twinEntities],
|
|
63
|
+
logging: false
|
|
64
|
+
})
|
|
65
|
+
await ds.initialize()
|
|
66
|
+
addDataSource('default', ds)
|
|
67
|
+
const d = await ds.getRepository(Domain).save({ name: 'cache-test', subdomain: 'cache-test' } as any)
|
|
68
|
+
domainId = d.id
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
after(async () => {
|
|
72
|
+
await TwinEngine.stop(domainId, INSTANCE).catch(() => {})
|
|
73
|
+
await ds?.destroy()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('지운 트윈의 캐시는 남지 않는다 — TTL 은 없어진 주인을 지우지 않는다', async () => {
|
|
77
|
+
await TwinEngine.ingestMaster(domainId, master())
|
|
78
|
+
/* 캐시를 실제로 채운다 — 손으로 넣으면 "채우는 경로" 가 바뀌어도 검사가 통과한다. */
|
|
79
|
+
await TwinEngine.structureRevOf(domainId, INSTANCE)
|
|
80
|
+
const { measured, structureRev } = caches()
|
|
81
|
+
assert.ok(keyOf(INSTANCE) in structureRev, '구조 리비전이 캐시에 올라와야 이 검사가 뜻이 있다')
|
|
82
|
+
|
|
83
|
+
await TwinEngine.remove(domainId, INSTANCE)
|
|
84
|
+
|
|
85
|
+
const after = caches()
|
|
86
|
+
assert.equal(keyOf(INSTANCE) in after.structureRev, false, '지운 트윈의 구조 리비전이 남았다')
|
|
87
|
+
assert.equal(after.measured.has(keyOf(INSTANCE)), false, '지운 트윈의 실측 캐시가 남았다')
|
|
88
|
+
assert.equal(keyOf(INSTANCE) in after.recovered, false, '지운 트윈의 복구 표식이 남았다')
|
|
89
|
+
assert.equal(measured, after.measured, '캐시 객체를 갈아치우는 것이 아니라 항목을 지운다')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('상한을 넘으면 가장 오래 손대지 않은 것부터 버린다 — 주인 없이 사라진 트윈까지 막는다', async () => {
|
|
93
|
+
const { measured } = caches()
|
|
94
|
+
measured.clear()
|
|
95
|
+
const MAX = (TwinEngine as any).MEASURED_MAX as number
|
|
96
|
+
assert.ok(MAX > 0, '상한이 선언되어 있어야 방어가 성립한다')
|
|
97
|
+
|
|
98
|
+
/*
|
|
99
|
+
* 상한 + 2 개를 넣고, 넣는 경로(`measuredEstimator`)를 그대로 쓴다. 트윈이 등록되어 있지 않으면
|
|
100
|
+
* KPI 조회가 빈 값을 내고 캐시에는 `undefined` 가 들어간다 — **그 자체도 캐시할 값이다**(없다는
|
|
101
|
+
* 사실을 알고 있는 것과 매번 다시 묻는 것은 다르다).
|
|
102
|
+
*/
|
|
103
|
+
const call = (TwinEngine as any).measuredEstimator.bind(TwinEngine)
|
|
104
|
+
for (let i = 0; i < MAX + 2; i++) await call(domainId, `ghost-${i}`)
|
|
105
|
+
|
|
106
|
+
assert.ok(measured.size <= MAX, `상한을 넘어 자랐다(${measured.size} > ${MAX})`)
|
|
107
|
+
assert.equal(measured.has(`${domainId}:ghost-0`), false, '가장 오래된 항목이 남아 있다 — 버릴 순서가 어긋났다')
|
|
108
|
+
assert.ok(measured.has(`${domainId}:ghost-${MAX + 1}`), '가장 최근 항목은 남아야 한다')
|
|
109
|
+
measured.clear()
|
|
110
|
+
})
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 커널 선택 가드 — **모르는 종류로는 뜨지 않는다.**
|
|
3
|
+
*
|
|
4
|
+
* ── 무엇이 있었나 (2026-08-08) ───────────────────────────────────────────────
|
|
5
|
+
* 커널을 고르는 자리 셋이 모두 이랬다:
|
|
6
|
+
*
|
|
7
|
+
* const Kernel = KERNELS[reg.kind ?? 'wms'] ?? WmsKernel
|
|
8
|
+
*
|
|
9
|
+
* 폴백이 둘 겹쳐 있었다. ①은 컬럼이 비었을 때, ②는 표에 없는 값일 때. 그리고 `kind` 는
|
|
10
|
+
* 검증 없는 자유 문자열이다(`@Arg('kind') kind: string` — enum 도 화이트리스트도 없다).
|
|
11
|
+
* 그래서 오타 하나·대소문자 하나로 **야드/생산 트윈이 조용히 창고 커널로 돌았다.**
|
|
12
|
+
* 오류가 없으니 화면에는 트윈이 정상으로 보이고, 안에서 도는 규칙만 다른 도메인의 것이다.
|
|
13
|
+
* 예측 경로가 가장 나빴다 — 야드의 미래를 창고 규칙으로 굴려 놓고 숫자만 뜬다.
|
|
14
|
+
*
|
|
15
|
+
* ── 왜 검사로 남기나 ────────────────────────────────────────────────────────
|
|
16
|
+
* 자리를 고치는 것만으로는 부족하다. 커널 종류가 늘 때(다음은 TMS 든 무엇이든) 새 자리가
|
|
17
|
+
* 생기고, `?? WmsKernel` 은 "안전한 기본값" 처럼 보여서 다시 자란다. 실제로 세 자리에
|
|
18
|
+
* 똑같이 자라 있었다. 틀린 공장을 조용히 띄우는 것보다 뜨지 않는 편이 낫다.
|
|
19
|
+
*
|
|
20
|
+
* 이 검사가 막는 것은 **표를 밖에서 직접 색인하는 것**이다. 종류→커널 해소는 `kernelFor`
|
|
21
|
+
* 하나만 한다. 새 자리를 쓸 때 이 검사가 걸리면, 폴백을 붙이지 말고 `kernelFor` 를 부를 것.
|
|
22
|
+
*/
|
|
23
|
+
import { test } from 'node:test'
|
|
24
|
+
import assert from 'node:assert/strict'
|
|
25
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
|
26
|
+
import { join } from 'node:path'
|
|
27
|
+
|
|
28
|
+
const ENGINE = new URL('../server/engine/twin-engine.ts', import.meta.url).pathname
|
|
29
|
+
const MUTATION = new URL('../server/service/twin-lifecycle/twin-lifecycle-mutation.ts', import.meta.url).pathname
|
|
30
|
+
|
|
31
|
+
/*
|
|
32
|
+
* 종류 폴백은 **한 패키지에 갇혀 있지 않다.** 처음에 twin-engine 두 자리만 고쳤더니, 라이브 미러를
|
|
33
|
+
* 띄우는 호출자 넷(operato-twin 의 벤치·부하시험·프리셋 프로비저닝, headless 의 reference-live)이
|
|
34
|
+
* `det.kind ?? 'wms'` 로 **엔진이 던지기 전에 먼저 이기고** 있었다. 커넥터 spec 도 마찬가지였다.
|
|
35
|
+
* 그래서 두 패키지의 서버 코드를 함께 훑는다.
|
|
36
|
+
*/
|
|
37
|
+
const ROOTS = [new URL('../server', import.meta.url).pathname, new URL('../../operato-twin/server', import.meta.url).pathname]
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 코드만 본다 — 주석은 **과거를 설명하느라 옛 코드를 그대로 인용한다**(바로 위 머리말이 그렇다).
|
|
41
|
+
* 줄 단위로 `*`·`//` 만 걸러내면 여러 줄 주석의 둘째 줄부터 새어 나오므로 블록을 통째로 지운다.
|
|
42
|
+
*/
|
|
43
|
+
function codeOf(path: string): string[] {
|
|
44
|
+
return readFileSync(path, 'utf-8')
|
|
45
|
+
.replace(/\/\*[\s\S]*?\*\//g, '') // 블록 주석
|
|
46
|
+
.split('\n')
|
|
47
|
+
.map(l => l.replace(/\/\/.*$/, '')) // 줄 주석
|
|
48
|
+
.filter(l => l.trim())
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
test('커널 폴백이 없다 — 모르는 종류는 창고로 떨어지지 않는다', () => {
|
|
52
|
+
const offenders = codeOf(ENGINE).filter(l => /\?\?\s*WmsKernel|\|\|\s*WmsKernel/.test(l))
|
|
53
|
+
assert.deepEqual(offenders, [], '커널 선택에 폴백이 다시 생겼다 — kernelFor 로 던지게 할 것')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('종류→커널 해소는 kernelFor 한 곳만 한다', () => {
|
|
57
|
+
/* 표를 직접 색인하는 줄. 정의부(`const KERNELS: Record…`)와 kernelFor 내부는 제외. */
|
|
58
|
+
const indexing = codeOf(ENGINE).filter(l => /KERNELS\[/.test(l) && !/const K = KERNELS\[kind\]/.test(l))
|
|
59
|
+
assert.deepEqual(indexing, [], 'KERNELS 를 밖에서 색인하고 있다 — kernelFor(kind) 를 쓸 것')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('kernelFor 는 모르는 종류에 던진다 — 조용히 고르지 않는다', () => {
|
|
63
|
+
const src = readFileSync(ENGINE, 'utf-8')
|
|
64
|
+
const fn = src.slice(src.indexOf('function kernelFor'))
|
|
65
|
+
assert.match(fn.slice(0, 300), /throw new Error/, 'kernelFor 가 던지지 않으면 이 계약이 무너진다')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test("board 를 주는 기동은 kind 를 요구한다 — `kind ?? 'wms'` 로 메우지 않는다", () => {
|
|
69
|
+
const offenders = codeOf(MUTATION).filter(l => /kind\s*\?\?\s*['"]/.test(l))
|
|
70
|
+
assert.deepEqual(offenders, [], '종류를 빠뜨린 프로비저닝이 다시 창고로 기동한다')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
/** 두 패키지의 서버 .ts 전부. */
|
|
74
|
+
function serverFiles(): string[] {
|
|
75
|
+
const out: string[] = []
|
|
76
|
+
const walk = (dir: string) => {
|
|
77
|
+
for (const e of readdirSync(dir)) {
|
|
78
|
+
const p = join(dir, e)
|
|
79
|
+
if (statSync(p).isDirectory()) walk(p)
|
|
80
|
+
else if (p.endsWith('.ts') && !p.endsWith('.d.ts')) out.push(p)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const r of ROOTS) walk(r)
|
|
84
|
+
return out
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
test('어느 서버 코드도 커널 종류를 리터럴로 메우지 않는다 — 호출자가 엔진의 검증을 앞질렀다', () => {
|
|
88
|
+
/* 예시 JSON 안의 `system: 'wms'` 는 필드를 보여 주는 것이라 대상이 아니다 — 폴백 연산자만 본다. */
|
|
89
|
+
const offenders: string[] = []
|
|
90
|
+
for (const f of serverFiles()) {
|
|
91
|
+
codeOf(f).forEach(l => {
|
|
92
|
+
if (/(\?\?|\|\|)\s*['"](wms|yms|mes)['"]/.test(l)) offenders.push(`${f.split('/packages/')[1]}: ${l.trim().slice(0, 90)}`)
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
assert.deepEqual(offenders, [], '종류 폴백이 다시 생겼다 — 선언이 없으면 메우지 말고 던질 것')
|
|
96
|
+
})
|