@things-factory/headless-twin 10.0.12 → 10.0.14
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 +376 -0
- package/dist-server/engine/ingest-health.js +482 -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 +386 -16
- package/dist-server/engine/twin-engine.js +1217 -172
- 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 +210 -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 +78 -0
- package/dist-server/service/reference/reference-assessment.js +156 -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 +134 -11
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-master.d.ts +59 -3
- 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 +26 -2
- 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 +46 -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 +108 -14
- 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 +782 -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 +1334 -168
- 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 +220 -5
- package/server/service/reference/reference-assessment.ts +250 -0
- package/server/service/reference/reference-live.ts +143 -11
- package/server/service/reference/reference-master.ts +76 -6
- 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 +29 -2
- 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 +29 -8
- package/server/service/twin-model/iec61850-coverage.ts +2 -8
- package/server/service/twin-model/isa95-coverage.ts +107 -14
- 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/checkpoint-refuses-empty.test.ts +103 -0
- package/test/command-routing.test.ts +1 -1
- package/test/cursor-stall-not-read-failure.test.ts +126 -0
- 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/mirror-resumes-from-checkpoint.test.ts +192 -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 +104 -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
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **적은 것도 추이로 남는다** — 「저널이 얼마나 쓰이고 있나」를 나중에도 물을 수 있게 (2026-08-20).
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 필요한가 ─────────────────────────────────────────────────────────────
|
|
5
|
+
* 계기(`journalRate`·`journaledTotal`)는 **지금**만 답한다. 프로세스를 다시 띄우면 0 에서 시작하므로
|
|
6
|
+
* 「어제 이 시각에도 이랬나」·「고친 뒤로 정말 줄었나」를 물을 자리가 없었다. 그런데 2026-08-20 에 고친
|
|
7
|
+
* 것이 바로 쓰기 경로(델타마다 한 행 → 창당 한 번)다. 세지 않는 개선은 되돌아가도 아무도 모른다.
|
|
8
|
+
*
|
|
9
|
+
* ── 왜 새 장부를 만들지 않았나 ──────────────────────────────────────────────
|
|
10
|
+
* 유입 창은 「닫힌 창만 최근」·「0 과 없음을 가른다」·「밀려난 것을 세어 드러낸다」를 이미 지키고, 닫힐 때
|
|
11
|
+
* 행으로 남는다. 같은 규율을 두 벌 만들면 한 벌만 고쳐진다. 그래서 그 창에 칸을 더했다.
|
|
12
|
+
*
|
|
13
|
+
* ── 이 시험이 지키는 것 ─────────────────────────────────────────────────────
|
|
14
|
+
* ① 유입이 한 번도 없어도 쓰기가 창을 넘긴다 — 시뮬 트윈이 추이에서 사라지지 않게.
|
|
15
|
+
* ② 행과 왕복을 **따로** 센다 — 행당 왕복이 1 로 되돌아가면 그것이 보인다.
|
|
16
|
+
* ③ 쓰기는 **「마지막 유입」을 갱신하지 않는다** — 섞으면 끊긴 피드가 옛 창으로 초록을 낸다.
|
|
17
|
+
* ④ 없는 것을 0 건 쓰기로 만들지 않는다.
|
|
18
|
+
* ⑤ 쓰기만 있는 장부의 시각이 1969년으로 뜨지 않는다.
|
|
19
|
+
*/
|
|
20
|
+
import { test } from 'node:test'
|
|
21
|
+
import assert from 'node:assert/strict'
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
INGEST_WINDOW_MS,
|
|
25
|
+
STALE_AFTER_MS,
|
|
26
|
+
ingestHealth,
|
|
27
|
+
newIngestLedger,
|
|
28
|
+
recordIngest,
|
|
29
|
+
recordJournalWrite
|
|
30
|
+
} from '../server/engine/ingest-health.ts'
|
|
31
|
+
|
|
32
|
+
const T0 = 1_700_000_000_000
|
|
33
|
+
|
|
34
|
+
/* ── ① 유입 없이도 추이가 생긴다 ──────────────────────────────────────────── */
|
|
35
|
+
|
|
36
|
+
test('받는 것 없이 적기만 하는 트윈도 추이에 남는다 — 시뮬이 조용히 빠지지 않게', () => {
|
|
37
|
+
const l = newIngestLedger()
|
|
38
|
+
const closed: any[] = []
|
|
39
|
+
|
|
40
|
+
recordJournalWrite(l, 40, T0, w => closed.push(w))
|
|
41
|
+
recordJournalWrite(l, 60, T0 + 1000, w => closed.push(w))
|
|
42
|
+
/* 창 길이를 넘긴 뒤의 쓰기가 앞 창을 닫는다. */
|
|
43
|
+
recordJournalWrite(l, 10, T0 + INGEST_WINDOW_MS + 1, w => closed.push(w))
|
|
44
|
+
|
|
45
|
+
assert.equal(closed.length, 1, '창이 닫혀 영속 훅까지 불린다')
|
|
46
|
+
assert.equal(closed[0].rows, 100)
|
|
47
|
+
assert.equal(closed[0].offered, 0, '유입은 0 이다 — 그것도 사실이다')
|
|
48
|
+
|
|
49
|
+
const view = ingestHealth(l, 'not-applicable', undefined, T0 + INGEST_WINDOW_MS + 2)
|
|
50
|
+
assert.equal(view.trend.length, 1, '유입 시각이 없다는 이유로 빈 화면을 내지 않는다')
|
|
51
|
+
assert.equal(view.trend[0].rows, 100)
|
|
52
|
+
assert.equal(view.total.rows, 110, '누적은 열린 창까지 센다')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
/* ── ② 행과 왕복을 따로 센다 ──────────────────────────────────────────────── */
|
|
56
|
+
|
|
57
|
+
test('행과 왕복을 따로 센다 — 행당 왕복이 1 로 되돌아가면 그것이 보인다', () => {
|
|
58
|
+
const batched = newIngestLedger()
|
|
59
|
+
recordJournalWrite(batched, 500, T0)
|
|
60
|
+
recordJournalWrite(batched, 500, T0 + 200)
|
|
61
|
+
assert.equal(batched.rows, 1000)
|
|
62
|
+
assert.equal(batched.flushes, 2, '모아 보낸 것은 두 번이다')
|
|
63
|
+
|
|
64
|
+
/* 되돌아간 모양 — 같은 1,000행에 왕복 1,000번. 행 수만 세면 두 장부가 똑같이 보인다. */
|
|
65
|
+
const perDelta = newIngestLedger()
|
|
66
|
+
for (let i = 0; i < 1000; i++) recordJournalWrite(perDelta, 1, T0 + i)
|
|
67
|
+
assert.equal(perDelta.rows, batched.rows, '행 수는 같다 — 그래서 행만으로는 가려낼 수 없다')
|
|
68
|
+
assert.ok(perDelta.flushes > batched.flushes * 100, '왕복 수가 그 차이를 드러낸다')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
/* ── ③ 축을 섞지 않는다 ───────────────────────────────────────────────────── */
|
|
72
|
+
|
|
73
|
+
test('쓰기가 「마지막 유입」을 갱신하지 않는다 — 끊긴 피드가 초록으로 보이지 않게', () => {
|
|
74
|
+
const l = newIngestLedger()
|
|
75
|
+
/* 건강한 유입이 한동안 있었고, 그 뒤 피드가 끊겼다. */
|
|
76
|
+
recordIngest(l, 100, [], T0)
|
|
77
|
+
recordIngest(l, 100, [], T0 + INGEST_WINDOW_MS + 1) /* 앞 창을 닫아 「최근」을 만든다 */
|
|
78
|
+
|
|
79
|
+
const muchLater = T0 + INGEST_WINDOW_MS + STALE_AFTER_MS + 60_000
|
|
80
|
+
/* 그 사이에도 트윈은 틱이 만든 델타를 계속 적는다 — 라이브에서 실제로 그렇다. */
|
|
81
|
+
for (let t = T0 + 60_000; t < muchLater; t += 60_000) recordJournalWrite(l, 5, t)
|
|
82
|
+
|
|
83
|
+
const view = ingestHealth(l, 'attached', undefined, muchLater)
|
|
84
|
+
/*
|
|
85
|
+
* 판정이 초록이 아니어야 한다. 어떤 말로 부르는지는 창의 모양에 달렸다(마지막 닫힌 창이 유입 0 이면
|
|
86
|
+
* 「유입 없음」, 창이 아직 안 닫혔으면 「끊긴 피드」) — 둘 다 「받고 있지 않다」다. 못 박는 것은 **적고
|
|
87
|
+
* 있다는 사실이 「받고 있다」로 번역되지 않는다**는 것이다.
|
|
88
|
+
*/
|
|
89
|
+
assert.notEqual(view.verdict, 'healthy', '적고 있다는 사실이 「받고 있다」로 번역되지 않는다')
|
|
90
|
+
assert.equal(view.lastAt, new Date(T0 + INGEST_WINDOW_MS + 1).toISOString(), '마지막 유입 시각이 쓰기로 밀리지 않는다')
|
|
91
|
+
assert.equal(view.lastWriteAt, new Date(muchLater - 60_000).toISOString(), '적은 시각은 따로 낸다')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
/* ── ④⑤ 없는 것과 0 ──────────────────────────────────────────────────────── */
|
|
95
|
+
|
|
96
|
+
test('없는 것을 0 건 쓰기로 만들지 않는다', () => {
|
|
97
|
+
const l = newIngestLedger()
|
|
98
|
+
for (const bad of [0, -3, NaN, Infinity]) recordJournalWrite(l, bad as number, T0)
|
|
99
|
+
assert.equal(l.flushes, 0, '아무 일도 없었으면 왕복도 없다')
|
|
100
|
+
assert.equal(l.rows, 0)
|
|
101
|
+
assert.equal(l.lastWriteAtMs, -1, '적지 않았으면 적은 시각도 없다')
|
|
102
|
+
assert.equal(ingestHealth(l, 'not-applicable', undefined, T0).lastWriteAt, null)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('쓰기만 있는 장부의 시각이 1969년으로 뜨지 않는다 — `-1` 을 시각으로 읽지 않는다', () => {
|
|
106
|
+
const l = newIngestLedger()
|
|
107
|
+
recordJournalWrite(l, 7, T0)
|
|
108
|
+
recordJournalWrite(l, 7, T0 + INGEST_WINDOW_MS + 1)
|
|
109
|
+
|
|
110
|
+
const view = ingestHealth(l, 'not-applicable', undefined, T0 + INGEST_WINDOW_MS + 2)
|
|
111
|
+
assert.equal(view.lastAt, null, '받은 적이 없으면 유입 시각은 **없다**(0 이 아니다)')
|
|
112
|
+
for (const w of [...view.trend, view.recent!]) {
|
|
113
|
+
assert.ok(new Date(w.to).getUTCFullYear() >= 2023, `창 끝 시각이 ${w.to} 로 떴다`)
|
|
114
|
+
}
|
|
115
|
+
})
|
|
@@ -31,7 +31,7 @@ const MUTATION = new URL('../server/service/twin-lifecycle/twin-lifecycle-mutati
|
|
|
31
31
|
/*
|
|
32
32
|
* 종류 폴백은 **한 패키지에 갇혀 있지 않다.** 처음에 twin-engine 두 자리만 고쳤더니, 라이브 미러를
|
|
33
33
|
* 띄우는 호출자 넷(operato-twin 의 벤치·부하시험·프리셋 프로비저닝, headless 의 reference-live)이
|
|
34
|
-
* `det.kind ?? 'wms'` 로 **엔진이
|
|
34
|
+
* `det.kind ?? 'wms'` 로 **엔진이 오류를 내기 전에 먼저 값을 정하고** 있었다. 커넥터 spec 도 마찬가지였다.
|
|
35
35
|
* 그래서 두 패키지의 서버 코드를 함께 훑는다.
|
|
36
36
|
*/
|
|
37
37
|
const ROOTS = [new URL('../server', import.meta.url).pathname, new URL('../../operato-twin/server', import.meta.url).pathname]
|
|
@@ -50,7 +50,7 @@ function codeOf(path: string): string[] {
|
|
|
50
50
|
|
|
51
51
|
test('커널 폴백이 없다 — 모르는 종류는 창고로 떨어지지 않는다', () => {
|
|
52
52
|
const offenders = codeOf(ENGINE).filter(l => /\?\?\s*WmsKernel|\|\|\s*WmsKernel/.test(l))
|
|
53
|
-
assert.deepEqual(offenders, [], '커널 선택에 폴백이 다시 생겼다 — kernelFor
|
|
53
|
+
assert.deepEqual(offenders, [], '커널 선택에 폴백이 다시 생겼다 — kernelFor 가 오류를 내게 할 것')
|
|
54
54
|
})
|
|
55
55
|
|
|
56
56
|
test('종류→커널 해소는 kernelFor 한 곳만 한다', () => {
|
|
@@ -59,10 +59,10 @@ test('종류→커널 해소는 kernelFor 한 곳만 한다', () => {
|
|
|
59
59
|
assert.deepEqual(indexing, [], 'KERNELS 를 밖에서 색인하고 있다 — kernelFor(kind) 를 쓸 것')
|
|
60
60
|
})
|
|
61
61
|
|
|
62
|
-
test('kernelFor 는 모르는 종류에
|
|
62
|
+
test('kernelFor 는 모르는 종류에 오류를 낸다 — 조용히 고르지 않는다', () => {
|
|
63
63
|
const src = readFileSync(ENGINE, 'utf-8')
|
|
64
64
|
const fn = src.slice(src.indexOf('function kernelFor'))
|
|
65
|
-
assert.match(fn.slice(0, 300), /throw new Error/, 'kernelFor 가
|
|
65
|
+
assert.match(fn.slice(0, 300), /throw new Error/, 'kernelFor 가 오류를 내지 않으면 이 계약이 무너진다')
|
|
66
66
|
})
|
|
67
67
|
|
|
68
68
|
test("board 를 주는 기동은 kind 를 요구한다 — `kind ?? 'wms'` 로 메우지 않는다", () => {
|
package/test/kpi-fold.test.ts
CHANGED
|
@@ -119,9 +119,51 @@ test('자원 점유: 작업에 붙어 있던 시간의 합과 창 대비 비율'
|
|
|
119
119
|
assert.equal(Math.round(r.utilization[0].ratio * 100), 50, '1시간 창에서 30분 = 50%')
|
|
120
120
|
})
|
|
121
121
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
/*
|
|
123
|
+
* ── 오더 완료: **동의어를 받지 않는다**(2026-08-23 에 바뀐 규칙) ─────────────
|
|
124
|
+
*
|
|
125
|
+
* 이 시험은 예전에 「도메인 어휘를 넓게 받는다(completed/fulfilled/done)」였다 — 그것이 **동의어 목록**
|
|
126
|
+
* 이었고, 그 방식이 낸 대가가 둘이었다.
|
|
127
|
+
*
|
|
128
|
+
* ① 원본의 낱말이 목록에 없으면 완료가 0 이 된다(첫 실 시스템의 `FINISHED` 가 그랬다).
|
|
129
|
+
* ② **시뮬 트윈에서는 언제나 0 이었다** — 시뮬은 `fulfilled` 만 올리고 `status` 는 바꾸지 않는다.
|
|
130
|
+
*
|
|
131
|
+
* 판정은 이제 커널이 한다(`isOrderTerminal`): 1차 근거는 **양**, 낱말은 닫힌 둘(`completed`·`cancelled`).
|
|
132
|
+
* 표준이 그 축을 갈라 두었다 — `RequestState` 는 표준도 열거하지 않고, 「됐나」는 실적에서 읽는다.
|
|
133
|
+
*/
|
|
134
|
+
test('오더 완료: 닫힌 종결 낱말만 받는다 — 동의어는 추론하지 않는다', () => {
|
|
135
|
+
const r = foldKpi(
|
|
136
|
+
[order('o1', 'completed', 10), order('o2', 'cancelled', 20), order('o3', 'in-progress', 30)],
|
|
137
|
+
WINDOW
|
|
138
|
+
)
|
|
139
|
+
assert.equal(r.throughput.orders, 2, 'completed · cancelled 둘')
|
|
140
|
+
const r2 = foldKpi([order('o4', 'fulfilled', 10), order('o5', 'done', 20), order('o6', 'FINISHED', 30)], WINDOW)
|
|
141
|
+
assert.equal(r2.throughput.orders, 0, '동의어로 보이는 낱말은 세지 않는다 — 커넥터가 옮길 일이다')
|
|
142
|
+
assert.deepEqual(r2.orderStatusNotCounted, { fulfilled: 1, done: 1, finished: 1 }, '대신 그 사실을 말한다')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('오더 완료: 양이 답한다 — 낱말이 진행 중이라 말해도', () => {
|
|
146
|
+
/*
|
|
147
|
+
* 시뮬을 되살리는 자리다. 시뮬 오더는 이행돼도 상태가 `created` 로 남으므로, 낱말로 묻는 판정은
|
|
148
|
+
* 그 완료를 한 건도 세지 못했다.
|
|
149
|
+
*/
|
|
150
|
+
const qty = (orderId: string, status: string, sec: number, requested: number, fulfilled: number): KpiEvent => ({
|
|
151
|
+
eventType: 'order.status',
|
|
152
|
+
eventTime: at(sec),
|
|
153
|
+
payload: { data: { orderId, status, requested, fulfilled } }
|
|
154
|
+
})
|
|
155
|
+
const r = foldKpi([qty('o1', 'created', 10, 10, 10), qty('o2', 'created', 20, 10, 9)], WINDOW)
|
|
156
|
+
assert.equal(r.throughput.orders, 1, '10 중 10 은 끝났고 10 중 9 는 아니다')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('오더 완료: 같은 오더를 한 번만 센다 — 폴링 원본이 다시 보내도', () => {
|
|
160
|
+
/*
|
|
161
|
+
* 폴링하는 원본은 같은 오더의 종결을 여러 주기에 걸쳐 다시 보낼 수 있다(결함이 아니라 폴링의 성질).
|
|
162
|
+
* 사건마다 세면 처리량이 조용히 부풀려진다 — 작업 쪽은 커널의 짝맞춤이 이미 그 규율을 갖고 있었고
|
|
163
|
+
* 오더만 없었다.
|
|
164
|
+
*/
|
|
165
|
+
const r = foldKpi([order('o1', 'completed', 10), order('o1', 'completed', 20), order('o2', 'completed', 30)], WINDOW)
|
|
166
|
+
assert.equal(r.throughput.orders, 2, '오더 둘 — 사건은 셋이다')
|
|
125
167
|
})
|
|
126
168
|
|
|
127
169
|
test('저널이 비면 0 이 아니라 "본 이벤트 없음" 으로 드러난다', () => {
|
|
@@ -1087,3 +1129,48 @@ test('피크 구간에 표본이 없던 분기는 피크 값을 만들지 않는
|
|
|
1087
1129
|
assert.equal(byKey.get('feeder-a')?.peakKW, 300)
|
|
1088
1130
|
assert.equal(byKey.get('feeder-b')?.peakKW, undefined, '그 순간 값을 모르면 만들지 않는다')
|
|
1089
1131
|
})
|
|
1132
|
+
|
|
1133
|
+
/*
|
|
1134
|
+
* ── 완료로 읽지 못한 오더 상태 낱말을 **센다** (2026-08-23) ──────────────────
|
|
1135
|
+
*
|
|
1136
|
+
* 오더의 상태 어휘는 계약이 도메인에 넘긴 것이라(`picking`·`shipped`·`FINISHED`…) 이 폴드는 「완료로
|
|
1137
|
+
* 읽히는 표현」을 동의어 목록으로 받는다. 그 대가는 **목록에 없는 낱말이 조용히 0 으로 세어진다**는 것이다.
|
|
1138
|
+
*
|
|
1139
|
+
* 실제로 그랬다: `finished` 가 빠져 있어 첫 실 시스템의 완료 오더가 전부 0 이었고, 사람이 화면을 보고
|
|
1140
|
+
* 의심해서야 드러났다. 목록을 넓히는 것으로는 다음 원본을 막지 못한다 — 그래서 **못 읽은 것을 센다.**
|
|
1141
|
+
*/
|
|
1142
|
+
test('완료로 센 낱말은 여기 오지 않는다 — 이 값은 세지 않은 것의 인구조사다', () => {
|
|
1143
|
+
const r = foldKpi(
|
|
1144
|
+
[order('o-1', 'completed', 10), order('o-2', 'CANCELLED', 20), order('o-3', 'cancelled', 30)],
|
|
1145
|
+
WINDOW
|
|
1146
|
+
)
|
|
1147
|
+
assert.equal(r.throughput.orders, 3, '대소문자를 가리지 않는다')
|
|
1148
|
+
assert.deepEqual(r.orderStatusNotCounted, {}, '완료로 센 낱말은 여기 오지 않는다')
|
|
1149
|
+
})
|
|
1150
|
+
|
|
1151
|
+
test('세지 않은 낱말이 건수와 함께 남는다 — 완료 0 의 원인이 곧 보이게', () => {
|
|
1152
|
+
const r = foldKpi(
|
|
1153
|
+
[order('o-1', 'CLOSED', 10), order('o-2', 'CLOSED', 20), order('o-3', 'picking', 30)],
|
|
1154
|
+
WINDOW
|
|
1155
|
+
)
|
|
1156
|
+
assert.equal(r.throughput.orders, 0, '목록에 없는 낱말을 완료로 세지 않는다 — 추론하지 않는 것이 옳다')
|
|
1157
|
+
assert.deepEqual(
|
|
1158
|
+
r.orderStatusNotCounted,
|
|
1159
|
+
{ closed: 2, picking: 1 },
|
|
1160
|
+
'무엇을 몇 건 세지 않았는지 말한다 — picking 이 함께 오는 것은 정상이다(완료가 아니다)'
|
|
1161
|
+
)
|
|
1162
|
+
})
|
|
1163
|
+
|
|
1164
|
+
test('창 밖의 낱말은 세지 않는다 — 이 값도 창의 사실이다', () => {
|
|
1165
|
+
const r = foldKpi([order('o-1', 'CLOSED', 10), order('o-2', 'CLOSED', 7200)], WINDOW)
|
|
1166
|
+
assert.deepEqual(r.orderStatusNotCounted, { closed: 1 }, '창 안의 것만 센다')
|
|
1167
|
+
})
|
|
1168
|
+
|
|
1169
|
+
test('상태를 말하지 않은 사건은 여기 오지 않는다 — 그것은 다른 결함이다', () => {
|
|
1170
|
+
/*
|
|
1171
|
+
* 빈 낱말을 여기 섞으면 이 값의 뜻이 흐려진다 — 상태가 없는 사건은 **다른 결함**이고(유입 문이 상태를
|
|
1172
|
+
* 필수로 받는다) 그 진단은 그 문의 몫이다.
|
|
1173
|
+
*/
|
|
1174
|
+
const r = foldKpi([{ eventType: 'order.status', eventTime: at(10), payload: { data: { orderId: 'o-1' } } }], WINDOW)
|
|
1175
|
+
assert.deepEqual(r.orderStatusNotCounted, {})
|
|
1176
|
+
})
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* 구간 성과 질의 비용 측정 —
|
|
2
|
+
* 구간 성과 질의 비용 측정 — **브로드캐스팅이냐 질의냐를 숫자로 정한다.**
|
|
3
3
|
*
|
|
4
4
|
* ── 왜 재는가 (2026-08-06) ─────────────────────────────────────────────────
|
|
5
5
|
* 성과 카드를 여러 단계(공간·트윈·구역·자리·설비)에 붙이려 한다. 두 방법이 있다.
|
|
6
6
|
*
|
|
7
|
-
* ·
|
|
7
|
+
* · 브로드캐스팅 — 서버가 주기마다 전 단계를 계산해 밀어 준다. 자리가 트윈당 최대 200개고 지표가 6개라
|
|
8
8
|
* 태그가 1,200개가 된다. **모델에 카드를 하나도 안 놓아도 계산은 돈다.**
|
|
9
9
|
* · 질의 — 카드가 필요할 때 묻는다. **모델에 놓인 카드 수만큼만** 든다. 시각(`toTime`)도 물을 수 있어
|
|
10
10
|
* 타임라인과 맞고, 공간 단위 합산(여러 트윈을 한 번에 접기)은 질의에만 있다.
|
|
@@ -53,8 +53,24 @@ const master = (): any => ({
|
|
|
53
53
|
productionSpec: {
|
|
54
54
|
companyPrefix: CP,
|
|
55
55
|
binding: { steel: '077101', frame: '078101' },
|
|
56
|
+
/*
|
|
57
|
+
* 작업지시의 문서 식별자를 **이 현장이 선언한다** — 커널은 문서 타입을 정하지 않습니다(0.7.43).
|
|
58
|
+
* GDTI 는 「회사 프리픽스 + 문서 타입 + 일련번호」이고, 그 타입은 프리픽스를 배정받은 회사가
|
|
59
|
+
* 자기 번호 용량에서 배정합니다. 그러니 픽스처도 그 선언을 해야 합니다.
|
|
60
|
+
*/
|
|
61
|
+
identity: { documentTypes: { workorder: '403' } },
|
|
56
62
|
definition: {
|
|
57
|
-
|
|
63
|
+
/* 자재는 **자기 보관 자리를 선언한다** — 커널이 자재가 놓이는 자리를 지어내지 않습니다(0.7.43). */
|
|
64
|
+
materials: [
|
|
65
|
+
{ key: 'steel', label: '강재', locationType: 'raw-store' },
|
|
66
|
+
{ key: 'frame', label: '프레임', locationType: 'fg-store' }
|
|
67
|
+
],
|
|
68
|
+
locationTypes: [
|
|
69
|
+
{ key: 'raw-store', label: '자재 보관' },
|
|
70
|
+
{ key: 'cut-station', label: '절단' },
|
|
71
|
+
{ key: 'weld-station', label: '용접' },
|
|
72
|
+
{ key: 'fg-store', label: '완제품 보관' }
|
|
73
|
+
],
|
|
58
74
|
routes: [{ key: 'frame-route', steps: ['cut', 'weld'] }],
|
|
59
75
|
recipes: [{ key: 'frame-recipe', label: 'f', route: 'frame-route', inputs: [{ material: 'steel', qty: 2 }], outputs: [{ material: 'frame', qty: 1 }] }],
|
|
60
76
|
operations: [
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ 읽기 커서는 **부르는 쪽이 자리를 가리켜야** 이어진다 — 커넥터는 그 이름을 알 수 없다.
|
|
3
|
+
*
|
|
4
|
+
* ── 무엇이 조용히 끊겨 있었나 (2026-08-23) ──────────────────────────────────
|
|
5
|
+
* 커서를 저장할 컬럼과 계약을 만들고 배선까지 했는데, **한 번도 저장되지 않았다.** 저장할 자리를
|
|
6
|
+
* `cfg`·`site` 에서 찾으려 했기 때문이다.
|
|
7
|
+
*
|
|
8
|
+
* cfg 사람이 화면에서 입력한 접속 설정 — endpoint · token · subdomain · liveLookbackDays
|
|
9
|
+
* site 커넥터가 원본에서 발견한 것 — { siteId, name }
|
|
10
|
+
*
|
|
11
|
+
* **둘 다 그 값을 가질 수 없다.** 레퍼런스의 이름(`TwinReference.source`)은 **호스트가 붙이는 것**이고
|
|
12
|
+
* 원본은 그것을 모른다. 커넥터가 채울 수 없는 값을 커넥터에서 읽으려 한 것이 잘못이었다.
|
|
13
|
+
*
|
|
14
|
+
* 그리고 그 실패는 **로그로는 말했다** — 「this feed does not say which reference it reads from」.
|
|
15
|
+
* 그 한 줄로 다른 레인이 바로 짚었다(조용히 넘기지 않은 것이 값을 했다). 그러나 로그는 사람이 볼 때만
|
|
16
|
+
* 값이 있다. 배선이 다시 끊기면 이 시험이 먼저 말한다.
|
|
17
|
+
*
|
|
18
|
+
* ── 왜 「늘었나」로 확인하면 안 되나 ────────────────────────────────────────
|
|
19
|
+
* 재기동 뒤 수가 늘어도 그것은 커서가 이어진 증거가 아니다 — 실제로 같은 판에 다른 변경(공정별 전이)이
|
|
20
|
+
* 들어가 작업 수가 배로 늘었고, 그 사이 커서는 여전히 매번 새로 만들어졌다. 확인할 것은 **배선**이다.
|
|
21
|
+
*/
|
|
22
|
+
import { test } from 'node:test'
|
|
23
|
+
import assert from 'node:assert/strict'
|
|
24
|
+
import { readFileSync } from 'node:fs'
|
|
25
|
+
import { join } from 'node:path'
|
|
26
|
+
|
|
27
|
+
const DIR = join(import.meta.dirname, '..', 'server', 'service', 'reference')
|
|
28
|
+
const LIVE = readFileSync(join(DIR, 'reference-live.ts'), 'utf8')
|
|
29
|
+
const RESOLVER = readFileSync(join(DIR, 'reference-resolver.ts'), 'utf8')
|
|
30
|
+
|
|
31
|
+
/** 주석을 걷어낸 코드만 — 「쓰지 않는다」를 적어 둔 근거가 가드를 뒤집지 않게. */
|
|
32
|
+
function codeOf(src: string): string {
|
|
33
|
+
return src.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/(^|[^:])\/\/.*$/gm, '$1')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test('커서를 저장할 자리는 부르는 쪽이 가리킨다 — cfg·site 에서 짐작하지 않는다', () => {
|
|
37
|
+
const code = codeOf(LIVE)
|
|
38
|
+
/* 커넥터가 채울 수 없는 값을 커넥터의 인자에서 읽지 않는다. */
|
|
39
|
+
assert.doesNotMatch(code, /cfg as any\)\?\.source/, 'cfg 에서 레퍼런스 이름을 찾지 않는다')
|
|
40
|
+
assert.doesNotMatch(code, /site as any\)\?\.source/, 'site 에서 레퍼런스 이름을 찾지 않는다')
|
|
41
|
+
/* 대신 인자로 받는다. */
|
|
42
|
+
assert.match(code, /loadLiveCursor\(domainId, instanceId, source\)/, '받은 이름으로 커서를 찾는다')
|
|
43
|
+
/* 가드가 스스로를 증명한다 — 주석을 걷어낸 코드가 비어 버리지 않았다. */
|
|
44
|
+
assert.match(code, /startReferenceLiveFeed/, '대상 함수가 남아 있다')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('모든 호출처가 그 이름을 넘긴다 — 한 곳만 빠져도 그 경로는 조용히 창이 미끄러진다', () => {
|
|
48
|
+
/*
|
|
49
|
+
* 호출처가 둘이고 **둘 다** `TwinReference` 를 들고 있다(부팅 재개 · 인제스트 직후 기동).
|
|
50
|
+
* 하나만 넘기면 그 경로로 세워진 트윈만 커서를 잃고, 그 차이는 오류 없이 조용하다.
|
|
51
|
+
*/
|
|
52
|
+
const calls = [...codeOf(LIVE).matchAll(/startReferenceLiveFeed\(([^)]*)\)/g)].map(m => m[1])
|
|
53
|
+
const resolverCalls = [...codeOf(RESOLVER).matchAll(/startReferenceLiveFeed\([\s\S]{0,200}?\)/g)].map(m => m[0])
|
|
54
|
+
const all = [...calls.filter(a => !a.includes('domainId: string')), ...resolverCalls]
|
|
55
|
+
assert.ok(all.length >= 2, `호출처를 찾지 못했다 (${all.length}) — 이 가드가 아무것도 지키지 않는다`)
|
|
56
|
+
for (const call of all) {
|
|
57
|
+
assert.match(call, /ref\.source/, `이 호출처가 레퍼런스 이름을 넘기지 않는다: ${call.replace(/\s+/g, ' ')}`)
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('이름이 없으면 조용히 돌지 않는다 — 말한다', () => {
|
|
62
|
+
/*
|
|
63
|
+
* 커서 없이 도는 것은 「창이 매번 미끄러진다」는 뜻이고, 그 손실은 오류 없이 사라진다. 그래서 그
|
|
64
|
+
* 사실을 로그로 말한다 — 실제로 그 한 줄이 이 결함을 찾아냈다.
|
|
65
|
+
*/
|
|
66
|
+
const code = codeOf(LIVE)
|
|
67
|
+
assert.match(code, /if \(!source\)[\s\S]{0,200}twinWarn/, '이름이 없으면 경고한다')
|
|
68
|
+
assert.match(code, /did not say which reference/, '무엇이 없어서 못 이었는지 말한다')
|
|
69
|
+
/* 행을 못 찾은 경우도 따로 말한다 — 이름은 왔는데 그 레퍼런스가 없는 것은 다른 사실이다. */
|
|
70
|
+
assert.match(code, /no reference row for source/, '레퍼런스 행이 없는 경우를 가려 말한다')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('커서는 레퍼런스의 것이다 — 인스턴스마다 두지 않는다', () => {
|
|
74
|
+
/*
|
|
75
|
+
* 한 레퍼런스가 여러 인스턴스를 낳고(`scopeSpec.produced`) 원본을 읽는 것은 한 번이다.
|
|
76
|
+
* 인스턴스마다 커서를 두면 같은 표를 여러 번 읽는다.
|
|
77
|
+
*/
|
|
78
|
+
const code = codeOf(LIVE)
|
|
79
|
+
assert.match(code, /getRepository\(TwinReference\)/, '레퍼런스 행에 저장한다')
|
|
80
|
+
assert.match(code, /liveCursor: next/, '어댑터가 준 모양을 그대로 적는다')
|
|
81
|
+
assert.doesNotMatch(code, /liveCursor[\s\S]{0,40}instanceId/, '인스턴스로 커서를 가르지 않는다')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('저장 실패를 삼키지 않는다', () => {
|
|
85
|
+
/* 조용히 실패하면 다음 재기동에서 창이 다시 미끄러지고, 이 컬럼을 만든 이유가 사라진다. */
|
|
86
|
+
assert.match(codeOf(LIVE), /live cursor not saved/, '저장 실패를 말한다')
|
|
87
|
+
})
|
|
@@ -11,7 +11,15 @@
|
|
|
11
11
|
import { test } from 'node:test'
|
|
12
12
|
import assert from 'node:assert/strict'
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
/*
|
|
15
|
+
* **빌드 산출물을 부른다** — 이 모듈은 로그 문(`engine/log.js`)을 값으로 참조하고, 타입만 지우고 소스를
|
|
16
|
+
* 실행하는 방식은 그 `.js` 경로를 `.ts` 로 바꿔 주지 않는다. 소스를 부르면 「모듈을 찾을 수 없다」로 선다.
|
|
17
|
+
* **소스를 고친 뒤에는 `npm run build:server` 를 먼저 돌린다.**
|
|
18
|
+
*/
|
|
19
|
+
import { createRequire } from 'node:module'
|
|
20
|
+
|
|
21
|
+
const req = createRequire(import.meta.url)
|
|
22
|
+
const { registerLiveFeedProbe, liveFeedAttached, liveFeedStateOf } = req('../dist-server/engine/live-feed-registry.js')
|
|
15
23
|
|
|
16
24
|
/* 두 계층의 장부를 흉내낸다 — 커넥터 기반과 프리셋 기반. */
|
|
17
25
|
const connectorFeeds = new Set<string>()
|
|
@@ -57,26 +57,26 @@ test('식별자가 아니라 **객체 동일성**으로 가른다 — 어댑터
|
|
|
57
57
|
assert.equal(/applying\.has\(e\.eventId\)/.test(start), false, 'id 비교로 되돌아갔다')
|
|
58
58
|
})
|
|
59
59
|
|
|
60
|
-
test('커널이 낸 사실도
|
|
60
|
+
test('커널이 낸 사실도 브로드캐스팅을 예약한다 — 피크·마감은 상태를 바꾼다', () => {
|
|
61
61
|
const body = methodOf('startLive')
|
|
62
|
-
assert.match(body, /inst\.dirty = true/, '상태가 바뀌었으므로 다음 주기에
|
|
63
|
-
assert.match(body, /ensureBroadcastCoalescer\(\)/, '
|
|
62
|
+
assert.match(body, /inst\.dirty = true/, '상태가 바뀌었으므로 다음 주기에 브로드캐스팅해야 한다')
|
|
63
|
+
assert.match(body, /ensureBroadcastCoalescer\(\)/, '브로드캐스팅은 주기로 합친다(사건마다 브로드캐스팅하면 폭발한다)')
|
|
64
64
|
})
|
|
65
65
|
|
|
66
66
|
/*
|
|
67
|
-
* ── 구조 전환도
|
|
67
|
+
* ── 구조 전환도 브로드캐스팅한다 (2026-08-18) ───────────────────────────────────────
|
|
68
68
|
* 화면이 보는 것 상당수가 구조에서 파생된다(계약 대비 판정·주목 신호·자리 색). 구조 전환이 조용히
|
|
69
69
|
* 끝나면 지도 레일은 다음 계측 표본이 올 때까지 옛 화면을 들고 있고, 헤더 배지는 폴링으로 먼저 알아
|
|
70
70
|
* 「배지엔 있고 목록엔 없는」 어긋난 화면이 된다(실화면에서 봤다).
|
|
71
71
|
*/
|
|
72
|
-
test('구조 전환은 상태
|
|
72
|
+
test('구조 전환은 상태 브로드캐스팅을 예약한다 — 다음 표본까지 화면이 옛것을 들고 있지 않게', () => {
|
|
73
73
|
const body = methodOf('adoptStructure')
|
|
74
74
|
assert.match(body, /inst\.dirty = true/, '구조가 바뀐 순간이 상태가 바뀐 순간이다')
|
|
75
75
|
assert.match(body, /ensureBroadcastCoalescer\(\)/, '새 경로가 아니라 있는 병합 규율에 얹는다')
|
|
76
|
-
/* 커널 전환보다 먼저
|
|
76
|
+
/* 커널 전환보다 먼저 브로드캐스팅하면 구독자가 옛 스냅샷을 되물어 간다 — 순서가 규율이다. */
|
|
77
77
|
assert.ok(
|
|
78
78
|
body.indexOf('inst.kernel.adoptStructure(model)') < body.indexOf('inst.dirty = true'),
|
|
79
|
-
'커널을 갈고 나서
|
|
79
|
+
'커널을 갈고 나서 브로드캐스팅한다'
|
|
80
80
|
)
|
|
81
81
|
})
|
|
82
82
|
|
|
@@ -22,6 +22,10 @@ const master = () => ({
|
|
|
22
22
|
source: 'mirror',
|
|
23
23
|
system: 'wms' as const,
|
|
24
24
|
siteName: 'M',
|
|
25
|
+
/* 이 픽스처가 식별자를 만드는 이름공간 — **픽스처마다 다르게** 둔다(같은 접두 아래에서 서로 다른
|
|
26
|
+
시험의 번호가 부딪치면 표준의 유일성 요구를 깬다, CBV 2.0 §8.2.4). 커널은 선언 없이 팔레트·문서를
|
|
27
|
+
만들지 않는다(0.7.45 에서 날조 프리픽스를 지웠다). */
|
|
28
|
+
identity: { namespaces: ['https://twin.test/live-mirror-parity'] },
|
|
25
29
|
space: { width: 90, depth: 40, unit: 'm' },
|
|
26
30
|
locations: [
|
|
27
31
|
{ id: 'a-dc', type: 'warehouse', role: 'area' as const, name: '창고' },
|
|
@@ -99,6 +103,8 @@ test('★ 라이브 미러: 핵심 관측 상태(점유·용량·위치·아이
|
|
|
99
103
|
function runSimRich() {
|
|
100
104
|
const m = {
|
|
101
105
|
source: 'rich', system: 'wms' as const, siteName: 'R',
|
|
106
|
+
/* 같은 파일이라도 픽스처가 다르면 이름공간도 다르다 — 두 픽스처의 번호가 한 공간에서 겹치지 않게. */
|
|
107
|
+
identity: { namespaces: ['https://twin.test/live-mirror-parity/rich'] },
|
|
102
108
|
space: { width: 90, depth: 40, unit: 'm' },
|
|
103
109
|
locations: [
|
|
104
110
|
{ id: 'a', type: 'warehouse', role: 'area' as const, name: '창고' },
|
package/test/load-meter.test.ts
CHANGED
|
@@ -11,21 +11,7 @@
|
|
|
11
11
|
import { test } from 'node:test'
|
|
12
12
|
import assert from 'node:assert/strict'
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
fleetLoad,
|
|
16
|
-
newCpuSampler,
|
|
17
|
-
sampleCpu,
|
|
18
|
-
judgeCycle,
|
|
19
|
-
loadSummary,
|
|
20
|
-
newLoadMeter,
|
|
21
|
-
recordFork,
|
|
22
|
-
recordPhase,
|
|
23
|
-
rollWindow,
|
|
24
|
-
slowTickMessage,
|
|
25
|
-
TICK_BUDGET_RATIO,
|
|
26
|
-
WARN_COOLDOWN_MS,
|
|
27
|
-
WINDOW_MS
|
|
28
|
-
} from '../server/engine/load-meter.ts'
|
|
14
|
+
import { LOAD_PHASES, fleetLoad, newCpuSampler, sampleCpu, judgeCycle, loadSummary, newLoadMeter, recordFork, recordPhase, rollWindow, slowTickMessage, TICK_BUDGET_RATIO, WARN_COOLDOWN_MS, WINDOW_MS } from '../server/engine/load-meter.ts'
|
|
29
15
|
|
|
30
16
|
const TICK = 1000
|
|
31
17
|
|
|
@@ -312,3 +298,31 @@ test('시계가 뒤로 가거나 구간이 0 이면 비율을 지어내지 않
|
|
|
312
298
|
assert.equal(sampleCpu(s, 1_200_000, 1000), null, '구간 0')
|
|
313
299
|
assert.equal(sampleCpu(newCpuSampler(), 5, 5), null)
|
|
314
300
|
})
|
|
301
|
+
|
|
302
|
+
/*
|
|
303
|
+
* ── 부팅 구간도 재는 이름을 갖는다 (2026-08-22) ─────────────────────────────
|
|
304
|
+
* 실측으로 부팅이 620~717초였고 트윈 하나를 세우는 데 15~93초였는데, **어느 작업이 그 시간을 쓰는지
|
|
305
|
+
* 답할 이름이 없었다** — 재는 이름이 도는 중의 작업뿐이었다. 그래서 짐작으로 원인을 적을 수밖에
|
|
306
|
+
* 없었고, 그것은 이 프로젝트가 금하는 일이다.
|
|
307
|
+
*/
|
|
308
|
+
test('부팅 구간을 재는 이름이 있다 — 웜스타트와 자극 시작', () => {
|
|
309
|
+
assert.ok(LOAD_PHASES.includes('warmStart' as any), '상태 심기를 재는 이름이 있어야 한다')
|
|
310
|
+
assert.ok(LOAD_PHASES.includes('stimulus' as any), '자극 시작을 재는 이름이 있어야 한다')
|
|
311
|
+
|
|
312
|
+
/* 계기가 그 값을 실제로 들고 답한다 — 이름만 있고 담기지 않으면 화면이 빈칸을 보인다. */
|
|
313
|
+
const m = newLoadMeter()
|
|
314
|
+
recordPhase(m, 'warmStart', 93_000, 1_000)
|
|
315
|
+
recordPhase(m, 'stimulus', 72_000, 1_000)
|
|
316
|
+
assert.equal(m.phases.warmStart?.maxMs, 93_000)
|
|
317
|
+
assert.equal(m.phases.stimulus?.maxMs, 72_000)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
test('부팅 구간은 틱 예산으로 판정하지 않는다 — 한 번뿐인 일이다', () => {
|
|
321
|
+
/*
|
|
322
|
+
* 웜스타트는 트윈을 세울 때 한 번이고 몇십 초가 정상일 수 있다. 그것을 틱 예산(간격의 절반)으로
|
|
323
|
+
* 재면 부팅마다 「느린 주기」가 쏟아진다 — 거짓 경보는 참 경보를 죽인다. 판정은 틱만 받는다.
|
|
324
|
+
*/
|
|
325
|
+
const m = newLoadMeter()
|
|
326
|
+
recordPhase(m, 'warmStart', 93_000, 1_000)
|
|
327
|
+
assert.equal(m.overBudget, 0, '기록만으로는 판정하지 않는다')
|
|
328
|
+
})
|
|
@@ -426,7 +426,7 @@ test('현장 겹이 원천 값을 덮으면 그 원천 값을 기억한다 —
|
|
|
426
426
|
* 모델에 설비 `load-1` 이 두 번 있던 것이다. 배선이 사라지지 않게 소스 모양으로 지킨다(이 함수는 DB 를
|
|
427
427
|
* 물고 있어 단위 시험이 불러올 수 없다).
|
|
428
428
|
*/
|
|
429
|
-
test('구조 투영이 중복 id 를 이름으로 말한다 — 제약 위반 문구를 사용자에게
|
|
429
|
+
test('구조 투영이 중복 id 를 이름으로 말한다 — 제약 위반 문구를 사용자에게 보이지 않는다', () => {
|
|
430
430
|
const src = readFileSync(fileURLToPath(new URL('../server/service/twin-model/project-structure.ts', import.meta.url)), 'utf-8')
|
|
431
431
|
assert.match(src, /has two \$\{what\} with id/, '무엇이 겹쳤는지 이름으로 말한다')
|
|
432
432
|
assert.match(src, /ids must be unique within a twin/, '규칙도 함께 말한다')
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 로그에 **시각이 있다** — 그리고 어떤 인자도 버리지 않는다 (2026-08-22).
|
|
3
|
+
*
|
|
4
|
+
* 루프가 55초 막힌 것을 계기로 알아냈는데 그 시각에 무슨 일이 있었는지 로그에서 찾을 수 없었다.
|
|
5
|
+
* 트윈의 줄이 `console.log` 로 나가 시각이 없었기 때문이다. 이 시험이 지키는 것:
|
|
6
|
+
* ① 형식 — 프레임워크 줄과 같은 모양, **밀리초까지**(계기의 값이 epoch ms 다)
|
|
7
|
+
* ② 지역 시각 — 프레임워크의 다른 줄과 같은 기준(UTC 로 찍으면 나란히 읽을 수 없다)
|
|
8
|
+
* ③ 인자를 버리지 않는다 — 오류는 스택까지(`console.error(msg, err)` 로 쓰던 자리가 많다)
|
|
9
|
+
* ④ 트윈 소스에 `console.` 직접 호출이 남지 않는다 — 하나라도 남으면 그 줄만 시각이 없다
|
|
10
|
+
*/
|
|
11
|
+
import { test } from 'node:test'
|
|
12
|
+
import assert from 'node:assert/strict'
|
|
13
|
+
import { readFileSync, readdirSync } from 'node:fs'
|
|
14
|
+
import { fileURLToPath } from 'node:url'
|
|
15
|
+
|
|
16
|
+
import { line, stamp } from '../server/engine/log.ts'
|
|
17
|
+
|
|
18
|
+
test('시각은 밀리초와 오프셋까지 적는다 — 같은 초 안의 순서를 가릴 수 있게', () => {
|
|
19
|
+
const s = stamp(new Date(2026, 7, 22, 5, 54, 12, 345))
|
|
20
|
+
assert.match(s, /^2026-08-22T05:54:12\.345[+-]\d\d:\d\d$/, `형식이 다르다: ${s}`)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('지역 시각으로 적는다 — 프레임워크의 다른 줄과 같은 기준', () => {
|
|
24
|
+
const at = new Date(2026, 0, 2, 3, 4, 5, 6)
|
|
25
|
+
const s = stamp(at)
|
|
26
|
+
assert.ok(s.startsWith('2026-01-02T03:04:05.006'), `지역 시각이 아니다: ${s}`)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('오류는 스택까지 남는다 — 「어디서 났나」를 잃지 않게', () => {
|
|
30
|
+
const err = new Error('불이 났다')
|
|
31
|
+
const out = line(['[twin-engine] 저장 실패', err])
|
|
32
|
+
assert.ok(out.includes('[twin-engine] 저장 실패'))
|
|
33
|
+
assert.ok(out.includes('불이 났다'))
|
|
34
|
+
assert.ok(out.includes('log-stamp.test'), '스택이 있어야 어느 자리에서 났는지 알 수 있다')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('값이 없어도 있었다는 사실은 남긴다 — 조용히 지우지 않는다', () => {
|
|
38
|
+
assert.equal(line(['a', undefined, null, 0]), 'a undefined null 0')
|
|
39
|
+
const circular: any = { name: 'ㄱ' }
|
|
40
|
+
circular.self = circular
|
|
41
|
+
assert.ok(line(['보고', circular]).startsWith('보고 '), '순환 참조에도 줄이 남아야 한다')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('트윈 소스에 console 직접 호출이 남아 있지 않다 — 한 줄이라도 남으면 그 줄만 시각이 없다', () => {
|
|
45
|
+
const root = fileURLToPath(new URL('../server/', import.meta.url))
|
|
46
|
+
const offenders: string[] = []
|
|
47
|
+
const walk = (dir: string) => {
|
|
48
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
49
|
+
const p = dir + e.name
|
|
50
|
+
if (e.isDirectory()) walk(p + '/')
|
|
51
|
+
else if (e.name.endsWith('.ts') && e.name !== 'log.ts') {
|
|
52
|
+
const src = readFileSync(p, 'utf-8')
|
|
53
|
+
for (const m of src.matchAll(/console\.(log|warn|error)\(/g)) offenders.push(`${p.slice(root.length)} console.${m[1]}`)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
walk(root)
|
|
58
|
+
assert.deepEqual(offenders, [], `시각 없는 로그가 남아 있다 — twinLog·twinWarn·twinError 를 쓴다:\n ${offenders.join('\n ')}`)
|
|
59
|
+
})
|