@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,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* **원본이 말한 적 없는 것** — 미해결 참조를 세는 한 자리. 순수(import 없음).
|
|
4
|
+
*
|
|
5
|
+
* ── 무엇을 답하나 ───────────────────────────────────────────────────────────
|
|
6
|
+
* 원천이 우리 모델에 없는 id 를 보내면 어떻게 되나. 유입 문(`ingestCanonicalRecords`)은 **거부하지
|
|
7
|
+
* 않는다** — 실측으로 확인했다(2026-08-20):
|
|
8
|
+
*
|
|
9
|
+
* 모르는 로케이션 「오타-99」 → 통과
|
|
10
|
+
* 모르는 bizStep 「PUTAWAY_X17」 → 통과
|
|
11
|
+
* 모르는 계량기 「MMXU-오타-99」 → 통과
|
|
12
|
+
*
|
|
13
|
+
* 거부하지 않는 것이 옳다. 원천이 모델보다 먼저 새 로케이션을 말하는 것은 정상이고(구조 갱신은 늦게 온다),
|
|
14
|
+
* 거부하면 그 선행 계측을 잃는다. 그래서 커널은 **흡수하고 표시한다** — 마스터에 없던 로케이션은
|
|
15
|
+
* `origin: 'observed'` 로 승격되어 스냅샷에 실린다(`ObservedReducer.touchLocation`).
|
|
16
|
+
*
|
|
17
|
+
* ── 왜 이 파일이 있나 ───────────────────────────────────────────────────────
|
|
18
|
+
* 그 표시를 읽는 곳이 **모델 인스펙션 화면 하나**였다. 그런데 이 사실을 필요로 하는 사람은 「이 트윈이
|
|
19
|
+
* 현장과 맞춰지고 있나」를 보는 사람이다 — 원천이 우리가 모르는 로케이션만 보내고 있으면 트윈은 도는데
|
|
20
|
+
* 스키매틱은 비어 있고, 그것이 우리가 「빈 공장」이라 불러 온 상태다.
|
|
21
|
+
*
|
|
22
|
+
* 도출을 두 곳에 적으면 두 화면이 다른 수를 말하게 된다. 그래서 여기 한 벌만 둔다.
|
|
23
|
+
*
|
|
24
|
+
* ── 판정을 바꾸지 않는다 ────────────────────────────────────────────────────
|
|
25
|
+
* 동기화 판정(`syncVerdictOf`)은 **피드**를 말한다: 오고 있나, 통과하나. 이 갭은 **모델**을 말한다:
|
|
26
|
+
* 원천이 아는 것을 우리가 선언했나. 둘을 한 판정에 섞으면 「유입은 정상인데 모델이 뒤처졌다」와
|
|
27
|
+
* 「유입이 막혔다」가 같은 색으로 보이고, 처방이 완전히 다르다.
|
|
28
|
+
*/
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.NOT_COVERED_METERS = exports.GAP_ID_LIMIT = void 0;
|
|
31
|
+
exports.modelGapOf = modelGapOf;
|
|
32
|
+
/**
|
|
33
|
+
* 화면에 내는 id 수 상한.
|
|
34
|
+
*
|
|
35
|
+
* 오타 하나를 찾는 데 스무 개면 충분하고, 원천이 수천 개를 보내는 경우(모델을 아예 안 맞춘 경우)에
|
|
36
|
+
* 목록으로 화면을 채울 이유가 없다. **넘친 수는 함께 낸다** — 조용히 자르지 않는다.
|
|
37
|
+
*/
|
|
38
|
+
exports.GAP_ID_LIMIT = 20;
|
|
39
|
+
/** 계량기 축이 아직 덮이지 않는 이유 — 화면이 그대로 말할 수 있게 이름을 여기 둔다. */
|
|
40
|
+
exports.NOT_COVERED_METERS = 'meters';
|
|
41
|
+
function axisOf(rows) {
|
|
42
|
+
const ids = [];
|
|
43
|
+
let count = 0;
|
|
44
|
+
let unknown = 0;
|
|
45
|
+
for (const r of rows ?? []) {
|
|
46
|
+
if (r?.origin === 'observed') {
|
|
47
|
+
count++;
|
|
48
|
+
if (ids.length < exports.GAP_ID_LIMIT)
|
|
49
|
+
ids.push(String(r.id ?? ''));
|
|
50
|
+
}
|
|
51
|
+
else if (r?.origin !== 'master') {
|
|
52
|
+
/* 표시가 없다 — 마스터로도 관측으로도 세지 않는다. 모르는 것은 모른다고 센다. */
|
|
53
|
+
unknown++;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { count, ids, more: Math.max(0, count - ids.length), unknown };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* 스냅샷 → 모델 갭.
|
|
60
|
+
*
|
|
61
|
+
* 스냅샷이 없으면 `null` 이다 — **0 이 아니다.** 트윈이 돌지 않으면 비교할 상대가 없고, 「없다」와
|
|
62
|
+
* 「세어 보지 못했다」는 다른 사실이다(모델 인스펙션이 같은 규율을 쓴다).
|
|
63
|
+
*/
|
|
64
|
+
function modelGapOf(snapshot) {
|
|
65
|
+
if (!snapshot)
|
|
66
|
+
return null;
|
|
67
|
+
const locations = axisOf(snapshot.locations);
|
|
68
|
+
const equipment = axisOf(snapshot.equipment);
|
|
69
|
+
const persons = axisOf(snapshot.persons);
|
|
70
|
+
const meters = axisOf(snapshot.energy?.points);
|
|
71
|
+
return {
|
|
72
|
+
locations,
|
|
73
|
+
equipment,
|
|
74
|
+
persons,
|
|
75
|
+
meters,
|
|
76
|
+
total: locations.count + equipment.count + persons.count + meters.count,
|
|
77
|
+
unhandled: (snapshot.unhandled ?? []).map(u => ({
|
|
78
|
+
eventType: u.eventType,
|
|
79
|
+
count: u.count,
|
|
80
|
+
/* 마지막 시각이 멈춘 종류는 이미 고쳐진 것이다. 없으면 지어내지 않는다. */
|
|
81
|
+
lastAt: Number.isFinite(u.lastAtMs) ? new Date(u.lastAtMs).toISOString() : null
|
|
82
|
+
})),
|
|
83
|
+
/*
|
|
84
|
+
* 커널은 0 이면 이 칸을 만들지 않는다. 그래서 「없음」은 두 뜻이다 — 에너지 트윈이 아니거나, 세었고
|
|
85
|
+
* 0 이었다. 둘을 가르려면 `energy` 자체가 있는지를 본다: 있으면 세었고 0 이다.
|
|
86
|
+
*/
|
|
87
|
+
unknownEquipmentReports: typeof snapshot.energy?.unknownEquipment === 'number' ? snapshot.energy.unknownEquipment : snapshot.energy ? 0 : null,
|
|
88
|
+
/*
|
|
89
|
+
* 이제 계량기도 덮는다(커널 0.7.39 가 `MeterPointState.origin` 을 낸다). 덮지 못하는 축이 생기면
|
|
90
|
+
* 다시 여기에 이름을 넣는다 — 화면이 「다 찾아 준다」고 말하지 않게.
|
|
91
|
+
*/
|
|
92
|
+
notCovered: []
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=model-gap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-gap.js","sourceRoot":"","sources":["../../server/engine/model-gap.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;;;AA6GH,gCAgCC;AAnHD;;;;;GAKG;AACU,QAAA,YAAY,GAAG,EAAE,CAAA;AAoD9B,uDAAuD;AAC1C,QAAA,kBAAkB,GAAG,QAAQ,CAAA;AAE1C,SAAS,MAAM,CAAC,IAAoD;IAClE,MAAM,GAAG,GAAa,EAAE,CAAA;IACxB,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,IAAI,OAAO,GAAG,CAAC,CAAA;IACf,KAAK,MAAM,CAAC,IAAI,IAAI,IAAI,EAAE,EAAE,CAAC;QAC3B,IAAI,CAAC,EAAE,MAAM,KAAK,UAAU,EAAE,CAAC;YAC7B,KAAK,EAAE,CAAA;YACP,IAAI,GAAG,CAAC,MAAM,GAAG,oBAAY;gBAAE,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;QAC7D,CAAC;aAAM,IAAI,CAAC,EAAE,MAAM,KAAK,QAAQ,EAAE,CAAC;YAClC,kDAAkD;YAClD,OAAO,EAAE,CAAA;QACX,CAAC;IACH,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAA;AACvE,CAAC;AAED;;;;;GAKG;AACH,SAAgB,UAAU,CAAC,QAA2C;IACpE,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAA;IAE1B,MAAM,SAAS,GAAG,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAA;IAC5C,MAAM,SAAS,GAAG,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAA;IAC5C,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;IACxC,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAE9C,OAAO;QACL,SAAS;QACT,SAAS;QACT,OAAO;QACP,MAAM;QACN,KAAK,EAAE,SAAS,CAAC,KAAK,GAAG,SAAS,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK;QACvE,SAAS,EAAE,CAAC,QAAQ,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YAC9C,SAAS,EAAE,CAAC,CAAC,SAAS;YACtB,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,8CAA8C;YAC9C,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,QAAkB,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI;SAC1F,CAAC,CAAC;QACH;;;WAGG;QACH,uBAAuB,EACrB,OAAO,QAAQ,CAAC,MAAM,EAAE,gBAAgB,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI;QACvH;;;WAGG;QACH,UAAU,EAAE,EAAE;KACf,CAAA;AACH,CAAC","sourcesContent":["/*\n * **원본이 말한 적 없는 것** — 미해결 참조를 세는 한 자리. 순수(import 없음).\n *\n * ── 무엇을 답하나 ───────────────────────────────────────────────────────────\n * 원천이 우리 모델에 없는 id 를 보내면 어떻게 되나. 유입 문(`ingestCanonicalRecords`)은 **거부하지\n * 않는다** — 실측으로 확인했다(2026-08-20):\n *\n * 모르는 로케이션 「오타-99」 → 통과\n * 모르는 bizStep 「PUTAWAY_X17」 → 통과\n * 모르는 계량기 「MMXU-오타-99」 → 통과\n *\n * 거부하지 않는 것이 옳다. 원천이 모델보다 먼저 새 로케이션을 말하는 것은 정상이고(구조 갱신은 늦게 온다),\n * 거부하면 그 선행 계측을 잃는다. 그래서 커널은 **흡수하고 표시한다** — 마스터에 없던 로케이션은\n * `origin: 'observed'` 로 승격되어 스냅샷에 실린다(`ObservedReducer.touchLocation`).\n *\n * ── 왜 이 파일이 있나 ───────────────────────────────────────────────────────\n * 그 표시를 읽는 곳이 **모델 인스펙션 화면 하나**였다. 그런데 이 사실을 필요로 하는 사람은 「이 트윈이\n * 현장과 맞춰지고 있나」를 보는 사람이다 — 원천이 우리가 모르는 로케이션만 보내고 있으면 트윈은 도는데\n * 스키매틱은 비어 있고, 그것이 우리가 「빈 공장」이라 불러 온 상태다.\n *\n * 도출을 두 곳에 적으면 두 화면이 다른 수를 말하게 된다. 그래서 여기 한 벌만 둔다.\n *\n * ── 판정을 바꾸지 않는다 ────────────────────────────────────────────────────\n * 동기화 판정(`syncVerdictOf`)은 **피드**를 말한다: 오고 있나, 통과하나. 이 갭은 **모델**을 말한다:\n * 원천이 아는 것을 우리가 선언했나. 둘을 한 판정에 섞으면 「유입은 정상인데 모델이 뒤처졌다」와\n * 「유입이 막혔다」가 같은 색으로 보이고, 처방이 완전히 다르다.\n */\n\n/** 커널 스냅샷에서 우리가 읽는 것만. 나머지 필드는 알 필요가 없다. */\nexport interface ModelGapSource {\n locations?: { id?: string; origin?: string }[]\n equipment?: { id?: string; origin?: string }[]\n persons?: { id?: string; origin?: string }[]\n /** 커널이 담을 줄 몰라 세어 둔 사건 — 종류별 개수와 처음·마지막 시각. */\n unhandled?: { eventType: string; count: number; firstAtMs?: number; lastAtMs?: number }[]\n /**\n * EMS 상태 — **`energy` 안에 있다.**\n *\n * `unknownEquipment` 를 루트에서 읽고 있었다(2026-08-20에 잡음). 커널은 그것을 `EnergyState` 안에\n * 담으므로, 루트에서 읽으면 **영원히 `null`** 이다 — 오류도 없이 「에너지 트윈이 아니다」로 읽힌다.\n * 나만 읽는 필드를 잘못된 경로에서 읽으면 이렇게 조용히 사라진다.\n *\n * 계량 지점의 `origin` 은 **없을 수 있다**(옛 스냅샷에서 이어받은 지점). 그때는 「선언되었다」가\n * 아니라 **모른다**다.\n */\n energy?: {\n points?: { id?: string; origin?: string }[]\n /** 모델이 모르는 설비가 에너지 상태를 보내 온 횟수. 0 이면 커널이 이 칸을 만들지 않는다. */\n unknownEquipment?: number\n }\n}\n\n/**\n * 화면에 내는 id 수 상한.\n *\n * 오타 하나를 찾는 데 스무 개면 충분하고, 원천이 수천 개를 보내는 경우(모델을 아예 안 맞춘 경우)에\n * 목록으로 화면을 채울 이유가 없다. **넘친 수는 함께 낸다** — 조용히 자르지 않는다.\n */\nexport const GAP_ID_LIMIT = 20\n\nexport interface ModelGapAxis {\n /** 관측으로만 알게 된 id 수. */\n count: number\n /** 그중 화면에 싣는 것(최대 `GAP_ID_LIMIT`). */\n ids: string[]\n /** 상한에서 밀려난 수. 0 이면 전부 실었다. */\n more: number\n /**\n * 출처를 **모르는** 수 — 표시가 없는 항목.\n *\n * 「선언되었다」로 세지 않는다. 계량기는 이 표시가 늦게 생겼으므로 옛 스냅샷에서 이어받은 지점에는\n * 칸이 없다(`undefined`). 그것을 마스터로 세면 오타를 놓치고, 관측으로 세면 정상을 문제로 만든다.\n * 둘 다 틀리므로 **모른다고 센다.**\n */\n unknown: number\n}\n\nexport interface ModelGapView {\n locations: ModelGapAxis\n equipment: ModelGapAxis\n persons: ModelGapAxis\n /**\n * EMS 계량 지점.\n *\n * ── 이 축이 가장 무겁다 ────────────────────────────────────────────────────\n * 모르는 `meterId` 는 조용히 새 계량기가 되고 그 kW 가 그 현장의 **수요 구간·피크·요금 판정**에\n * 더해진다. 오타 하나면 진짜 계량기는 멈추고 유령이 누적된다 — 숫자는 그럴듯하고 틀린다. 자리 오타는\n * 스키매틱이 비는 것으로 드러나지만, 이건 그럴듯한 숫자로 나온다.\n *\n * 커널이 `origin` 을 **표본마다 다시 맞춘다** — 선언의 정본은 모델이고 모델은 채택으로 늘어나므로,\n * 한 번 찍고 두면 나중에 선언된 계량기가 영원히 `observed` 로 남는다. 그래서 이 값은 「지금 모델에\n * 있는가」의 답이고, 「한 번 오타였다」의 답이 아니다.\n */\n meters: ModelGapAxis\n /** 세 축의 합 — 화면이 「있나 없나」를 한 번에 묻기 위한 값. */\n total: number\n /** 커널이 담을 줄 모른 사건들(종류별). 참조 문제와 다른 사실이므로 따로 낸다. */\n unhandled: { eventType: string; count: number; lastAt: string | null }[]\n /** EMS: 모델이 모르는 설비의 에너지 보고 횟수. 없으면 `null`(0 과 구별한다). */\n unknownEquipmentReports: number | null\n /**\n * 아직 덮지 못하는 축 — **모르는 것을 모른다고 말한다.**\n *\n * 계량기는 커널 0.7.39 부터 덮인다(그 전에는 `origin` 표시가 없어 오타를 찾아 줄 수 없었다).\n * 비어 있는 것이 정상이고, 새 축이 표시 없이 들어오면 여기에 이름을 넣는다 — 화면이 「다 찾아\n * 준다」고 말하지 않게.\n */\n notCovered: string[]\n}\n\n/** 계량기 축이 아직 덮이지 않는 이유 — 화면이 그대로 말할 수 있게 이름을 여기 둔다. */\nexport const NOT_COVERED_METERS = 'meters'\n\nfunction axisOf(rows: { id?: string; origin?: string }[] | undefined): ModelGapAxis {\n const ids: string[] = []\n let count = 0\n let unknown = 0\n for (const r of rows ?? []) {\n if (r?.origin === 'observed') {\n count++\n if (ids.length < GAP_ID_LIMIT) ids.push(String(r.id ?? ''))\n } else if (r?.origin !== 'master') {\n /* 표시가 없다 — 마스터로도 관측으로도 세지 않는다. 모르는 것은 모른다고 센다. */\n unknown++\n }\n }\n return { count, ids, more: Math.max(0, count - ids.length), unknown }\n}\n\n/**\n * 스냅샷 → 모델 갭.\n *\n * 스냅샷이 없으면 `null` 이다 — **0 이 아니다.** 트윈이 돌지 않으면 비교할 상대가 없고, 「없다」와\n * 「세어 보지 못했다」는 다른 사실이다(모델 인스펙션이 같은 규율을 쓴다).\n */\nexport function modelGapOf(snapshot: ModelGapSource | null | undefined): ModelGapView | null {\n if (!snapshot) return null\n\n const locations = axisOf(snapshot.locations)\n const equipment = axisOf(snapshot.equipment)\n const persons = axisOf(snapshot.persons)\n const meters = axisOf(snapshot.energy?.points)\n\n return {\n locations,\n equipment,\n persons,\n meters,\n total: locations.count + equipment.count + persons.count + meters.count,\n unhandled: (snapshot.unhandled ?? []).map(u => ({\n eventType: u.eventType,\n count: u.count,\n /* 마지막 시각이 멈춘 종류는 이미 고쳐진 것이다. 없으면 지어내지 않는다. */\n lastAt: Number.isFinite(u.lastAtMs) ? new Date(u.lastAtMs as number).toISOString() : null\n })),\n /*\n * 커널은 0 이면 이 칸을 만들지 않는다. 그래서 「없음」은 두 뜻이다 — 에너지 트윈이 아니거나, 세었고\n * 0 이었다. 둘을 가르려면 `energy` 자체가 있는지를 본다: 있으면 세었고 0 이다.\n */\n unknownEquipmentReports:\n typeof snapshot.energy?.unknownEquipment === 'number' ? snapshot.energy.unknownEquipment : snapshot.energy ? 0 : null,\n /*\n * 이제 계량기도 덮는다(커널 0.7.39 가 `MeterPointState.origin` 을 낸다). 덮지 못하는 축이 생기면\n * 다시 여기에 이름을 넣는다 — 화면이 「다 찾아 준다」고 말하지 않게.\n */\n notCovered: []\n }\n}\n"]}
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
export type RestartPolicy = 'resync' | 'resume' | 'reset';
|
|
3
3
|
export declare const RESTART_POLICIES: readonly RestartPolicy[];
|
|
4
4
|
/**
|
|
5
|
-
* 저장된 값을 정책으로 읽는다 — **모르면
|
|
5
|
+
* 저장된 값을 정책으로 읽는다 — **모르면 오류를 낸다**(기본값으로 메우지 않는다).
|
|
6
6
|
*
|
|
7
7
|
* 옛 어휘(`mirror`·`sim-world`·`sim-experiment`)를 여기서 번역하지 않는다: 번역을 두면 옛 값이 살아남고,
|
|
8
8
|
* 그 값이 다시 어딘가에 저장된다. 저장된 값은 **한 번 옮기고**(데이터 이관) 코드는 새 어휘만 안다.
|
|
9
|
-
* 그래서 이 함수는 옛 값에 대해서도
|
|
9
|
+
* 그래서 이 함수는 옛 값에 대해서도 오류를 내며, 무엇으로 옮겨야 하는지 문장으로 말한다.
|
|
10
10
|
*/
|
|
11
11
|
export declare function readRestartPolicy(raw: unknown, where: string): RestartPolicy;
|
|
12
|
-
/** 이 값이 정책인가 —
|
|
12
|
+
/** 이 값이 정책인가 — 오류를 내지 않고 묻는 자리(화면·검사)를 위해. */
|
|
13
13
|
export declare function isRestartPolicy(raw: unknown): raw is RestartPolicy;
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* ── 기본값을 두지 않는다 ────────────────────────────────────────────────────
|
|
20
20
|
* 예전에는 미선언이 조용히 `sim-experiment`(=저널 초기화)로 떨어졌다. 그 관용은 값이 하나 어긋나는
|
|
21
21
|
* 순간 **저널을 지우는 길**이 된다 — 개명 뒤 옛 값이 남아 있는 행을 그 길로 읽으면 재기동 한 번에
|
|
22
|
-
* 이력이 사라진다. 그래서 모르는 값은
|
|
22
|
+
* 이력이 사라진다. 그래서 모르는 값은 **오류를 낸다.** 부르는 쪽이 선언하게 만드는 것이 이 축의 요점이다.
|
|
23
23
|
*/
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
25
|
exports.RESTART_POLICIES = void 0;
|
|
@@ -27,11 +27,11 @@ exports.readRestartPolicy = readRestartPolicy;
|
|
|
27
27
|
exports.isRestartPolicy = isRestartPolicy;
|
|
28
28
|
exports.RESTART_POLICIES = ['resync', 'resume', 'reset'];
|
|
29
29
|
/**
|
|
30
|
-
* 저장된 값을 정책으로 읽는다 — **모르면
|
|
30
|
+
* 저장된 값을 정책으로 읽는다 — **모르면 오류를 낸다**(기본값으로 메우지 않는다).
|
|
31
31
|
*
|
|
32
32
|
* 옛 어휘(`mirror`·`sim-world`·`sim-experiment`)를 여기서 번역하지 않는다: 번역을 두면 옛 값이 살아남고,
|
|
33
33
|
* 그 값이 다시 어딘가에 저장된다. 저장된 값은 **한 번 옮기고**(데이터 이관) 코드는 새 어휘만 안다.
|
|
34
|
-
* 그래서 이 함수는 옛 값에 대해서도
|
|
34
|
+
* 그래서 이 함수는 옛 값에 대해서도 오류를 내며, 무엇으로 옮겨야 하는지 문장으로 말한다.
|
|
35
35
|
*/
|
|
36
36
|
function readRestartPolicy(raw, where) {
|
|
37
37
|
const value = typeof raw === 'string' ? raw.trim() : '';
|
|
@@ -45,7 +45,7 @@ function readRestartPolicy(raw, where) {
|
|
|
45
45
|
throw new Error(`${where}: restart policy ${value ? `"${value}"` : 'is missing'} — declare one of ${exports.RESTART_POLICIES.join(' | ')}. ` +
|
|
46
46
|
'There is no default: an unspecified policy used to fall through to "reset", which erases the journal on the next reboot.');
|
|
47
47
|
}
|
|
48
|
-
/** 이 값이 정책인가 —
|
|
48
|
+
/** 이 값이 정책인가 — 오류를 내지 않고 묻는 자리(화면·검사)를 위해. */
|
|
49
49
|
function isRestartPolicy(raw) {
|
|
50
50
|
return typeof raw === 'string' && exports.RESTART_POLICIES.includes(raw);
|
|
51
51
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"restart-policy.js","sourceRoot":"","sources":["../../server/engine/restart-policy.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;;;AAcH,8CAcC;AAGD,0CAEC;AA5BY,QAAA,gBAAgB,GAA6B,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,CAAU,CAAA;AAEhG;;;;;;GAMG;AACH,SAAgB,iBAAiB,CAAC,GAAY,EAAE,KAAa;IAC3D,MAAM,KAAK,GAAG,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;IACvD,IAAK,wBAAsC,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,KAAsB,CAAA;IAC1F,MAAM,MAAM,GAAkC,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,gBAAgB,EAAE,OAAO,EAAE,CAAA;IACpH,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CACb,GAAG,KAAK,qBAAqB,KAAK,uDAAuD,MAAM,CAAC,KAAK,CAAC,IAAI;YACxG,4IAA4I,CAC/I,CAAA;IACH,CAAC;IACD,MAAM,IAAI,KAAK,CACb,GAAG,KAAK,oBAAoB,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,YAAY,qBAAqB,wBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI;QAClH,0HAA0H,CAC7H,CAAA;AACH,CAAC;AAED,
|
|
1
|
+
{"version":3,"file":"restart-policy.js","sourceRoot":"","sources":["../../server/engine/restart-policy.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;;;AAcH,8CAcC;AAGD,0CAEC;AA5BY,QAAA,gBAAgB,GAA6B,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,CAAU,CAAA;AAEhG;;;;;;GAMG;AACH,SAAgB,iBAAiB,CAAC,GAAY,EAAE,KAAa;IAC3D,MAAM,KAAK,GAAG,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;IACvD,IAAK,wBAAsC,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,KAAsB,CAAA;IAC1F,MAAM,MAAM,GAAkC,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,gBAAgB,EAAE,OAAO,EAAE,CAAA;IACpH,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CACb,GAAG,KAAK,qBAAqB,KAAK,uDAAuD,MAAM,CAAC,KAAK,CAAC,IAAI;YACxG,4IAA4I,CAC/I,CAAA;IACH,CAAC;IACD,MAAM,IAAI,KAAK,CACb,GAAG,KAAK,oBAAoB,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,YAAY,qBAAqB,wBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI;QAClH,0HAA0H,CAC7H,CAAA;AACH,CAAC;AAED,8CAA8C;AAC9C,SAAgB,eAAe,CAAC,GAAY;IAC1C,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAK,wBAAsC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAA;AACzF,CAAC","sourcesContent":["/*\n * **재기동 정책** — 이 트윈은 다시 세워질 때 자기 과거를 어떻게 대하나.\n *\n * ── 왜 이 이름인가 (ADR-0029 · plans/simulator-as-source.md §2) ─────────────\n * 옛 이름은 `realityMode` 였고 값은 `mirror`·`sim-world`·`sim-experiment` 였다. 그 축은 「현실이 어디서\n * 오나」를 말하는 것처럼 보였지만, 코드가 그것으로 가른 것은 **재기동 거동 하나**였다. 그리고 「현실이\n * 어디서 오나」는 ADR-0029 가 **원본의 종류**로 옮긴 물음이다(`TwinReference.adapterType`) — 트윈은 한\n * 종류이고 언제나 원본을 반영한다.\n *\n * 그래서 이 축은 사라지지 않고 **트윈에 남아 이름을 바꿨다.** 남는 이유: 「자기 저널을 잇는가」는 원본의\n * 성질이 아니라 **트윈과 자기 과거의 관계**다. 같은 시뮬레이터 원본을 보는 두 트윈이 하나는 이어가고\n * 하나는 매번 처음부터 재현할 수 있다 — 원본은 그 둘을 구별하지 못한다.\n *\n * resync — 외부 실물에서 다시 읽는다(원본이 진실이므로 우리 과거를 이어 붙이지 않는다).\n * resume — 저널을 이어 세운다(이 트윈이 낳은 타임라인이 곧 사실이다).\n * reset — 저널을 비우고 씨앗부터 다시 돌린다(재현 가능한 실험).\n *\n * ── 기본값을 두지 않는다 ────────────────────────────────────────────────────\n * 예전에는 미선언이 조용히 `sim-experiment`(=저널 초기화)로 떨어졌다. 그 관용은 값이 하나 어긋나는\n * 순간 **저널을 지우는 길**이 된다 — 개명 뒤 옛 값이 남아 있는 행을 그 길로 읽으면 재기동 한 번에\n * 이력이 사라진다. 그래서 모르는 값은 **오류를 낸다.** 부르는 쪽이 선언하게 만드는 것이 이 축의 요점이다.\n */\n\n/** 재기동 정책 — 셋뿐이다. */\nexport type RestartPolicy = 'resync' | 'resume' | 'reset'\n\nexport const RESTART_POLICIES: readonly RestartPolicy[] = ['resync', 'resume', 'reset'] as const\n\n/**\n * 저장된 값을 정책으로 읽는다 — **모르면 오류를 낸다**(기본값으로 메우지 않는다).\n *\n * 옛 어휘(`mirror`·`sim-world`·`sim-experiment`)를 여기서 번역하지 않는다: 번역을 두면 옛 값이 살아남고,\n * 그 값이 다시 어딘가에 저장된다. 저장된 값은 **한 번 옮기고**(데이터 이관) 코드는 새 어휘만 안다.\n * 그래서 이 함수는 옛 값에 대해서도 오류를 내며, 무엇으로 옮겨야 하는지 문장으로 말한다.\n */\nexport function readRestartPolicy(raw: unknown, where: string): RestartPolicy {\n const value = typeof raw === 'string' ? raw.trim() : ''\n if ((RESTART_POLICIES as readonly string[]).includes(value)) return value as RestartPolicy\n const legacy: Record<string, RestartPolicy> = { mirror: 'resync', 'sim-world': 'resume', 'sim-experiment': 'reset' }\n if (legacy[value]) {\n throw new Error(\n `${where}: restart policy \"${value}\" is the old vocabulary — move the stored value to \"${legacy[value]}\" ` +\n '(ADR-0029 · plans/simulator-as-source.md §2). Nothing is translated at read time on purpose: a translation would keep the old value alive.'\n )\n }\n throw new Error(\n `${where}: restart policy ${value ? `\"${value}\"` : 'is missing'} — declare one of ${RESTART_POLICIES.join(' | ')}. ` +\n 'There is no default: an unspecified policy used to fall through to \"reset\", which erases the journal on the next reboot.'\n )\n}\n\n/** 이 값이 정책인가 — 오류를 내지 않고 묻는 자리(화면·검사)를 위해. */\nexport function isRestartPolicy(raw: unknown): raw is RestartPolicy {\n return typeof raw === 'string' && (RESTART_POLICIES as readonly string[]).includes(raw)\n}\n"]}
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* `${domainId}:${instanceId}` — 이 저장소가 이미 쓰는 모양이다(`structureRevCache`·`measuredCache`).
|
|
27
27
|
* 새 모양을 만들지 않는다. 파싱은 **첫 구분자**에서 자르므로 `instanceId` 에 `:` 가 들어 있어도
|
|
28
28
|
* 안전하다(레퍼런스 소스 이름에 들어갈 수 있다). 대신 `domainId` 에 구분자가 있으면 키가 모호해지므로
|
|
29
|
-
* **그때는
|
|
29
|
+
* **그때는 오류를 낸다** — 조용히 잘못된 키를 만들면 그게 곧 이 결함의 재발이다.
|
|
30
30
|
*/
|
|
31
31
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
32
|
exports.RUNTIME_KEY_SEP = void 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-key.js","sourceRoot":"","sources":["../../server/engine/runtime-key.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;;;AAUH,gCAOC;AAGD,0CAIC;AAGD,gCAEC;AA3BD,kDAAkD;AACrC,QAAA,eAAe,GAAG,GAAG,CAAA;AAElC;;;;GAIG;AACH,SAAgB,UAAU,CAAC,QAAgB,EAAE,UAAkB;IAC7D,IAAI,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,8EAA8E,CAAC,CAAA;IAC9G,IAAI,CAAC,UAAU;QAAE,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAA;IACtE,IAAI,QAAQ,CAAC,QAAQ,CAAC,uBAAe,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CAAC,0CAA0C,uBAAe,kCAAkC,CAAC,CAAA;IAC9G,CAAC;IACD,OAAO,GAAG,QAAQ,GAAG,uBAAe,GAAG,UAAU,EAAE,CAAA;AACrD,CAAC;AAED,iDAAiD;AACjD,SAAgB,eAAe,CAAC,GAAW;IACzC,MAAM,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC,uBAAe,CAAC,CAAA;IACvC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,GAAG,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,qBAAqB,GAAG,wBAAwB,CAAC,CAAA;IACvG,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAA;AACtE,CAAC;AAED,iDAAiD;AACjD,SAAgB,UAAU,CAAC,GAAW,EAAE,QAAgB;IACtD,OAAO,CAAC,CAAC,QAAQ,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,QAAQ,GAAG,uBAAe,EAAE,CAAC,CAAA;AACtE,CAAC","sourcesContent":["/*\n * 런타임 레지스트리 키 — **트윈의 정체성은 `(도메인, 인스턴스 id)` 다.** 순수.\n *\n * ── 왜 있나 (2026-08-06 테넌트 격리 전수 확인) ───────────────────────────────\n * `TwinEngine.instances` 와 `recovered` 가 **`instanceId` 하나로** 키를 잡고 있었다. 그런데 DB 유일성은\n * `[domain, instanceId]` 이고(`TwinInstance` 인덱스) 트윈의 정체성도 그쪽이다. 즉 런타임 맵만 **전역\n * 이름 공간**이었다.\n *\n * **충돌은 이론이 아니다.** 레퍼런스 경로가 `instanceId` 로 **레퍼런스 소스 이름**을 쓴다\n * (`sap-ewm-1710`). 벤더 시스템 이름은 테넌트가 **공유하는 어휘**라, 두 테넌트가 같은 시스템에\n * 연결하면 같은 id 가 된다 — 드문 사고가 아니라 예상되는 경우다.\n *\n * 그래서 무엇이 어긋났나:\n * · `startLive` 의 조기 반환이 A 의 런타임을 B 에게 돌려주고 `ingestLive` 는 도메인을 확인하지 않아\n * **B 의 라이브 이벤트가 A 의 커널에 적용**될 수 있었다.\n * · `recovered` 가 겹치면 웜스타트 씨앗이 **남의 스냅샷**이 되어 재고가 섞인다.\n * · `start`/`stop` 이 남의 런타임을 집으므로 B 는 \"기동됨\" 을 받고도 이후 전부 거부됐다.\n *\n * 이 부류는 **이미 한 번 물었다** — `twin-lifecycle-mutation.ts` 주석이 증언한다: \"과거엔\n * `Object.keys(TwinEngine.instances)` 로 전역 반환 → 다른 도메인 트윈이 새던 버그.\" 그 자리만 고쳤다.\n * 그래서 이번에는 가드를 더 붙이지 않고 **키를 고친다** — 저수준 메서드가 `domainId` 를 받게 되는 것이\n * 요점이다. \"누구의 트윈인가\" 를 말하지 않고는 만질 수 없다.\n *\n * ── 구분자 규약 ─────────────────────────────────────────────────────────────\n * `${domainId}:${instanceId}` — 이 저장소가 이미 쓰는 모양이다(`structureRevCache`·`measuredCache`).\n * 새 모양을 만들지 않는다. 파싱은 **첫 구분자**에서 자르므로 `instanceId` 에 `:` 가 들어 있어도\n * 안전하다(레퍼런스 소스 이름에 들어갈 수 있다). 대신 `domainId` 에 구분자가 있으면 키가 모호해지므로\n * **그때는
|
|
1
|
+
{"version":3,"file":"runtime-key.js","sourceRoot":"","sources":["../../server/engine/runtime-key.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;;;AAUH,gCAOC;AAGD,0CAIC;AAGD,gCAEC;AA3BD,kDAAkD;AACrC,QAAA,eAAe,GAAG,GAAG,CAAA;AAElC;;;;GAIG;AACH,SAAgB,UAAU,CAAC,QAAgB,EAAE,UAAkB;IAC7D,IAAI,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,8EAA8E,CAAC,CAAA;IAC9G,IAAI,CAAC,UAAU;QAAE,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAA;IACtE,IAAI,QAAQ,CAAC,QAAQ,CAAC,uBAAe,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CAAC,0CAA0C,uBAAe,kCAAkC,CAAC,CAAA;IAC9G,CAAC;IACD,OAAO,GAAG,QAAQ,GAAG,uBAAe,GAAG,UAAU,EAAE,CAAA;AACrD,CAAC;AAED,iDAAiD;AACjD,SAAgB,eAAe,CAAC,GAAW;IACzC,MAAM,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC,uBAAe,CAAC,CAAA;IACvC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,GAAG,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,qBAAqB,GAAG,wBAAwB,CAAC,CAAA;IACvG,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAA;AACtE,CAAC;AAED,iDAAiD;AACjD,SAAgB,UAAU,CAAC,GAAW,EAAE,QAAgB;IACtD,OAAO,CAAC,CAAC,QAAQ,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,QAAQ,GAAG,uBAAe,EAAE,CAAC,CAAA;AACtE,CAAC","sourcesContent":["/*\n * 런타임 레지스트리 키 — **트윈의 정체성은 `(도메인, 인스턴스 id)` 다.** 순수.\n *\n * ── 왜 있나 (2026-08-06 테넌트 격리 전수 확인) ───────────────────────────────\n * `TwinEngine.instances` 와 `recovered` 가 **`instanceId` 하나로** 키를 잡고 있었다. 그런데 DB 유일성은\n * `[domain, instanceId]` 이고(`TwinInstance` 인덱스) 트윈의 정체성도 그쪽이다. 즉 런타임 맵만 **전역\n * 이름 공간**이었다.\n *\n * **충돌은 이론이 아니다.** 레퍼런스 경로가 `instanceId` 로 **레퍼런스 소스 이름**을 쓴다\n * (`sap-ewm-1710`). 벤더 시스템 이름은 테넌트가 **공유하는 어휘**라, 두 테넌트가 같은 시스템에\n * 연결하면 같은 id 가 된다 — 드문 사고가 아니라 예상되는 경우다.\n *\n * 그래서 무엇이 어긋났나:\n * · `startLive` 의 조기 반환이 A 의 런타임을 B 에게 돌려주고 `ingestLive` 는 도메인을 확인하지 않아\n * **B 의 라이브 이벤트가 A 의 커널에 적용**될 수 있었다.\n * · `recovered` 가 겹치면 웜스타트 씨앗이 **남의 스냅샷**이 되어 재고가 섞인다.\n * · `start`/`stop` 이 남의 런타임을 집으므로 B 는 \"기동됨\" 을 받고도 이후 전부 거부됐다.\n *\n * 이 부류는 **이미 한 번 물었다** — `twin-lifecycle-mutation.ts` 주석이 증언한다: \"과거엔\n * `Object.keys(TwinEngine.instances)` 로 전역 반환 → 다른 도메인 트윈이 새던 버그.\" 그 자리만 고쳤다.\n * 그래서 이번에는 가드를 더 붙이지 않고 **키를 고친다** — 저수준 메서드가 `domainId` 를 받게 되는 것이\n * 요점이다. \"누구의 트윈인가\" 를 말하지 않고는 만질 수 없다.\n *\n * ── 구분자 규약 ─────────────────────────────────────────────────────────────\n * `${domainId}:${instanceId}` — 이 저장소가 이미 쓰는 모양이다(`structureRevCache`·`measuredCache`).\n * 새 모양을 만들지 않는다. 파싱은 **첫 구분자**에서 자르므로 `instanceId` 에 `:` 가 들어 있어도\n * 안전하다(레퍼런스 소스 이름에 들어갈 수 있다). 대신 `domainId` 에 구분자가 있으면 키가 모호해지므로\n * **그때는 오류를 낸다** — 조용히 잘못된 키를 만들면 그게 곧 이 결함의 재발이다.\n */\n\n/** 도메인과 인스턴스 id 를 잇는 문자 — 저장소의 기존 캐시 키와 같은 모양. */\nexport const RUNTIME_KEY_SEP = ':'\n\n/**\n * 런타임 레지스트리 키.\n *\n * @throws 도메인 id 가 비었거나 구분자를 담고 있으면(키가 모호해진다). 인스턴스 id 가 비었으면.\n */\nexport function runtimeKey(domainId: string, instanceId: string): string {\n if (!domainId) throw new Error('runtimeKey: domainId is required — a twin runtime always belongs to a tenant')\n if (!instanceId) throw new Error('runtimeKey: instanceId is required')\n if (domainId.includes(RUNTIME_KEY_SEP)) {\n throw new Error(`runtimeKey: domainId must not contain \"${RUNTIME_KEY_SEP}\" (would make the key ambiguous)`)\n }\n return `${domainId}${RUNTIME_KEY_SEP}${instanceId}`\n}\n\n/** 키를 되돌린다 — 첫 구분자에서 자른다(인스턴스 id 안의 구분자는 보존). */\nexport function parseRuntimeKey(key: string): { domainId: string; instanceId: string } {\n const at = key.indexOf(RUNTIME_KEY_SEP)\n if (at <= 0 || at === key.length - 1) throw new Error(`parseRuntimeKey: \"${key}\" is not a runtime key`)\n return { domainId: key.slice(0, at), instanceId: key.slice(at + 1) }\n}\n\n/** 이 키가 이 테넌트의 것인가 — 레지스트리를 조회할 때 도메인으로 걸러낸다. */\nexport function isOfDomain(key: string, domainId: string): boolean {\n return !!domainId && key.startsWith(`${domainId}${RUNTIME_KEY_SEP}`)\n}\n"]}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export type StageId = 'structure' | 'live' | 'forecast' | 'closed-loop';
|
|
2
|
+
/** 판정에 쓰는 사실. `null` = **계측되지 않았다**(0 이 아니다). */
|
|
3
|
+
export interface StageEvidence {
|
|
4
|
+
/** 이 트윈이 든 자리 수. */
|
|
5
|
+
locationCount: number;
|
|
6
|
+
/** 자리에 놓인 물품 수(비직렬 수량 물품 포함 — 식품·화학은 개수가 아니라 총량으로 온다). */
|
|
7
|
+
itemCount: number;
|
|
8
|
+
/**
|
|
9
|
+
* 마스터를 옮기며 **못 옮긴 값의 수**. `null` = 세지 않았다.
|
|
10
|
+
*
|
|
11
|
+
* 0 이면 통과이고 n 이어도 통과다 — 이 판정이 요구하는 것은 「없다」가 아니라 **「말할 수 있다」**다.
|
|
12
|
+
* 못 옮긴 값을 세지 않은 구조 인제스트는 도크를 랙으로 바꿔 놓고도 조용히 성공한다.
|
|
13
|
+
*/
|
|
14
|
+
masterWarnings: number | null;
|
|
15
|
+
/**
|
|
16
|
+
* 이 축이 이 트윈에 **해당하나**. 레퍼런스 없이 템플릿으로 세운 트윈에는 옮겨 온 마스터가 없으므로
|
|
17
|
+
* 「못 옮긴 값」을 물을 대상이 아니다 — 그걸 미측정으로 접으면 영원히 배선을 기다린다.
|
|
18
|
+
*/
|
|
19
|
+
masterWarningsApplicable?: boolean;
|
|
20
|
+
/** 인제스트 누적 건수. */
|
|
21
|
+
liveIngestTotal: number;
|
|
22
|
+
/** **반출(remove)을 접은 누적 건수.** `null` = 계측되지 않음. 0 = 세었으나 없었다(→ 스냅샷 의심). */
|
|
23
|
+
removalsObserved: number | null;
|
|
24
|
+
/** 이 트윈에서 예측이 한 번이라도 돌았나. */
|
|
25
|
+
forecastRan: boolean;
|
|
26
|
+
/** 선언 적재량 — `declared`/`total`. `null` = 알 수 없음. */
|
|
27
|
+
specCoverage: {
|
|
28
|
+
declared: number;
|
|
29
|
+
total: number;
|
|
30
|
+
} | null;
|
|
31
|
+
/**
|
|
32
|
+
* 이 트윈이 **무엇인가** — 시뮬레이션은 원천에서 받는 것이 없다.
|
|
33
|
+
*
|
|
34
|
+
* 시뮬에 라이브를 요구하면 영원히 빨갛고, 그 빨강은 결함이 아니다. 도입 단계는 **연결된 트윈**의
|
|
35
|
+
* 축이므로 시뮬은 라이브 이후를 「해당 없음」으로 말한다(달성으로도, 미측정으로도 접지 않는다).
|
|
36
|
+
*/
|
|
37
|
+
subject?: 'live' | 'sim';
|
|
38
|
+
/** 피드 성격 — `snapshot-poll` 이면 반출 0 은 「아직」이 아니라 **구조적으로 불가**다. */
|
|
39
|
+
feed?: 'push' | 'delta-poll' | 'snapshot-poll' | 'none' | 'unknown';
|
|
40
|
+
/** 원본이 **선언∩구현**으로 액추에이션 능력을 갖나(`capabilitiesOf`). */
|
|
41
|
+
actuateCapability: boolean;
|
|
42
|
+
/** 내 명령의 효과가 인바운드로 **상관되어** 돌아온 누적 건수. `null` = 계측되지 않음. */
|
|
43
|
+
correlatedEffectObserved: number | null;
|
|
44
|
+
}
|
|
45
|
+
export interface StageVerdict {
|
|
46
|
+
id: StageId;
|
|
47
|
+
achieved: boolean;
|
|
48
|
+
/** 아직 아닌 이유 — 언어중립 코드(화면이 옮긴다). 달성이면 빈 배열. */
|
|
49
|
+
blockedBy: string[];
|
|
50
|
+
/** 판정할 수 없었던 신호 — **배선되지 않은 계측**. 있으면 달성이 아니다. */
|
|
51
|
+
unmeasured: string[];
|
|
52
|
+
}
|
|
53
|
+
export interface StagePath {
|
|
54
|
+
stages: StageVerdict[];
|
|
55
|
+
/** 지금 서 있는 칸 — 연속으로 달성된 마지막 단계. 하나도 없으면 `null`. */
|
|
56
|
+
reached: StageId | null;
|
|
57
|
+
/**
|
|
58
|
+
* 다음 한 걸음 — 사람이 오늘 할 수 있는 일 하나. 없으면(전부 달성) `null`.
|
|
59
|
+
* 「무엇이 부족한지」의 목록이 아니라 **다음 하나**다. 목록은 사람을 멈추게 한다.
|
|
60
|
+
*/
|
|
61
|
+
next: {
|
|
62
|
+
stage: StageId;
|
|
63
|
+
blockedBy: string[];
|
|
64
|
+
unmeasured: string[];
|
|
65
|
+
} | null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 증거로 단계를 판정한다.
|
|
69
|
+
*
|
|
70
|
+
* **단계는 앞선 단계를 전제한다**(구조 없이 라이브를 달성했다고 말할 수 없다). 앞이 막히면 뒤는
|
|
71
|
+
* 자기 조건을 따지지 않고 `prerequisite` 으로 막는다 — 뒤 단계의 조건을 나열하면 사람이 순서를
|
|
72
|
+
* 건너뛴다.
|
|
73
|
+
*/
|
|
74
|
+
export declare function assessStagePath(e: StageEvidence): StagePath;
|
|
75
|
+
/**
|
|
76
|
+
* 이 단계에 이르지 못했다면 **쓰지 않기로 한 말**.
|
|
77
|
+
*
|
|
78
|
+
* 근거 수준 규율(`connector-developer-guide` §8)을 판정과 같은 자리에 둔다. 문장이 문서에만 있으면
|
|
79
|
+
* 제안서를 쓰는 사람이 그 문서를 읽지 않는다. 값이 되면 화면이 그 자리에서 말할 수 있다.
|
|
80
|
+
*/
|
|
81
|
+
export declare const CLAIM_GATE: Record<StageId, string>;
|
|
82
|
+
/** 아직 쓸 수 없는 주장들 — 도달하지 못한 단계의 게이트 전부. */
|
|
83
|
+
export declare function forbiddenClaims(path: StagePath): string[];
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* 단계 판정 — **가벼운 성공을 최종 성공으로 잇는 길을, 사람의 체크가 아니라 증거로 센다.**
|
|
4
|
+
*
|
|
5
|
+
* ── 왜 이 모듈이 있나 ───────────────────────────────────────────────────────
|
|
6
|
+
* 트윈 도입은 「구조 → 라이브 → 예측 → 폐루프」로 간다. 그런데 이 진행을 **체크리스트**로 관리하면
|
|
7
|
+
* 사람이 체크하는 순간 참이 된다. 실제로 가장 자주 건너뛰어지는 자리가 구조 다음이다 — 구조가 서면
|
|
8
|
+
* 화면이 그럴듯해지므로 「실시간」이라고 말해 버리고, 되풀어 읽는 스냅샷이 그 말을 뒷받침하지 못한다.
|
|
9
|
+
*
|
|
10
|
+
* 그래서 단계를 **판정식**으로 둔다. 각 단계는 시스템이 이미 내놓는 사실로만 판정되고, 선언으로
|
|
11
|
+
* 위조할 수 없다. 특히:
|
|
12
|
+
*
|
|
13
|
+
* · **반출 관측**이 라이브의 판별식이다. 스냅샷을 되풀어 읽는 방식은 **원리적으로** 반출을 낼 수 없다
|
|
14
|
+
* (사라진 것은 그냥 없어질 뿐이라 `OBSERVE` 만 남는다). 그러므로 「반출을 한 번이라도 접었는가」는
|
|
15
|
+
* 진짜 delta 의 증거이고, 폴링을 실시간이라 부르는 것을 막는다.
|
|
16
|
+
* · **선언 적재량**이 예측의 판별식이다. 커널 기본값으로 돌아간 예측도 숫자를 내므로, 화면만 보면
|
|
17
|
+
* 구별되지 않는다. 그 숫자가 무엇 위에 섰는지는 `specCoverage` 만 안다.
|
|
18
|
+
*
|
|
19
|
+
* ── 이 모듈의 정수: 「모른다」를 「아니다」로도 「그렇다」로도 바꾸지 않는다 ──────────────
|
|
20
|
+
* 계측되지 않은 신호는 `null` 로 온다. `null` 은 0 이 아니다. 계측되지 않았으면 그 단계는 **달성이
|
|
21
|
+
* 아니고**, 그 사실을 `unmeasured` 로 말한다 — 신호를 배선하지 않은 채 단계를 주장할 길을 남기지
|
|
22
|
+
* 않는다. (0 으로 대체하면 「반출 0건」과 「반출을 세지 않았다」가 같아진다.)
|
|
23
|
+
*
|
|
24
|
+
* 순수 함수다(프레임워크·DB 무의존) — 증거 수집은 호출자 몫이다.
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.CLAIM_GATE = void 0;
|
|
28
|
+
exports.assessStagePath = assessStagePath;
|
|
29
|
+
exports.forbiddenClaims = forbiddenClaims;
|
|
30
|
+
const ORDER = ['structure', 'live', 'forecast', 'closed-loop'];
|
|
31
|
+
/**
|
|
32
|
+
* 증거로 단계를 판정한다.
|
|
33
|
+
*
|
|
34
|
+
* **단계는 앞선 단계를 전제한다**(구조 없이 라이브를 달성했다고 말할 수 없다). 앞이 막히면 뒤는
|
|
35
|
+
* 자기 조건을 따지지 않고 `prerequisite` 으로 막는다 — 뒤 단계의 조건을 나열하면 사람이 순서를
|
|
36
|
+
* 건너뛴다.
|
|
37
|
+
*/
|
|
38
|
+
function assessStagePath(e) {
|
|
39
|
+
const stages = [];
|
|
40
|
+
let prereqOk = true;
|
|
41
|
+
for (const id of ORDER) {
|
|
42
|
+
const blockedBy = [];
|
|
43
|
+
const unmeasured = [];
|
|
44
|
+
if (!prereqOk) {
|
|
45
|
+
blockedBy.push('prerequisite');
|
|
46
|
+
}
|
|
47
|
+
else if (id === 'structure') {
|
|
48
|
+
if (!(e.locationCount > 0))
|
|
49
|
+
blockedBy.push('no-locations');
|
|
50
|
+
if (!(e.itemCount > 0))
|
|
51
|
+
blockedBy.push('no-items');
|
|
52
|
+
if (e.masterWarningsApplicable !== false && e.masterWarnings === null)
|
|
53
|
+
unmeasured.push('master-warnings');
|
|
54
|
+
}
|
|
55
|
+
else if (id === 'live') {
|
|
56
|
+
if (e.subject === 'sim')
|
|
57
|
+
blockedBy.push('subject-is-simulation');
|
|
58
|
+
else {
|
|
59
|
+
if (!(e.liveIngestTotal > 0))
|
|
60
|
+
blockedBy.push('no-ingest');
|
|
61
|
+
if (e.removalsObserved === null)
|
|
62
|
+
unmeasured.push('removals');
|
|
63
|
+
/*
|
|
64
|
+
* 세었는데 없다. 이유가 둘이고 **처방이 정반대**다: 스냅샷을 되풀어 읽는 방식이면 더 기다려도
|
|
65
|
+
* 오지 않으므로 방식을 바꿔야 하고, 그 밖이면 아직 반출이 일어나지 않은 것일 수 있다.
|
|
66
|
+
*/
|
|
67
|
+
else if (e.removalsObserved === 0)
|
|
68
|
+
blockedBy.push(e.feed === 'snapshot-poll' ? 'snapshot-only-feed' : 'no-removal-observed');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else if (id === 'forecast') {
|
|
72
|
+
if (!e.forecastRan)
|
|
73
|
+
blockedBy.push('no-forecast-run');
|
|
74
|
+
if (e.specCoverage === null)
|
|
75
|
+
unmeasured.push('spec-coverage');
|
|
76
|
+
/* 선언이 0 이면 예측은 커널 기본값 위에 섰다 — 숫자는 나오지만 이 현장의 숫자가 아니다. */
|
|
77
|
+
else if (!(e.specCoverage.declared > 0))
|
|
78
|
+
blockedBy.push('declarations-empty');
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
if (!e.actuateCapability)
|
|
82
|
+
blockedBy.push('no-actuate-capability');
|
|
83
|
+
if (e.correlatedEffectObserved === null)
|
|
84
|
+
unmeasured.push('correlated-effect');
|
|
85
|
+
/* 접수만으로는 폐루프가 아니다 — 효과가 관측으로 돌아와야 닫힌다. */
|
|
86
|
+
else if (!(e.correlatedEffectObserved > 0))
|
|
87
|
+
blockedBy.push('effect-not-observed');
|
|
88
|
+
}
|
|
89
|
+
const achieved = blockedBy.length === 0 && unmeasured.length === 0;
|
|
90
|
+
stages.push({ id, achieved, blockedBy, unmeasured });
|
|
91
|
+
if (!achieved)
|
|
92
|
+
prereqOk = false;
|
|
93
|
+
}
|
|
94
|
+
const achievedIds = stages.filter(s => s.achieved).map(s => s.id);
|
|
95
|
+
const firstUnmet = stages.find(s => !s.achieved);
|
|
96
|
+
return {
|
|
97
|
+
stages,
|
|
98
|
+
reached: achievedIds.length ? achievedIds[achievedIds.length - 1] : null,
|
|
99
|
+
next: firstUnmet ? { stage: firstUnmet.id, blockedBy: firstUnmet.blockedBy, unmeasured: firstUnmet.unmeasured } : null
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* 이 단계에 이르지 못했다면 **쓰지 않기로 한 말**.
|
|
104
|
+
*
|
|
105
|
+
* 근거 수준 규율(`connector-developer-guide` §8)을 판정과 같은 자리에 둔다. 문장이 문서에만 있으면
|
|
106
|
+
* 제안서를 쓰는 사람이 그 문서를 읽지 않는다. 값이 되면 화면이 그 자리에서 말할 수 있다.
|
|
107
|
+
*/
|
|
108
|
+
exports.CLAIM_GATE = {
|
|
109
|
+
structure: 'claim.structure-synced',
|
|
110
|
+
live: 'claim.realtime',
|
|
111
|
+
forecast: 'claim.prediction',
|
|
112
|
+
'closed-loop': 'claim.automatic-control'
|
|
113
|
+
};
|
|
114
|
+
/** 아직 쓸 수 없는 주장들 — 도달하지 못한 단계의 게이트 전부. */
|
|
115
|
+
function forbiddenClaims(path) {
|
|
116
|
+
return path.stages.filter(s => !s.achieved).map(s => exports.CLAIM_GATE[s.id]);
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=stage-path.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stage-path.js","sourceRoot":"","sources":["../../server/engine/stage-path.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 단계 판정 — **가벼운 성공을 최종 성공으로 잇는 길을, 사람의 체크가 아니라 증거로 센다.**\n *\n * ── 왜 이 모듈이 있나 ───────────────────────────────────────────────────────\n * 트윈 도입은 「구조 → 라이브 → 예측 → 폐루프」로 간다. 그런데 이 진행을 **체크리스트**로 관리하면\n * 사람이 체크하는 순간 참이 된다. 실제로 가장 자주 건너뛰어지는 자리가 구조 다음이다 — 구조가 서면\n * 화면이 그럴듯해지므로 「실시간」이라고 말해 버리고, 되풀어 읽는 스냅샷이 그 말을 뒷받침하지 못한다.\n *\n * 그래서 단계를 **판정식**으로 둔다. 각 단계는 시스템이 이미 내놓는 사실로만 판정되고, 선언으로\n * 위조할 수 없다. 특히:\n *\n * · **반출 관측**이 라이브의 판별식이다. 스냅샷을 되풀어 읽는 방식은 **원리적으로** 반출을 낼 수 없다\n * (사라진 것은 그냥 없어질 뿐이라 `OBSERVE` 만 남는다). 그러므로 「반출을 한 번이라도 접었는가」는\n * 진짜 delta 의 증거이고, 폴링을 실시간이라 부르는 것을 막는다.\n * · **선언 적재량**이 예측의 판별식이다. 커널 기본값으로 돌아간 예측도 숫자를 내므로, 화면만 보면\n * 구별되지 않는다. 그 숫자가 무엇 위에 섰는지는 `specCoverage` 만 안다.\n *\n * ── 이 모듈의 정수: 「모른다」를 「아니다」로도 「그렇다」로도 바꾸지 않는다 ──────────────\n * 계측되지 않은 신호는 `null` 로 온다. `null` 은 0 이 아니다. 계측되지 않았으면 그 단계는 **달성이\n * 아니고**, 그 사실을 `unmeasured` 로 말한다 — 신호를 배선하지 않은 채 단계를 주장할 길을 남기지\n * 않는다. (0 으로 대체하면 「반출 0건」과 「반출을 세지 않았다」가 같아진다.)\n *\n * 순수 함수다(프레임워크·DB 무의존) — 증거 수집은 호출자 몫이다.\n */\n\nexport type StageId = 'structure' | 'live' | 'forecast' | 'closed-loop'\n\n/** 판정에 쓰는 사실. `null` = **계측되지 않았다**(0 이 아니다). */\nexport interface StageEvidence {\n /* ── S1 구조 ── */\n /** 이 트윈이 든 자리 수. */\n locationCount: number\n /** 자리에 놓인 물품 수(비직렬 수량 물품 포함 — 식품·화학은 개수가 아니라 총량으로 온다). */\n itemCount: number\n /**\n * 마스터를 옮기며 **못 옮긴 값의 수**. `null` = 세지 않았다.\n *\n * 0 이면 통과이고 n 이어도 통과다 — 이 판정이 요구하는 것은 「없다」가 아니라 **「말할 수 있다」**다.\n * 못 옮긴 값을 세지 않은 구조 인제스트는 도크를 랙으로 바꿔 놓고도 조용히 성공한다.\n */\n masterWarnings: number | null\n /**\n * 이 축이 이 트윈에 **해당하나**. 레퍼런스 없이 템플릿으로 세운 트윈에는 옮겨 온 마스터가 없으므로\n * 「못 옮긴 값」을 물을 대상이 아니다 — 그걸 미측정으로 접으면 영원히 배선을 기다린다.\n */\n masterWarningsApplicable?: boolean\n\n /* ── S2 라이브 ── */\n /** 인제스트 누적 건수. */\n liveIngestTotal: number\n /** **반출(remove)을 접은 누적 건수.** `null` = 계측되지 않음. 0 = 세었으나 없었다(→ 스냅샷 의심). */\n removalsObserved: number | null\n\n /* ── S3 예측 ── */\n /** 이 트윈에서 예측이 한 번이라도 돌았나. */\n forecastRan: boolean\n /** 선언 적재량 — `declared`/`total`. `null` = 알 수 없음. */\n specCoverage: { declared: number; total: number } | null\n\n /* ── S4 폐루프 ── */\n /**\n * 이 트윈이 **무엇인가** — 시뮬레이션은 원천에서 받는 것이 없다.\n *\n * 시뮬에 라이브를 요구하면 영원히 빨갛고, 그 빨강은 결함이 아니다. 도입 단계는 **연결된 트윈**의\n * 축이므로 시뮬은 라이브 이후를 「해당 없음」으로 말한다(달성으로도, 미측정으로도 접지 않는다).\n */\n subject?: 'live' | 'sim'\n /** 피드 성격 — `snapshot-poll` 이면 반출 0 은 「아직」이 아니라 **구조적으로 불가**다. */\n feed?: 'push' | 'delta-poll' | 'snapshot-poll' | 'none' | 'unknown'\n /** 원본이 **선언∩구현**으로 액추에이션 능력을 갖나(`capabilitiesOf`). */\n actuateCapability: boolean\n /** 내 명령의 효과가 인바운드로 **상관되어** 돌아온 누적 건수. `null` = 계측되지 않음. */\n correlatedEffectObserved: number | null\n}\n\nexport interface StageVerdict {\n id: StageId\n achieved: boolean\n /** 아직 아닌 이유 — 언어중립 코드(화면이 옮긴다). 달성이면 빈 배열. */\n blockedBy: string[]\n /** 판정할 수 없었던 신호 — **배선되지 않은 계측**. 있으면 달성이 아니다. */\n unmeasured: string[]\n}\n\nexport interface StagePath {\n stages: StageVerdict[]\n /** 지금 서 있는 칸 — 연속으로 달성된 마지막 단계. 하나도 없으면 `null`. */\n reached: StageId | null\n /**\n * 다음 한 걸음 — 사람이 오늘 할 수 있는 일 하나. 없으면(전부 달성) `null`.\n * 「무엇이 부족한지」의 목록이 아니라 **다음 하나**다. 목록은 사람을 멈추게 한다.\n */\n next: { stage: StageId; blockedBy: string[]; unmeasured: string[] } | null\n}\n\nconst ORDER: StageId[] = ['structure', 'live', 'forecast', 'closed-loop']\n\n/**\n * 증거로 단계를 판정한다.\n *\n * **단계는 앞선 단계를 전제한다**(구조 없이 라이브를 달성했다고 말할 수 없다). 앞이 막히면 뒤는\n * 자기 조건을 따지지 않고 `prerequisite` 으로 막는다 — 뒤 단계의 조건을 나열하면 사람이 순서를\n * 건너뛴다.\n */\nexport function assessStagePath(e: StageEvidence): StagePath {\n const stages: StageVerdict[] = []\n let prereqOk = true\n\n for (const id of ORDER) {\n const blockedBy: string[] = []\n const unmeasured: string[] = []\n\n if (!prereqOk) {\n blockedBy.push('prerequisite')\n } else if (id === 'structure') {\n if (!(e.locationCount > 0)) blockedBy.push('no-locations')\n if (!(e.itemCount > 0)) blockedBy.push('no-items')\n if (e.masterWarningsApplicable !== false && e.masterWarnings === null) unmeasured.push('master-warnings')\n } else if (id === 'live') {\n if (e.subject === 'sim') blockedBy.push('subject-is-simulation')\n else {\n if (!(e.liveIngestTotal > 0)) blockedBy.push('no-ingest')\n if (e.removalsObserved === null) unmeasured.push('removals')\n /*\n * 세었는데 없다. 이유가 둘이고 **처방이 정반대**다: 스냅샷을 되풀어 읽는 방식이면 더 기다려도\n * 오지 않으므로 방식을 바꿔야 하고, 그 밖이면 아직 반출이 일어나지 않은 것일 수 있다.\n */\n else if (e.removalsObserved === 0) blockedBy.push(e.feed === 'snapshot-poll' ? 'snapshot-only-feed' : 'no-removal-observed')\n }\n } else if (id === 'forecast') {\n if (!e.forecastRan) blockedBy.push('no-forecast-run')\n if (e.specCoverage === null) unmeasured.push('spec-coverage')\n /* 선언이 0 이면 예측은 커널 기본값 위에 섰다 — 숫자는 나오지만 이 현장의 숫자가 아니다. */\n else if (!(e.specCoverage.declared > 0)) blockedBy.push('declarations-empty')\n } else {\n if (!e.actuateCapability) blockedBy.push('no-actuate-capability')\n if (e.correlatedEffectObserved === null) unmeasured.push('correlated-effect')\n /* 접수만으로는 폐루프가 아니다 — 효과가 관측으로 돌아와야 닫힌다. */\n else if (!(e.correlatedEffectObserved > 0)) blockedBy.push('effect-not-observed')\n }\n\n const achieved = blockedBy.length === 0 && unmeasured.length === 0\n stages.push({ id, achieved, blockedBy, unmeasured })\n if (!achieved) prereqOk = false\n }\n\n const achievedIds = stages.filter(s => s.achieved).map(s => s.id)\n const firstUnmet = stages.find(s => !s.achieved)\n return {\n stages,\n reached: achievedIds.length ? achievedIds[achievedIds.length - 1] : null,\n next: firstUnmet ? { stage: firstUnmet.id, blockedBy: firstUnmet.blockedBy, unmeasured: firstUnmet.unmeasured } : null\n }\n}\n\n/**\n * 이 단계에 이르지 못했다면 **쓰지 않기로 한 말**.\n *\n * 근거 수준 규율(`connector-developer-guide` §8)을 판정과 같은 자리에 둔다. 문장이 문서에만 있으면\n * 제안서를 쓰는 사람이 그 문서를 읽지 않는다. 값이 되면 화면이 그 자리에서 말할 수 있다.\n */\nexport const CLAIM_GATE: Record<StageId, string> = {\n structure: 'claim.structure-synced',\n live: 'claim.realtime',\n forecast: 'claim.prediction',\n 'closed-loop': 'claim.automatic-control'\n}\n\n/** 아직 쓸 수 없는 주장들 — 도달하지 못한 단계의 게이트 전부. */\nexport function forbiddenClaims(path: StagePath): string[] {\n return path.stages.filter(s => !s.achieved).map(s => CLAIM_GATE[s.id])\n}\n"]}
|