@things-factory/headless-twin 10.0.12 → 10.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-server/engine/canonical-ingest.d.ts +57 -2
- package/dist-server/engine/canonical-ingest.js +43 -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/index.d.ts +3 -0
- package/dist-server/engine/index.js +4 -0
- package/dist-server/engine/index.js.map +1 -1
- package/dist-server/engine/ingest-health.d.ts +335 -0
- package/dist-server/engine/ingest-health.js +434 -0
- package/dist-server/engine/ingest-health.js.map +1 -0
- package/dist-server/engine/integration-coverage.d.ts +76 -0
- package/dist-server/engine/integration-coverage.js +73 -0
- package/dist-server/engine/integration-coverage.js.map +1 -0
- package/dist-server/engine/integration-probes.d.ts +65 -0
- package/dist-server/engine/integration-probes.js +100 -0
- package/dist-server/engine/integration-probes.js.map +1 -0
- package/dist-server/engine/integration-runner.d.ts +45 -0
- package/dist-server/engine/integration-runner.js +59 -0
- package/dist-server/engine/integration-runner.js.map +1 -0
- package/dist-server/engine/integration-target-profile.d.ts +57 -0
- package/dist-server/engine/integration-target-profile.js +79 -0
- package/dist-server/engine/integration-target-profile.js.map +1 -0
- package/dist-server/engine/kpi-fold.d.ts +27 -0
- package/dist-server/engine/kpi-fold.js +47 -4
- package/dist-server/engine/kpi-fold.js.map +1 -1
- package/dist-server/engine/kpi-query.js +128 -14
- package/dist-server/engine/kpi-query.js.map +1 -1
- package/dist-server/engine/live-feed-registry.js +2 -16
- package/dist-server/engine/live-feed-registry.js.map +1 -1
- package/dist-server/engine/load-meter.d.ts +1 -1
- package/dist-server/engine/load-meter.js +12 -3
- package/dist-server/engine/load-meter.js.map +1 -1
- package/dist-server/engine/log.d.ts +18 -0
- package/dist-server/engine/log.js +80 -0
- package/dist-server/engine/log.js.map +1 -0
- package/dist-server/engine/loop-lag.d.ts +54 -0
- package/dist-server/engine/loop-lag.js +87 -0
- package/dist-server/engine/loop-lag.js.map +1 -0
- package/dist-server/engine/model-gap.d.ts +108 -0
- package/dist-server/engine/model-gap.js +95 -0
- package/dist-server/engine/model-gap.js.map +1 -0
- package/dist-server/engine/restart-policy.d.ts +3 -3
- package/dist-server/engine/restart-policy.js +4 -4
- 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/stage-path.d.ts +83 -0
- package/dist-server/engine/stage-path.js +118 -0
- package/dist-server/engine/stage-path.js.map +1 -0
- package/dist-server/engine/twin-engine.d.ts +352 -16
- package/dist-server/engine/twin-engine.js +1037 -166
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/index.js +34 -2
- package/dist-server/index.js.map +1 -1
- package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.d.ts +5 -0
- package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js +58 -0
- package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js.map +1 -0
- package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.d.ts +5 -0
- package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js +55 -0
- package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js.map +1 -0
- package/dist-server/migrations/index.js +7 -1
- package/dist-server/migrations/index.js.map +1 -1
- package/dist-server/service/index.d.ts +5 -2
- package/dist-server/service/index.js +18 -7
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/reference/discovery-result.d.ts +1 -1
- package/dist-server/service/reference/discovery-result.js +1 -1
- package/dist-server/service/reference/discovery-result.js.map +1 -1
- package/dist-server/service/reference/index.d.ts +4 -1
- package/dist-server/service/reference/index.js +6 -1
- package/dist-server/service/reference/index.js.map +1 -1
- package/dist-server/service/reference/reference-adapter.d.ts +184 -3
- package/dist-server/service/reference/reference-adapter.js +28 -4
- package/dist-server/service/reference/reference-adapter.js.map +1 -1
- package/dist-server/service/reference/reference-assessment.d.ts +68 -0
- package/dist-server/service/reference/reference-assessment.js +136 -0
- package/dist-server/service/reference/reference-assessment.js.map +1 -0
- package/dist-server/service/reference/reference-live.d.ts +12 -1
- package/dist-server/service/reference/reference-live.js +116 -11
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-master.d.ts +47 -1
- package/dist-server/service/reference/reference-master.js +34 -4
- package/dist-server/service/reference/reference-master.js.map +1 -1
- package/dist-server/service/reference/reference-probe.d.ts +92 -0
- package/dist-server/service/reference/reference-probe.js +186 -0
- package/dist-server/service/reference/reference-probe.js.map +1 -0
- package/dist-server/service/reference/reference-progress-subscription.d.ts +17 -0
- package/dist-server/service/reference/reference-progress-subscription.js +94 -0
- package/dist-server/service/reference/reference-progress-subscription.js.map +1 -0
- package/dist-server/service/reference/reference-progress.d.ts +38 -0
- package/dist-server/service/reference/reference-progress.js +71 -0
- package/dist-server/service/reference/reference-progress.js.map +1 -0
- package/dist-server/service/reference/reference-resolver.js +210 -43
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/reference/twin-reference.d.ts +32 -0
- package/dist-server/service/reference/twin-reference.js +10 -0
- package/dist-server/service/reference/twin-reference.js.map +1 -1
- package/dist-server/service/twin-control/twin-control-mutation.js +4 -3
- package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
- package/dist-server/service/twin-event/journal-count.d.ts +21 -0
- package/dist-server/service/twin-event/journal-count.js +26 -0
- package/dist-server/service/twin-event/journal-count.js.map +1 -0
- package/dist-server/service/twin-event/twin-event-keys.d.ts +9 -0
- package/dist-server/service/twin-event/twin-event-keys.js +15 -18
- package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
- package/dist-server/service/twin-event/twin-event-type.d.ts +3 -1
- package/dist-server/service/twin-event/twin-event-type.js +18 -1
- package/dist-server/service/twin-event/twin-event-type.js.map +1 -1
- package/dist-server/service/twin-event/twin-event.d.ts +1 -0
- package/dist-server/service/twin-event/twin-event.js +9 -1
- package/dist-server/service/twin-event/twin-event.js.map +1 -1
- package/dist-server/service/twin-forecast/twin-forecast-query.js +2 -1
- package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
- package/dist-server/service/twin-ingest-window/index.d.ts +5 -0
- package/dist-server/service/twin-ingest-window/index.js +10 -0
- package/dist-server/service/twin-ingest-window/index.js.map +1 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-query.d.ts +12 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js +123 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js.map +1 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.d.ts +1 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js +51 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js.map +1 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window.d.ts +16 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window.js +108 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window.js.map +1 -0
- package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
- package/dist-server/service/twin-journal/twin-journal-query.js +58 -5
- package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +14 -0
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +72 -2
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
- package/dist-server/service/twin-metrics/twin-metrics-query.d.ts +1 -0
- package/dist-server/service/twin-metrics/twin-metrics-query.js +78 -3
- package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
- package/dist-server/service/twin-model/axis-journal-evidence.d.ts +2 -0
- package/dist-server/service/twin-model/axis-journal-evidence.js +89 -0
- package/dist-server/service/twin-model/axis-journal-evidence.js.map +1 -0
- package/dist-server/service/twin-model/epcis-coverage.d.ts +1 -1
- package/dist-server/service/twin-model/epcis-coverage.js +19 -7
- package/dist-server/service/twin-model/epcis-coverage.js.map +1 -1
- package/dist-server/service/twin-model/iec61850-coverage.d.ts +1 -1
- package/dist-server/service/twin-model/iec61850-coverage.js +18 -7
- package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -1
- package/dist-server/service/twin-model/isa95-coverage.d.ts +14 -0
- package/dist-server/service/twin-model/isa95-coverage.js +23 -9
- package/dist-server/service/twin-model/isa95-coverage.js.map +1 -1
- package/dist-server/service/twin-model/item-ref.d.ts +24 -0
- package/dist-server/service/twin-model/item-ref.js +88 -0
- package/dist-server/service/twin-model/item-ref.js.map +1 -0
- package/dist-server/service/twin-model/name-index.d.ts +36 -0
- package/dist-server/service/twin-model/name-index.js +116 -0
- package/dist-server/service/twin-model/name-index.js.map +1 -0
- package/dist-server/service/twin-model/project-structure.js +1 -1
- package/dist-server/service/twin-model/project-structure.js.map +1 -1
- package/dist-server/service/twin-model/status-tally.d.ts +9 -0
- package/dist-server/service/twin-model/status-tally.js +37 -0
- package/dist-server/service/twin-model/status-tally.js.map +1 -0
- package/dist-server/service/twin-model/twin-lineage-query.js +40 -12
- package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-item-query.js +38 -39
- package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-query.js +156 -4
- package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-tree-query.js +7 -0
- package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
- package/dist-server/service/twin-readiness/index.d.ts +2 -0
- package/dist-server/service/twin-readiness/index.js +6 -0
- package/dist-server/service/twin-readiness/index.js.map +1 -0
- package/dist-server/service/twin-readiness/twin-readiness-query.d.ts +3 -0
- package/dist-server/service/twin-readiness/twin-readiness-query.js +103 -0
- package/dist-server/service/twin-readiness/twin-readiness-query.js.map +1 -0
- package/dist-server/service/twin-space/twin-space-resolver.d.ts +2 -2
- package/dist-server/service/twin-space/twin-space-resolver.js +5 -4
- package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
- package/dist-shared/entity-delta.d.ts +23 -3
- package/dist-shared/entity-delta.js +12 -8
- package/dist-shared/entity-delta.js.map +1 -1
- package/dist-shared/kpi-broadcast.js +1 -1
- package/dist-shared/kpi-broadcast.js.map +1 -1
- package/dist-shared/touched-items.d.ts +7 -0
- package/dist-shared/touched-items.js +80 -0
- package/dist-shared/touched-items.js.map +1 -0
- package/package.json +7 -7
- package/server/engine/canonical-ingest.ts +92 -7
- package/server/engine/command-routing.ts +1 -1
- package/server/engine/index.ts +4 -0
- package/server/engine/ingest-health.ts +704 -0
- package/server/engine/integration-coverage.ts +147 -0
- package/server/engine/integration-probes.ts +144 -0
- package/server/engine/integration-runner.ts +95 -0
- package/server/engine/integration-target-profile.ts +103 -0
- package/server/engine/kpi-fold.ts +72 -4
- package/server/engine/kpi-query.ts +129 -15
- package/server/engine/live-feed-registry.ts +2 -1
- package/server/engine/load-meter.ts +12 -3
- package/server/engine/log.ts +72 -0
- package/server/engine/loop-lag.ts +120 -0
- package/server/engine/model-gap.ts +168 -0
- package/server/engine/restart-policy.ts +4 -4
- package/server/engine/runtime-key.ts +1 -1
- package/server/engine/stage-path.ts +172 -0
- package/server/engine/twin-engine.ts +1135 -162
- package/server/index.ts +35 -2
- package/server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.ts +54 -0
- package/server/migrations/1786200000000-CarryLiveFeedCursor.ts +53 -0
- package/server/migrations/index.ts +7 -1
- package/server/service/index.ts +11 -0
- package/server/service/reference/discovery-result.ts +1 -1
- package/server/service/reference/index.ts +6 -1
- package/server/service/reference/reference-adapter.ts +197 -5
- package/server/service/reference/reference-assessment.ts +215 -0
- package/server/service/reference/reference-live.ts +123 -11
- package/server/service/reference/reference-master.ts +64 -4
- package/server/service/reference/reference-probe.ts +264 -0
- package/server/service/reference/reference-progress-subscription.ts +73 -0
- package/server/service/reference/reference-progress.ts +95 -0
- package/server/service/reference/reference-resolver.ts +200 -12
- package/server/service/reference/twin-reference.ts +39 -1
- package/server/service/twin-control/twin-control-mutation.ts +4 -3
- package/server/service/twin-event/journal-count.ts +53 -0
- package/server/service/twin-event/twin-event-keys.ts +16 -1
- package/server/service/twin-event/twin-event-type.ts +26 -2
- package/server/service/twin-event/twin-event.ts +7 -0
- package/server/service/twin-forecast/twin-forecast-query.ts +2 -1
- package/server/service/twin-ingest-window/index.ts +7 -0
- package/server/service/twin-ingest-window/twin-ingest-window-query.ts +125 -0
- package/server/service/twin-ingest-window/twin-ingest-window-writer.ts +57 -0
- package/server/service/twin-ingest-window/twin-ingest-window.ts +113 -0
- package/server/service/twin-instance/twin-instance.ts +1 -1
- package/server/service/twin-journal/twin-journal-query.ts +59 -5
- package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +77 -3
- package/server/service/twin-metrics/twin-metrics-query.ts +81 -3
- package/server/service/twin-model/axis-journal-evidence.ts +57 -0
- package/server/service/twin-model/epcis-coverage.ts +2 -8
- package/server/service/twin-model/iec61850-coverage.ts +2 -8
- package/server/service/twin-model/isa95-coverage.ts +22 -9
- package/server/service/twin-model/item-ref.ts +80 -0
- package/server/service/twin-model/name-index.ts +94 -0
- package/server/service/twin-model/project-structure.ts +1 -1
- package/server/service/twin-model/status-tally.ts +37 -0
- package/server/service/twin-model/twin-lineage-query.ts +38 -9
- package/server/service/twin-model/twin-model-item-query.ts +26 -27
- package/server/service/twin-model/twin-model-query.ts +155 -4
- package/server/service/twin-model/twin-model-tree-query.ts +7 -0
- package/server/service/twin-readiness/index.ts +3 -0
- package/server/service/twin-readiness/twin-readiness-query.ts +96 -0
- package/server/service/twin-space/twin-space-resolver.ts +5 -4
- package/shared/entity-delta.ts +31 -7
- package/shared/kpi-broadcast.ts +1 -1
- package/shared/touched-items.ts +73 -0
- package/test/axis-journal-evidence.test.ts +71 -0
- package/test/axis-read.test.ts +101 -1
- package/test/boot-resume.test.ts +92 -26
- package/test/broadcast-cost-baseline.test.ts +200 -0
- package/test/broadcast-period.test.ts +58 -0
- package/test/canonical-ingest-vocabularies.test.ts +36 -1
- package/test/canonical-quantity-door.test.ts +61 -0
- package/test/command-routing.test.ts +1 -1
- package/test/declared-location-types.test.ts +89 -0
- package/test/discovery-result.test.ts +1 -1
- package/test/duration-estimators.test.ts +1 -1
- package/test/entity-delta.test.ts +2 -2
- package/test/ingest-bench.test.ts +3 -3
- package/test/ingest-health-engine.test.ts +248 -0
- package/test/ingest-health-wiring.test.ts +119 -0
- package/test/ingest-health.test.ts +306 -0
- package/test/ingest-history.test.ts +247 -0
- package/test/integration-probes.test.ts +103 -0
- package/test/integration-runner.test.ts +95 -0
- package/test/item-ref.test.ts +78 -0
- package/test/journal-read-discipline.test.ts +177 -0
- package/test/journal-retention.test.ts +133 -0
- package/test/journal-sort-axis.test.ts +142 -0
- package/test/journal-write-door.test.ts +110 -0
- package/test/journal-write-trend.test.ts +115 -0
- package/test/kernel-kind-guard.test.ts +4 -4
- package/test/kpi-fold.test.ts +90 -3
- package/test/kpi-query-bench.test.ts +2 -2
- package/test/lineage-survives-restart.test.ts +17 -1
- package/test/live-cursor-wiring.test.ts +87 -0
- package/test/live-feed-registry.test.ts +9 -1
- package/test/live-kernel-facts.test.ts +7 -7
- package/test/live-mirror-parity.test.ts +6 -0
- package/test/load-meter.test.ts +29 -15
- package/test/local-declarations.test.ts +1 -1
- package/test/log-stamp.test.ts +59 -0
- package/test/loop-lag.test.ts +82 -0
- package/test/model-gap.test.ts +153 -0
- package/test/oee-accumulator.test.ts +4 -0
- package/test/operational-vocabulary.test.ts +4 -4
- package/test/read-failure-visible.test.ts +145 -0
- package/test/reference-grounding.test.ts +70 -0
- package/test/resolve-ts-siblings.mjs +52 -0
- package/test/restart-policy.test.ts +7 -7
- package/test/revision-axis.test.ts +93 -0
- package/test/runtime-key.test.ts +2 -2
- package/test/scale-twin-bench.test.ts +2 -2
- package/test/spec-coverage.test.ts +1 -1
- package/test/stage-path.test.ts +95 -0
- package/test/standard-coverage.test.ts +15 -3
- package/test/status-tally.test.ts +55 -0
- package/test/structure-revision-db.test.ts +4 -0
- package/test/tenant-registry-db.test.ts +1 -1
- package/test/time-range.test.ts +152 -0
- package/test/touched-items.test.ts +61 -0
- package/test/twin-event-keys.test.ts +10 -2
- package/test/twin-model-item-db.test.ts +2 -0
- package/test/warm-start-seam.test.ts +4 -0
- package/test/yield-loop.test.ts +57 -2
- package/tsconfig.shared.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* 이름이 바뀌었다.
|
|
9
9
|
*
|
|
10
10
|
* ── 지키는 규율 둘 (계획서 §6) ──────────────────────────────────────────────
|
|
11
|
-
* ① **모르는 값은
|
|
11
|
+
* ① **모르는 값은 오류를 낸다.** 예전에는 미선언이 조용히 `sim-experiment`(=저널 초기화)로 떨어졌다 — 값이
|
|
12
12
|
* 하나 어긋나는 순간 이력을 지우는 길이다. 기본값을 없앴다.
|
|
13
13
|
* ② **옛 어휘를 번역하지 않는다.** 번역을 두면 옛 값이 살아남고 다시 저장된다. 저장된 값은 한 번
|
|
14
14
|
* 옮기고(데이터 이관), 코드는 새 어휘만 안다 — 다만 무엇으로 옮겨야 하는지는 오류 문장이 말한다.
|
|
@@ -27,21 +27,21 @@ test('셋뿐이다 — 그리고 그 셋을 읽는다', () => {
|
|
|
27
27
|
assert.equal(readRestartPolicy(' resume ', 'test'), 'resume', '공백은 다듬는다')
|
|
28
28
|
})
|
|
29
29
|
|
|
30
|
-
test('미선언은
|
|
30
|
+
test('미선언은 오류를 낸다 — 기본값으로 메우면 저널이 지워지는 길이 열린다', () => {
|
|
31
31
|
for (const raw of [undefined, null, '', ' ', 42, {}]) {
|
|
32
32
|
assert.throws(() => readRestartPolicy(raw, 'twin "x"'), /twin "x": restart policy/)
|
|
33
33
|
}
|
|
34
|
-
/* 무엇을 해야 하는지 문장이 말한다 — 코드를 그대로
|
|
34
|
+
/* 무엇을 해야 하는지 문장이 말한다 — 코드를 그대로 오류를 내지 않는다. */
|
|
35
35
|
try {
|
|
36
36
|
readRestartPolicy(undefined, 'twin "x"')
|
|
37
|
-
assert.fail('
|
|
37
|
+
assert.fail('오류를 내어야 한다')
|
|
38
38
|
} catch (e: any) {
|
|
39
39
|
assert.match(e.message, /resync \| resume \| reset/)
|
|
40
40
|
assert.match(e.message, /no default/)
|
|
41
41
|
}
|
|
42
42
|
})
|
|
43
43
|
|
|
44
|
-
test('옛 어휘도
|
|
44
|
+
test('옛 어휘도 오류를 낸다 — 다만 무엇으로 옮길지 말한다(번역해 주지 않는다)', () => {
|
|
45
45
|
const cases: [string, string][] = [
|
|
46
46
|
['mirror', 'resync'],
|
|
47
47
|
['sim-world', 'resume'],
|
|
@@ -50,7 +50,7 @@ test('옛 어휘도 던진다 — 다만 무엇으로 옮길지 말한다(번역
|
|
|
50
50
|
for (const [old, next] of cases) {
|
|
51
51
|
try {
|
|
52
52
|
readRestartPolicy(old, 'twin "x"')
|
|
53
|
-
assert.fail(`${old} 은
|
|
53
|
+
assert.fail(`${old} 은 오류를 내어야 한다`)
|
|
54
54
|
} catch (e: any) {
|
|
55
55
|
assert.match(e.message, /old vocabulary/)
|
|
56
56
|
assert.match(e.message, new RegExp(`"${next}"`), '옮길 값을 말한다')
|
|
@@ -58,7 +58,7 @@ test('옛 어휘도 던진다 — 다만 무엇으로 옮길지 말한다(번역
|
|
|
58
58
|
}
|
|
59
59
|
})
|
|
60
60
|
|
|
61
|
-
test('묻는 자리도 있다 — 화면·검사는
|
|
61
|
+
test('묻는 자리도 있다 — 화면·검사는 오류를 내지 않고 판별한다', () => {
|
|
62
62
|
assert.equal(isRestartPolicy('resume'), true)
|
|
63
63
|
assert.equal(isRestartPolicy('sim-world'), false)
|
|
64
64
|
assert.equal(isRestartPolicy(undefined), false)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ 리비전은 **두 셈**이고, 둘이 같은 것을 세지 않는다 — 그 사실을 코드가 지키는지 본다.
|
|
3
|
+
*
|
|
4
|
+
* ── 무엇이 어긋나 있었나 (2026-08-23 실측) ──────────────────────────────────
|
|
5
|
+
* 포천 미러의 체크포인트가 뜻이 다른 두 수를 나란히 적었다.
|
|
6
|
+
*
|
|
7
|
+
* 바깥 revision 12,972 저널에 적힌 줄 번호
|
|
8
|
+
* state.revision 6,478 커널이 처리한 봉투 수
|
|
9
|
+
*
|
|
10
|
+
* 차이 6,494 는 그 트윈을 **다시 세운 시각**(09:11)에 저널이 이미 갖고 있던 줄 수와 정확히 같았다
|
|
11
|
+
* (줄 6494 = 08:23:59 · 줄 6495 = 09:11:13). 산수가 맞았으니 결함은 「수가 틀렸다」가 아니다.
|
|
12
|
+
*
|
|
13
|
+
* 결함은 **비대칭**이었다. 재기동 경로(`start` → `resumeRevision`)는 커널에 저널 머리를 알려 주는데
|
|
14
|
+
* 미러를 세우는 경로(`startLive`)는 알려 주지 않았다. 그래서 같은 저장물을 읽는 사람이 같은 이름의 두
|
|
15
|
+
* 수를 같은 축으로 견주게 되고, 실제로 그렇게 읽혔다(반나절이 그 오독에서 나갔다).
|
|
16
|
+
*
|
|
17
|
+
* ── 왜 알려 주어야 하나 ─────────────────────────────────────────────────────
|
|
18
|
+
* 커널의 계약이 그 이유를 적어 두었다(`resumeRevision`): 「저널이 비어 있지 않은데 0부터 다시 세면 같은
|
|
19
|
+
* 트윈에 같은 리비전이 둘 생기고, 재생 순서가 뒤섞이며, 시간여행이 엉뚱한 시점을 답한다 — 오류는 안 난다.」
|
|
20
|
+
* 조용히 깨지는 종류이므로 시험이 지킨다.
|
|
21
|
+
*
|
|
22
|
+
* ── 이 시험이 무엇을 재는가 ─────────────────────────────────────────────────
|
|
23
|
+
* DB 도 커널도 띄우지 않는다. 이 규율은 **어느 경로가 무엇을 부르나**에 있고 그것은 소스에서 확인된다.
|
|
24
|
+
*/
|
|
25
|
+
import { test } from 'node:test'
|
|
26
|
+
import assert from 'node:assert/strict'
|
|
27
|
+
import { readFileSync } from 'node:fs'
|
|
28
|
+
import { join } from 'node:path'
|
|
29
|
+
|
|
30
|
+
const ENGINE = join(import.meta.dirname, '..', 'server', 'engine', 'twin-engine.ts')
|
|
31
|
+
const src = readFileSync(ENGINE, 'utf8')
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 그 함수 본문만 — 다른 경로의 호출이 섞여 판정이 흐려지지 않게.
|
|
35
|
+
*
|
|
36
|
+
* 끝은 **다음 멤버 선언**으로 자른다. 글자 수로 자르면 주석이 늘어난 날 조용히 짧아져서, 있는 것을
|
|
37
|
+
* 없다고 말한다(거짓 빨강을 내는 가드는 없는 것보다 나쁘다).
|
|
38
|
+
*/
|
|
39
|
+
function bodyOf(signature: string): string {
|
|
40
|
+
const at = src.indexOf(signature)
|
|
41
|
+
assert.ok(at > 0, `${signature} 이 있어야 한다`)
|
|
42
|
+
const rest = src.slice(at + signature.length)
|
|
43
|
+
const end = rest.search(/\n (?:static|private static|\/\*\*)/)
|
|
44
|
+
const body = end === -1 ? rest : rest.slice(0, end)
|
|
45
|
+
/* 잘라낸 본문이 비면 판정이 성립하지 않는다 — 그것을 통과로 읽지 않는다. */
|
|
46
|
+
assert.ok(body.length > 200, `${signature} 본문을 잘라내지 못했다(${body.length}자)`)
|
|
47
|
+
return body
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
test('미러를 세우는 경로도 커널에 저널 머리를 알려 준다', () => {
|
|
51
|
+
const body = bodyOf('static startLive(')
|
|
52
|
+
/* 저널 머리를 읽는다 — 이 조회는 예전부터 있었다(inst.revision 시드용). */
|
|
53
|
+
assert.match(body, /order: \{ revision: 'DESC' \}/, '저널 머리를 읽어야 한다')
|
|
54
|
+
/* 그리고 **커널에도** 알려 준다 — 이 한 줄이 없어서 두 수가 갈렸다. */
|
|
55
|
+
assert.match(body, /resumeRevision\(head\)/, '커널에도 그 번호를 알려 준다')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('재기동 경로는 예전부터 알려 주고 있었다 — 두 경로가 같아졌는지 본다', () => {
|
|
59
|
+
/*
|
|
60
|
+
* 이 단정은 「고쳤다」가 아니라 **두 경로가 대칭인지**를 지킨다. 한쪽만 남으면 같은 결함이 다른
|
|
61
|
+
* 문으로 되살아난다.
|
|
62
|
+
*/
|
|
63
|
+
const body = bodyOf('static start(')
|
|
64
|
+
assert.match(body, /resumeRevision\(resumeFrom\)/, '재기동 경로도 알려 준다')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('저널이 비어 있으면 알려 주지 않는다 — 새 트윈은 0 부터 세는 것이 맞다', () => {
|
|
68
|
+
const body = bodyOf('static startLive(')
|
|
69
|
+
assert.match(body, /if \(!head \|\| typeof/, '머리가 0 이면 아무 일도 하지 않는다')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('뒤로 되돌리려는 거절을 삼키지 않는다', () => {
|
|
73
|
+
/*
|
|
74
|
+
* 조회가 비동기라 그 사이에 이벤트가 들어와 있을 수 있다. 그때 커널은 뒤로 가기를 거절한다(겹치는
|
|
75
|
+
* 번호를 막는 그 판정이다). 그 거절을 조용히 버리면 「왜 두 수가 아직 다르지」를 답할 계기가 없다.
|
|
76
|
+
*/
|
|
77
|
+
const body = bodyOf('static startLive(')
|
|
78
|
+
assert.match(body, /catch \(err[^)]*\)[\s\S]{0,200}twinWarn/, '거절을 로그에 남긴다')
|
|
79
|
+
assert.match(body, /is behind the kernel/, '무엇이 어긋났는지 말한다')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('체크포인트는 두 수를 나란히 적는다 — 그 사실 자체는 옳다', () => {
|
|
83
|
+
/*
|
|
84
|
+
* 바깥은 저널 줄 번호(이어 쓸 자리), 안쪽은 커널이 접은 봉투 수다. **둘을 하나로 합치지 않는다** —
|
|
85
|
+
* 합치면 「어디까지 적었나」와 「어디까지 반영했나」를 구별할 수 없고, 그 구별이 미러의 정직성이다.
|
|
86
|
+
* 두 경로가 대칭이면 값이 같아지지만, **자리는 둘이어야 한다.**
|
|
87
|
+
*/
|
|
88
|
+
const at = src.indexOf('static async persistSnapshot')
|
|
89
|
+
assert.ok(at > 0, 'persistSnapshot 이 있어야 한다')
|
|
90
|
+
const body = src.slice(at, at + 1200)
|
|
91
|
+
assert.match(body, /inst\.revision \?\? state\.revision/, '바깥은 저널 줄 번호를 먼저 쓴다')
|
|
92
|
+
assert.match(body, /\{ revision, state/, '두 수를 나란히 싣는다')
|
|
93
|
+
})
|
package/test/runtime-key.test.ts
CHANGED
|
@@ -39,7 +39,7 @@ test('인스턴스 id 안의 구분자를 보존한다 — 첫 구분자에서
|
|
|
39
39
|
assert.equal(instanceId, weird)
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
-
test('도메인 id 에 구분자가 있으면
|
|
42
|
+
test('도메인 id 에 구분자가 있으면 오류를 낸다 — 조용히 모호한 키를 만들지 않는다', () => {
|
|
43
43
|
/* 조용히 만들면 그게 곧 이 결함의 재발이다(어느 테넌트인지 알 수 없는 키). */
|
|
44
44
|
assert.throws(() => runtimeKey(`a${RUNTIME_KEY_SEP}b`, 'x'), /ambiguous/)
|
|
45
45
|
})
|
|
@@ -49,7 +49,7 @@ test('빈 값은 키가 될 수 없다 — 테넌트 없는 런타임은 없다'
|
|
|
49
49
|
assert.throws(() => runtimeKey(D1, ''), /instanceId is required/)
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
-
test('키가 아닌 것을 되돌리려 하면
|
|
52
|
+
test('키가 아닌 것을 되돌리려 하면 오류를 낸다', () => {
|
|
53
53
|
for (const bad of ['', 'no-separator', `${RUNTIME_KEY_SEP}leading`, `trailing${RUNTIME_KEY_SEP}`]) {
|
|
54
54
|
assert.throws(() => parseRuntimeKey(bad), /is not a runtime key/, `입력: "${bad}"`)
|
|
55
55
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* 스케일 테스트 트윈 벤치 — 미국 공장(WMS+MES+YMS) 형태를 대규모로 스케일업, 5종 동기 엔티티 전부 부하.
|
|
3
3
|
* 동기 엔티티 = locations·items(재고)·equipment·tasks·orders (노드/무버뿐이 아님). area 는 계층(건물>층>존).
|
|
4
|
-
* 측정(가정 아닌 실측): masterToTwin(구조 생성)·apply(혼합 이벤트 처리량)·snapshot(전체 투영)·buildEntityDeltas(
|
|
4
|
+
* 측정(가정 아닌 실측): masterToTwin(구조 생성)·apply(혼합 이벤트 처리량)·snapshot(전체 투영)·buildEntityDeltas(브로드캐스팅, 't1')·heap.
|
|
5
5
|
* 순수 커널+호스트 매핑(shell·DB 무관) → node:test. area 는 공간층(projector State 밖)이라 처리량엔 무영향 — 구조 생성 비용에만.
|
|
6
6
|
*/
|
|
7
7
|
import { test } from 'node:test'
|
|
@@ -87,7 +87,7 @@ for (const [name, tier] of Object.entries(TIERS)) {
|
|
|
87
87
|
const tSnap = (performance.now() - t) / iters
|
|
88
88
|
console.log(` [${name}] snapshot: ${tSnap.toFixed(2)}ms/call (items ${s.items.length.toLocaleString()}·locations ${s.locations.length.toLocaleString()}·equipment ${s.equipment.length}·tasks ${s.tasks.length}·orders ${s.orders.length}) → 최대 ${Math.round(1000 / tSnap).toLocaleString()} snap/s`)
|
|
89
89
|
|
|
90
|
-
// 4)
|
|
90
|
+
// 4) 브로드캐스팅 payload(buildEntityDeltas)
|
|
91
91
|
t = performance.now()
|
|
92
92
|
for (let k = 0; k < iters; k++) buildEntityDeltas(s, 't1')
|
|
93
93
|
const tDelta = (performance.now() - t) / iters
|
|
@@ -61,7 +61,7 @@ test('커널이 보고 API 를 갖지 않으면 undefined — "전부 기본값"
|
|
|
61
61
|
assert.equal(readSpecCoverage(undefined), undefined)
|
|
62
62
|
})
|
|
63
63
|
|
|
64
|
-
test('커널 보고가
|
|
64
|
+
test('커널 보고가 오류를 내어도 예측을 깨지 않는다', () => {
|
|
65
65
|
assert.equal(readSpecCoverage({ specCoverage: () => { throw new Error('boom') } }), undefined)
|
|
66
66
|
})
|
|
67
67
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ 단계 판정 — **체크리스트가 아니라 증거로 센다.**
|
|
3
|
+
*
|
|
4
|
+
* 이 테스트가 지키는 것:
|
|
5
|
+
* ① **「모른다」는 달성이 아니다.** 계측되지 않은 신호(`null`)를 0 으로 대체하면 「반출 0건」과
|
|
6
|
+
* 「반출을 세지 않았다」가 같아진다. 배선하지 않은 계측으로 단계를 주장할 길을 남기지 않는다.
|
|
7
|
+
* ② **반출을 세었는데 없으면 라이브가 아니다.** 되풀어 읽는 스냅샷은 원리적으로 반출을 못 낸다 —
|
|
8
|
+
* 이 판별식이 폴링을 「실시간」이라 부르는 것을 막는다.
|
|
9
|
+
* ③ **선언이 비면 예측이 아니다.** 커널 기본값 위에 선 숫자도 화면에서는 예측처럼 보인다.
|
|
10
|
+
* ④ **앞을 건너뛸 수 없다.** 뒤 단계는 자기 조건을 따지지 않고 `prerequisite` 으로 막는다.
|
|
11
|
+
*/
|
|
12
|
+
import { test } from 'node:test'
|
|
13
|
+
import assert from 'node:assert/strict'
|
|
14
|
+
|
|
15
|
+
import { assessStagePath, forbiddenClaims, type StageEvidence } from '../server/engine/stage-path.ts'
|
|
16
|
+
|
|
17
|
+
/** 전부 달성한 증거 — 각 시험은 여기서 한 축만 무너뜨린다(무엇이 판정을 바꿨는지 분명해진다). */
|
|
18
|
+
const full = (): StageEvidence => ({
|
|
19
|
+
locationCount: 12,
|
|
20
|
+
itemCount: 340,
|
|
21
|
+
masterWarnings: 3,
|
|
22
|
+
liveIngestTotal: 1200,
|
|
23
|
+
removalsObserved: 17,
|
|
24
|
+
forecastRan: true,
|
|
25
|
+
specCoverage: { declared: 6, total: 8 },
|
|
26
|
+
actuateCapability: true,
|
|
27
|
+
correlatedEffectObserved: 2
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('전부 증거가 있으면 폐루프까지 도달하고 금지 표현이 없다', () => {
|
|
31
|
+
const p = assessStagePath(full())
|
|
32
|
+
assert.deepEqual(p.stages.map(s => s.achieved), [true, true, true, true])
|
|
33
|
+
assert.equal(p.reached, 'closed-loop')
|
|
34
|
+
assert.equal(p.next, null)
|
|
35
|
+
assert.deepEqual(forbiddenClaims(p), [])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('못 옮긴 값을 세지 않았으면 구조도 달성이 아니다 — 0 이라고 우기지 않는다', () => {
|
|
39
|
+
const p = assessStagePath({ ...full(), masterWarnings: null })
|
|
40
|
+
const s1 = p.stages[0]
|
|
41
|
+
assert.equal(s1.achieved, false)
|
|
42
|
+
assert.deepEqual(s1.unmeasured, ['master-warnings'])
|
|
43
|
+
assert.deepEqual(s1.blockedBy, [], '자리와 물품은 있으므로 막힌 것이 아니라 모르는 것이다')
|
|
44
|
+
assert.equal(p.reached, null, '첫 칸을 못 밟았으면 서 있는 칸이 없다')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('못 옮긴 값이 3건이어도 구조는 달성이다 — 요구는 「없다」가 아니라 「말할 수 있다」', () => {
|
|
48
|
+
assert.equal(assessStagePath({ ...full(), masterWarnings: 3 }).stages[0].achieved, true)
|
|
49
|
+
assert.equal(assessStagePath({ ...full(), masterWarnings: 0 }).stages[0].achieved, true)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('반출을 세었는데 0 이면 라이브가 아니다 — 스냅샷 되풀어 읽기의 징표', () => {
|
|
53
|
+
const p = assessStagePath({ ...full(), removalsObserved: 0 })
|
|
54
|
+
assert.equal(p.stages[1].achieved, false)
|
|
55
|
+
assert.deepEqual(p.stages[1].blockedBy, ['no-removal-observed'])
|
|
56
|
+
assert.equal(p.reached, 'structure')
|
|
57
|
+
assert.ok(forbiddenClaims(p).includes('claim.realtime'), '실시간이라고 말할 수 없다')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('반출이 계측되지 않으면 「없다」가 아니라 「모른다」로 막힌다', () => {
|
|
61
|
+
const p = assessStagePath({ ...full(), removalsObserved: null })
|
|
62
|
+
assert.deepEqual(p.stages[1].unmeasured, ['removals'])
|
|
63
|
+
assert.deepEqual(p.stages[1].blockedBy, [], '인제스트는 있으므로 막힌 것이 아니다')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('선언이 비면 예측이 아니다 — 기본값 위에 선 숫자도 숫자로 보인다', () => {
|
|
67
|
+
const p = assessStagePath({ ...full(), specCoverage: { declared: 0, total: 8 } })
|
|
68
|
+
assert.equal(p.stages[2].achieved, false)
|
|
69
|
+
assert.deepEqual(p.stages[2].blockedBy, ['declarations-empty'])
|
|
70
|
+
assert.equal(p.reached, 'live')
|
|
71
|
+
assert.ok(forbiddenClaims(p).includes('claim.prediction'))
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('접수만으로는 폐루프가 아니다 — 효과가 관측으로 돌아와야 닫힌다', () => {
|
|
75
|
+
const p = assessStagePath({ ...full(), correlatedEffectObserved: 0 })
|
|
76
|
+
assert.deepEqual(p.stages[3].blockedBy, ['effect-not-observed'])
|
|
77
|
+
assert.equal(p.reached, 'forecast')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('앞이 막히면 뒤는 자기 조건을 따지지 않는다 — 순서를 건너뛸 수 없다', () => {
|
|
81
|
+
const p = assessStagePath({ ...full(), locationCount: 0, itemCount: 0 })
|
|
82
|
+
assert.deepEqual(p.stages[0].blockedBy, ['no-locations', 'no-items'])
|
|
83
|
+
for (const later of p.stages.slice(1)) assert.deepEqual(later.blockedBy, ['prerequisite'])
|
|
84
|
+
assert.deepEqual(p.next, { stage: 'structure', blockedBy: ['no-locations', 'no-items'], unmeasured: [] })
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('다음 걸음은 목록이 아니라 하나다', () => {
|
|
88
|
+
const p = assessStagePath({ ...full(), removalsObserved: 0, specCoverage: null, actuateCapability: false })
|
|
89
|
+
assert.equal(p.next?.stage, 'live', '뒤쪽 결손을 함께 들이밀지 않는다')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('비직렬 수량 물품만 있어도 구조는 달성이다 — 식품·화학은 개수로 오지 않는다', () => {
|
|
93
|
+
/* itemCount 는 「자리에 놓인 물품 수」이고, 비직렬 수량 물품(subLot)도 한 줄로 센다. */
|
|
94
|
+
assert.equal(assessStagePath({ ...full(), itemCount: 1 }).stages[0].achieved, true)
|
|
95
|
+
})
|
|
@@ -11,9 +11,21 @@
|
|
|
11
11
|
import { test } from 'node:test'
|
|
12
12
|
import assert from 'node:assert/strict'
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
/*
|
|
15
|
+
* **빌드 산출물을 부른다** — 소스를 직접 실행하지 않는다.
|
|
16
|
+
*
|
|
17
|
+
* 이 모듈들은 서로를 값으로 참조한다(축 어긋남 판정을 한 곳에 두었다). 그 참조는 계약대로 `.js` 로
|
|
18
|
+
* 적히고, 타입만 지우고 소스를 실행하는 방식은 그 경로를 `.ts` 로 바꿔 주지 않는다. 그래서 소스를
|
|
19
|
+
* 부르면 「모듈을 찾을 수 없다」로 선다.
|
|
20
|
+
*
|
|
21
|
+
* **소스를 고친 뒤에는 `npm run build:server` 를 먼저 돌린다.**
|
|
22
|
+
*/
|
|
23
|
+
import { createRequire } from 'node:module'
|
|
24
|
+
|
|
25
|
+
const req = createRequire(import.meta.url)
|
|
26
|
+
const { standardsForKind } = req('../dist-server/service/twin-model/standard-coverage.js')
|
|
27
|
+
const { iec61850Coverage } = req('../dist-server/service/twin-model/iec61850-coverage.js')
|
|
28
|
+
const { epcisCoverage } = req('../dist-server/service/twin-model/epcis-coverage.js')
|
|
17
29
|
import { readFileSync } from 'node:fs'
|
|
18
30
|
import { fileURLToPath } from 'node:url'
|
|
19
31
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 상태별 내림 — **수 하나가 뜻을 바꾸지 않게** (2026-08-23).
|
|
3
|
+
*
|
|
4
|
+
* 커널이 관측 흡수에서 완료된 작업을 떨어뜨리지 않게 고쳐진 뒤, 개념 지도의 작업 카드가 0 에서 2,820 이
|
|
5
|
+
* 되었다. 카드는 상태의 인구조사이므로 그 수가 맞지만, 사람은 「지금 할 일 2,820건」으로 읽는다.
|
|
6
|
+
* 수를 줄여 맞는 척하게 만들지 않고, 무엇으로 이루어진 수인지 함께 낸다.
|
|
7
|
+
*
|
|
8
|
+
* 이 시험이 지키는 것:
|
|
9
|
+
* ① 상태별로 갈라 세고, **많은 것부터** 낸다(카드가 한 줄로 보이므로 순서가 뜻을 만든다).
|
|
10
|
+
* ② 낱말은 커널의 것 그대로 — 여기서 묶거나 고쳐 부르지 않는다.
|
|
11
|
+
* ③ 상태를 말하지 않는 축은 **필드를 만들지 않는다**(빈 표는 「상태 없음」과 「못 읽음」을 뭉갠다).
|
|
12
|
+
*/
|
|
13
|
+
import { test } from 'node:test'
|
|
14
|
+
import assert from 'node:assert/strict'
|
|
15
|
+
|
|
16
|
+
import { statusTally } from '../server/service/twin-model/status-tally.ts'
|
|
17
|
+
|
|
18
|
+
test('상태별로 갈라 세고 많은 것부터 낸다', () => {
|
|
19
|
+
const r = statusTally([
|
|
20
|
+
{ id: 'a', status: 'completed' },
|
|
21
|
+
{ id: 'b', status: 'in-progress' },
|
|
22
|
+
{ id: 'c', status: 'completed' },
|
|
23
|
+
{ id: 'd', status: 'completed' }
|
|
24
|
+
])
|
|
25
|
+
assert.deepEqual(r.statusCounts, [
|
|
26
|
+
{ status: 'completed', count: 3 },
|
|
27
|
+
{ status: 'in-progress', count: 1 }
|
|
28
|
+
])
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('낱말은 커널의 것 그대로 — 묶거나 고쳐 부르지 않는다', () => {
|
|
32
|
+
const r = statusTally([{ status: 'assigned' }, { status: 'created' }])
|
|
33
|
+
assert.deepEqual(
|
|
34
|
+
r.statusCounts?.map(s => s.status).sort(),
|
|
35
|
+
['assigned', 'created'],
|
|
36
|
+
'「대기」처럼 앱의 낱말로 바꾸면 커널이 늘린 낱말을 화면이 조용히 잘못 부른다'
|
|
37
|
+
)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('상태를 말하지 않는 축은 필드를 만들지 않는다', () => {
|
|
41
|
+
assert.deepEqual(statusTally([{ id: 'loc-1' }, { id: 'loc-2' }]), {}, '빈 표를 내지 않는다')
|
|
42
|
+
assert.deepEqual(statusTally([]), {})
|
|
43
|
+
assert.deepEqual(statusTally(null), {})
|
|
44
|
+
assert.deepEqual(statusTally({ id: 'not-a-list' }), {}, '목록이 아니면 셀 것이 없다')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('상태를 말하는 것만 센다 — 없는 것을 「모름」이라는 갈래로 만들지 않는다', () => {
|
|
48
|
+
const r = statusTally([{ status: 'completed' }, { id: 'x' }, { status: '' }, { status: 7 }])
|
|
49
|
+
assert.deepEqual(r.statusCounts, [{ status: 'completed', count: 1 }])
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('한 갈래뿐이면 그대로 한 갈래다 — 화면이 그때는 내림을 감춘다', () => {
|
|
53
|
+
const r = statusTally([{ status: 'completed' }, { status: 'completed' }])
|
|
54
|
+
assert.deepEqual(r.statusCounts, [{ status: 'completed', count: 2 }], '판단은 화면이 하고, 사실은 그대로 낸다')
|
|
55
|
+
})
|
|
@@ -41,6 +41,10 @@ const GTIN = 'urn:epc:idpat:sgtin:0614141.012345.*'
|
|
|
41
41
|
/** 저장 자리 `bins` 개 + 지게차 `forklifts` 대 — 구조를 다이얼한다. */
|
|
42
42
|
const boardWith = (bins: number, forklifts: number): any => ({
|
|
43
43
|
spaceId: 'struct-space',
|
|
44
|
+
/* 이 픽스처가 식별자를 만드는 이름공간 — **픽스처마다 다르게** 둔다(같은 접두 아래에서 서로 다른
|
|
45
|
+
시험의 번호가 부딪치면 표준의 유일성 요구를 깬다, CBV 2.0 §8.2.4). 커널은 선언 없이 팔레트·문서를
|
|
46
|
+
만들지 않는다(0.7.45 에서 날조 프리픽스를 지웠다). */
|
|
47
|
+
identity: { namespaces: ['https://twin.test/structure-revision'] },
|
|
44
48
|
utcOffsetMinutes: 0,
|
|
45
49
|
locations: [
|
|
46
50
|
{ id: 'dock-recv', type: 'dock', capacity: 100 },
|
|
@@ -67,7 +67,7 @@ before(async () => {
|
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
69
|
* **스펙마다 반드시 멈춘다.** 남기면 두 가지가 따라온다: 틱 타이머 때문에 러너가 끝나지 않고, 틱이
|
|
70
|
-
* 뒤늦게
|
|
70
|
+
* 뒤늦게 오류를 내는 DB 쓰기가 teardown 이후에 도착해 `SQLITE_MISUSE` 로 스위트를 오염시킨다.
|
|
71
71
|
*/
|
|
72
72
|
afterEach(async () => {
|
|
73
73
|
await TwinEngine.stop(alpha, SHARED_ID).catch(() => {})
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ 시각축은 **두 수만 묻는다** — 저널을 다 읽어 오지 않는다.
|
|
3
|
+
*
|
|
4
|
+
* ── 무엇을 겪었나 (2026-08-20 실측) ─────────────────────────────────────────
|
|
5
|
+
* `TwinEngine.timeRange` 는 공간의 인스턴스마다 `find()` 로 **행을 전부 엔티티로 하이드레이션**한 뒤
|
|
6
|
+
* JS 에서 min/max 를 골랐다. 그런데 이 함수는 화면 상단의 컨텍스트 띠가 **4초마다** 부른다. 개발
|
|
7
|
+
* 서버에서 `order-check` 한 트윈이 275,882행 · `payload` 131MB 였다 — 4초마다 그 JSON 을 전부 파싱했다.
|
|
8
|
+
*
|
|
9
|
+
* 결과: 프로세스 CPU 81~152%, 아무 일도 하지 않는 질의가 8~14초. JS 프로파일 상위가 TypeORM 의
|
|
10
|
+
* `RelationIdLoader`·`RawSqlResultsToEntityTransformer`·`stringToSimpleJson` 이고 GC 가 16.7% 였다.
|
|
11
|
+
* 트윈 틱(계기가 16%)도 저널 쓰기도 아니라 **이 읽기**였다.
|
|
12
|
+
*
|
|
13
|
+
* ── 이 시험이 지키는 것 ─────────────────────────────────────────────────────
|
|
14
|
+
* ① 답이 맞다(공간의 모든 트윈에 걸친 min/max, 남의 공간은 안 섞인다)
|
|
15
|
+
* ② 「없음」을 0 으로 떨어뜨리지 않는다(시각축이 1970년에서 시작하면 스크러버가 못 쓰게 된다)
|
|
16
|
+
* ③ **행을 실어 오지 않는다** — 고친 방식이 되돌아가면(다시 `find()`) 답은 그대로 맞고 느려지기만
|
|
17
|
+
* 하므로, 결과만 보는 시험으로는 회귀를 잡을 수 없다. 그래서 소스를 함께 본다.
|
|
18
|
+
*/
|
|
19
|
+
import { test, before, after } from 'node:test'
|
|
20
|
+
import assert from 'node:assert/strict'
|
|
21
|
+
import { createRequire } from 'node:module'
|
|
22
|
+
import { readFileSync } from 'node:fs'
|
|
23
|
+
import type { DataSource } from 'typeorm'
|
|
24
|
+
|
|
25
|
+
const req = createRequire(import.meta.url)
|
|
26
|
+
const { NamingStrategy, addDataSource, entities: shellEntities, Domain } = req('@things-factory/shell')
|
|
27
|
+
const { entities: authEntities } = req('@things-factory/auth-base')
|
|
28
|
+
const { entities: twinEntities } = req('../dist-server/service/index.js')
|
|
29
|
+
const { TwinEngine } = req('../dist-server/engine/index.js')
|
|
30
|
+
const { TwinEvent } = req('../dist-server/service/twin-event/twin-event.js')
|
|
31
|
+
const { TwinInstance } = req('../dist-server/service/twin-instance/twin-instance.js')
|
|
32
|
+
const { DataSource: TypeOrmDataSource } = req('typeorm')
|
|
33
|
+
|
|
34
|
+
let ds: DataSource
|
|
35
|
+
let domainId: string
|
|
36
|
+
|
|
37
|
+
const SPACE = 'space-yard'
|
|
38
|
+
const OTHER_SPACE = 'space-elsewhere'
|
|
39
|
+
|
|
40
|
+
/** 저널 행 하나 — 시각축이 보는 것은 `eventTime` 뿐이다. */
|
|
41
|
+
async function event(instanceId: string, eventTime: string | null, revision: number): Promise<void> {
|
|
42
|
+
await ds.getRepository(TwinEvent).save(
|
|
43
|
+
ds.getRepository(TwinEvent).create({
|
|
44
|
+
domain: { id: domainId } as any,
|
|
45
|
+
instanceId,
|
|
46
|
+
eventType: 'test.event',
|
|
47
|
+
revision,
|
|
48
|
+
eventTime: eventTime === null ? null : new Date(eventTime),
|
|
49
|
+
payload: { note: 'x' }
|
|
50
|
+
} as any)
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function instance(instanceId: string, spaceId: string): Promise<void> {
|
|
55
|
+
await ds.getRepository(TwinInstance).save(
|
|
56
|
+
ds.getRepository(TwinInstance).create({
|
|
57
|
+
domain: { id: domainId } as any,
|
|
58
|
+
instanceId,
|
|
59
|
+
spaceId,
|
|
60
|
+
kind: 'yms',
|
|
61
|
+
status: 'stopped',
|
|
62
|
+
restartPolicy: 'reset',
|
|
63
|
+
purpose: 'operational'
|
|
64
|
+
} as any)
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
before(async () => {
|
|
69
|
+
ds = new TypeOrmDataSource({
|
|
70
|
+
type: 'sqlite',
|
|
71
|
+
database: ':memory:',
|
|
72
|
+
synchronize: true,
|
|
73
|
+
dropSchema: true,
|
|
74
|
+
namingStrategy: new NamingStrategy(),
|
|
75
|
+
entities: [...shellEntities, ...authEntities, ...twinEntities],
|
|
76
|
+
logging: false
|
|
77
|
+
})
|
|
78
|
+
await ds.initialize()
|
|
79
|
+
addDataSource('default', ds)
|
|
80
|
+
|
|
81
|
+
const d = await ds.getRepository(Domain).save({ name: 'range-test', subdomain: 'range-test' } as any)
|
|
82
|
+
domainId = d.id
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
after(async () => {
|
|
86
|
+
await ds?.destroy()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('공간의 여러 트윈에 걸쳐 가장 이른 것과 가장 늦은 것을 낸다', async () => {
|
|
90
|
+
await instance('yard-a', SPACE)
|
|
91
|
+
await instance('yard-b', SPACE)
|
|
92
|
+
|
|
93
|
+
/* 가장 이른 것은 b 에, 가장 늦은 것은 a 에 있다 — 한 트윈만 보면 답이 틀린다. */
|
|
94
|
+
await event('yard-a', '2026-03-01T00:00:00.000Z', 1)
|
|
95
|
+
await event('yard-a', '2026-06-30T12:00:00.000Z', 2)
|
|
96
|
+
await event('yard-b', '2026-01-15T09:00:00.000Z', 1)
|
|
97
|
+
|
|
98
|
+
const r = await TwinEngine.timeRange(domainId, SPACE)
|
|
99
|
+
assert.equal(r.minTime, '2026-01-15T09:00:00.000Z')
|
|
100
|
+
assert.equal(r.maxTime, '2026-06-30T12:00:00.000Z')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('다른 공간의 저널은 섞이지 않는다 — 시각축은 그 현장의 것이다', async () => {
|
|
104
|
+
await instance('far-c', OTHER_SPACE)
|
|
105
|
+
await event('far-c', '2020-01-01T00:00:00.000Z', 1)
|
|
106
|
+
await event('far-c', '2030-01-01T00:00:00.000Z', 2)
|
|
107
|
+
|
|
108
|
+
const r = await TwinEngine.timeRange(domainId, SPACE)
|
|
109
|
+
assert.equal(r.minTime, '2026-01-15T09:00:00.000Z', '2020년이 새어 들어오지 않는다')
|
|
110
|
+
assert.equal(r.maxTime, '2026-06-30T12:00:00.000Z', '2030년도 그렇다')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('시각이 빈 행이 시각축을 1970년으로 끌어내리지 않는다', async () => {
|
|
114
|
+
/* 옛 행에는 `eventTime` 이 없을 수 있다. 그것을 0 으로 읽으면 스크러버가 56년을 그린다. */
|
|
115
|
+
await event('yard-a', null, 3)
|
|
116
|
+
|
|
117
|
+
const r = await TwinEngine.timeRange(domainId, SPACE)
|
|
118
|
+
assert.equal(r.minTime, '2026-01-15T09:00:00.000Z', '빈 시각은 「없음」이고 0 이 아니다')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('이벤트가 없으면 「없음」이다 — 0 이나 지금이 아니다', async () => {
|
|
122
|
+
await instance('empty-d', 'space-empty')
|
|
123
|
+
|
|
124
|
+
const r = await TwinEngine.timeRange(domainId, 'space-empty')
|
|
125
|
+
assert.equal(r.minTime, null)
|
|
126
|
+
assert.equal(r.maxTime, null)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('그 공간에 트윈이 없으면 「없음」이다', async () => {
|
|
130
|
+
const r = await TwinEngine.timeRange(domainId, 'space-nobody')
|
|
131
|
+
assert.equal(r.minTime, null)
|
|
132
|
+
assert.equal(r.maxTime, null)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('두 수를 구하려고 행을 실어 오지 않는다 — 되돌아가면 답은 맞고 느려지기만 한다', () => {
|
|
136
|
+
/*
|
|
137
|
+
* 결과만 보는 시험으로는 이 회귀를 잡을 수 없다(`find()` 로 돌려도 min/max 는 맞다). 그래서 함수의
|
|
138
|
+
* 소스를 본다: 집계(MIN/MAX)를 쓰고, 저널을 `find()` 로 긁지 않는다.
|
|
139
|
+
*/
|
|
140
|
+
const src = readFileSync(new URL('../server/engine/twin-engine.ts', import.meta.url).pathname, 'utf-8')
|
|
141
|
+
const at = src.indexOf('static async timeRange(')
|
|
142
|
+
assert.ok(at > 0, 'timeRange 를 찾지 못했다 — 이름이 바뀌면 이 검사도 함께 옮긴다')
|
|
143
|
+
|
|
144
|
+
/* 함수 본문만 떼어 본다 — 다음 멤버 선언까지. */
|
|
145
|
+
const body = src.slice(at, src.indexOf('\n /** 정지 훅', at) > 0 ? src.indexOf('\n /** 정지 훅', at) : at + 4000)
|
|
146
|
+
|
|
147
|
+
assert.ok(body.includes("MIN(e.eventTime)") && body.includes("MAX(e.eventTime)"), '집계는 DB 가 한다')
|
|
148
|
+
assert.ok(
|
|
149
|
+
!/getRepository\(TwinEvent\)\s*\.?\s*\n?\s*\.find\(/.test(body),
|
|
150
|
+
'저널을 행 단위로 실어 오면 안 된다 — 275,882행 · payload 131MB 를 4초마다 파싱했던 자리다'
|
|
151
|
+
)
|
|
152
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 사건이 **어느 물품을 건드렸나** — 브로드캐스팅이 바뀐 것만 만들 수 있게 (2026-08-21).
|
|
3
|
+
*
|
|
4
|
+
* 이 판정이 틀리면 **화면이 낡는다.** 그래서 지키는 것을 값으로 못 박는다:
|
|
5
|
+
* ① 개체를 말한 자리를 전부 읽는다 — 관측·집약(부모/자식)·변환(입력/출력).
|
|
6
|
+
* ② 부류 수량만 말한 사건은 「모른다」다 — 그 부류의 어느 개체인지 말하지 않았다.
|
|
7
|
+
* ③ 물품을 만지지 않는다고 **선언된** 사건은 빈 범위다(작업·설비·인원·자산·오더·품질·확인·에너지).
|
|
8
|
+
* ④ 낯선 낱말은 「모른다」다 — 커널이 사건을 늘렸을 때 조용히 빠뜨리지 않는다.
|
|
9
|
+
*
|
|
10
|
+
* 「모른다」(`null`)와 「하나도 없다」(빈 집합)는 **다른 사실**이다. 앞은 전부 다시 만들게 하고 뒤는
|
|
11
|
+
* 물품을 건너뛰게 한다 — 둘을 같게 두면 둘 중 하나가 반드시 틀린다.
|
|
12
|
+
*/
|
|
13
|
+
import { test } from 'node:test'
|
|
14
|
+
import assert from 'node:assert/strict'
|
|
15
|
+
|
|
16
|
+
import { touchedItemKeys } from '../shared/touched-items.ts'
|
|
17
|
+
|
|
18
|
+
const epcis = (data: any) => ({ eventType: 'epcis.object', data })
|
|
19
|
+
|
|
20
|
+
test('관측 사건은 말한 개체를 모두 낸다', () => {
|
|
21
|
+
const got = touchedItemKeys(epcis({ epcList: ['urn:epc:id:sgtin:952.1.A', 'urn:epc:id:sgtin:952.1.B'] }))
|
|
22
|
+
assert.deepEqual([...got!].sort(), ['urn:epc:id:sgtin:952.1.A', 'urn:epc:id:sgtin:952.1.B'])
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('집약은 부모와 자식을 함께 낸다 — 부모만 내면 팔레트에 담긴 것이 낡는다', () => {
|
|
26
|
+
const got = touchedItemKeys({
|
|
27
|
+
eventType: 'epcis.aggregation',
|
|
28
|
+
data: { parentID: 'urn:epc:id:sscc:952.P1', childEPCs: ['urn:epc:id:sgtin:952.1.A'] }
|
|
29
|
+
})
|
|
30
|
+
assert.deepEqual([...got!].sort(), ['urn:epc:id:sgtin:952.1.A', 'urn:epc:id:sscc:952.P1'])
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('변환은 입력과 출력을 함께 낸다 — 소비된 것도 값이 바뀐다', () => {
|
|
34
|
+
const got = touchedItemKeys({
|
|
35
|
+
eventType: 'epcis.transformation',
|
|
36
|
+
data: { inputEPCList: ['in-1', 'in-2'], outputEPCList: ['out-1'] }
|
|
37
|
+
})
|
|
38
|
+
assert.deepEqual([...got!].sort(), ['in-1', 'in-2', 'out-1'])
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('부류 수량만 말한 사건은 「모른다」다 — 그 부류의 어느 개체인지 말하지 않았다', () => {
|
|
42
|
+
const got = touchedItemKeys(epcis({ quantityList: [{ epcClass: 'urn:epc:idpat:sgtin:952.1.*', quantity: 3 }] }))
|
|
43
|
+
assert.equal(got, null, '개체를 특정하지 못하면 전부 다시 만들어야 한다')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('물품을 만지지 않는다고 선언된 사건은 빈 범위다 — 전부 다시 만들 이유가 없다', () => {
|
|
47
|
+
for (const eventType of ['task.status', 'equipment.status', 'person.status', 'asset.status', 'order.status', 'quality.output', 'attention.acked']) {
|
|
48
|
+
const got = touchedItemKeys({ eventType, data: { taskId: 't1', moverId: 'fk-1' } })
|
|
49
|
+
assert.ok(got, `${eventType} 은 「모른다」가 아니다`)
|
|
50
|
+
assert.equal(got!.size, 0, `${eventType} 은 물품을 건드리지 않는다`)
|
|
51
|
+
}
|
|
52
|
+
const energy = touchedItemKeys({ eventType: 'energy.measured', data: { id: 'm-1', kW: 12 } })
|
|
53
|
+
assert.equal(energy!.size, 0)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('낯선 낱말은 「모른다」다 — 커널이 사건을 늘렸을 때 조용히 빠뜨리지 않는다', () => {
|
|
57
|
+
assert.equal(touchedItemKeys({ eventType: 'something.new', data: { epcList: ['x'] } }), null)
|
|
58
|
+
assert.equal(touchedItemKeys({ data: { epcList: ['x'] } }), null, '낱말이 없으면 판정할 수 없다')
|
|
59
|
+
assert.equal(touchedItemKeys(undefined), null)
|
|
60
|
+
assert.equal(touchedItemKeys({ eventType: 'epcis.object' }), null, '내용이 없으면 판정할 수 없다')
|
|
61
|
+
})
|
|
@@ -7,7 +7,15 @@
|
|
|
7
7
|
import { test } from 'node:test'
|
|
8
8
|
import assert from 'node:assert/strict'
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
/*
|
|
11
|
+
* **빌드 산출물을 부른다** — 이 모듈은 로그 문(`engine/log.js`)을 값으로 참조하고, 타입만 지우고 소스를
|
|
12
|
+
* 실행하는 방식은 그 `.js` 경로를 `.ts` 로 바꿔 주지 않는다. 소스를 부르면 「모듈을 찾을 수 없다」로 선다.
|
|
13
|
+
* **소스를 고친 뒤에는 `npm run build:server` 를 먼저 돌린다.**
|
|
14
|
+
*/
|
|
15
|
+
import { createRequire } from 'node:module'
|
|
16
|
+
|
|
17
|
+
const req = createRequire(import.meta.url)
|
|
18
|
+
const { bizStepOf, epcOf, orderOf, locationOf, equipmentIdOf, twinEventKeys } = req('../dist-server/service/twin-event/twin-event-keys.js')
|
|
11
19
|
|
|
12
20
|
/** EPCIS ObjectEvent 한 건(입고) — 실제 인제스트가 싣는 모양. */
|
|
13
21
|
const EPCIS_OBJECT = {
|
|
@@ -102,7 +110,7 @@ test('상한을 넘는 값은 조용히 사라지지 않는다 — 잘리되 경
|
|
|
102
110
|
})
|
|
103
111
|
|
|
104
112
|
test('빈 봉투에도 터지지 않는다', () => {
|
|
105
|
-
const empty = { bizStep: undefined, epc: undefined, orderId: undefined, locationId: undefined, moverId: undefined }
|
|
113
|
+
const empty = { action: undefined, bizStep: undefined, epc: undefined, orderId: undefined, locationId: undefined, moverId: undefined }
|
|
106
114
|
assert.deepEqual(twinEventKeys(undefined), empty)
|
|
107
115
|
assert.deepEqual(twinEventKeys({}), empty)
|
|
108
116
|
})
|
|
@@ -238,6 +238,8 @@ test('멈춘 트윈도 저널을 접어 답하고, 그 답은 다시 계산한
|
|
|
238
238
|
const model = {
|
|
239
239
|
spaceId: 'journal-space',
|
|
240
240
|
utcOffsetMinutes: 0,
|
|
241
|
+
/* 커널은 선언 없이 팔레트·문서를 만들지 않는다 — 픽스처 자기 이름공간을 준다. */
|
|
242
|
+
identity: { namespaces: ['https://twin.test/twin-model-item/journal'] },
|
|
241
243
|
locations: [
|
|
242
244
|
{ id: 'dock-recv', type: 'dock', capacity: 100 },
|
|
243
245
|
{ id: 'bin-1', type: 'storage', capacity: 20 }
|