@things-factory/headless-twin 10.0.12 → 10.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-server/engine/canonical-ingest.d.ts +57 -2
- package/dist-server/engine/canonical-ingest.js +43 -2
- package/dist-server/engine/canonical-ingest.js.map +1 -1
- package/dist-server/engine/command-routing.js +1 -1
- package/dist-server/engine/command-routing.js.map +1 -1
- package/dist-server/engine/index.d.ts +3 -0
- package/dist-server/engine/index.js +4 -0
- package/dist-server/engine/index.js.map +1 -1
- package/dist-server/engine/ingest-health.d.ts +335 -0
- package/dist-server/engine/ingest-health.js +434 -0
- package/dist-server/engine/ingest-health.js.map +1 -0
- package/dist-server/engine/integration-coverage.d.ts +76 -0
- package/dist-server/engine/integration-coverage.js +73 -0
- package/dist-server/engine/integration-coverage.js.map +1 -0
- package/dist-server/engine/integration-probes.d.ts +65 -0
- package/dist-server/engine/integration-probes.js +100 -0
- package/dist-server/engine/integration-probes.js.map +1 -0
- package/dist-server/engine/integration-runner.d.ts +45 -0
- package/dist-server/engine/integration-runner.js +59 -0
- package/dist-server/engine/integration-runner.js.map +1 -0
- package/dist-server/engine/integration-target-profile.d.ts +57 -0
- package/dist-server/engine/integration-target-profile.js +79 -0
- package/dist-server/engine/integration-target-profile.js.map +1 -0
- package/dist-server/engine/kpi-fold.d.ts +27 -0
- package/dist-server/engine/kpi-fold.js +47 -4
- package/dist-server/engine/kpi-fold.js.map +1 -1
- package/dist-server/engine/kpi-query.js +128 -14
- package/dist-server/engine/kpi-query.js.map +1 -1
- package/dist-server/engine/live-feed-registry.js +2 -16
- package/dist-server/engine/live-feed-registry.js.map +1 -1
- package/dist-server/engine/load-meter.d.ts +1 -1
- package/dist-server/engine/load-meter.js +12 -3
- package/dist-server/engine/load-meter.js.map +1 -1
- package/dist-server/engine/log.d.ts +18 -0
- package/dist-server/engine/log.js +80 -0
- package/dist-server/engine/log.js.map +1 -0
- package/dist-server/engine/loop-lag.d.ts +54 -0
- package/dist-server/engine/loop-lag.js +87 -0
- package/dist-server/engine/loop-lag.js.map +1 -0
- package/dist-server/engine/model-gap.d.ts +108 -0
- package/dist-server/engine/model-gap.js +95 -0
- package/dist-server/engine/model-gap.js.map +1 -0
- package/dist-server/engine/restart-policy.d.ts +3 -3
- package/dist-server/engine/restart-policy.js +4 -4
- package/dist-server/engine/restart-policy.js.map +1 -1
- package/dist-server/engine/runtime-key.js +1 -1
- package/dist-server/engine/runtime-key.js.map +1 -1
- package/dist-server/engine/stage-path.d.ts +83 -0
- package/dist-server/engine/stage-path.js +118 -0
- package/dist-server/engine/stage-path.js.map +1 -0
- package/dist-server/engine/twin-engine.d.ts +352 -16
- package/dist-server/engine/twin-engine.js +1037 -166
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/index.js +34 -2
- package/dist-server/index.js.map +1 -1
- package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.d.ts +5 -0
- package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js +58 -0
- package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js.map +1 -0
- package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.d.ts +5 -0
- package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js +55 -0
- package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js.map +1 -0
- package/dist-server/migrations/index.js +7 -1
- package/dist-server/migrations/index.js.map +1 -1
- package/dist-server/service/index.d.ts +5 -2
- package/dist-server/service/index.js +18 -7
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/reference/discovery-result.d.ts +1 -1
- package/dist-server/service/reference/discovery-result.js +1 -1
- package/dist-server/service/reference/discovery-result.js.map +1 -1
- package/dist-server/service/reference/index.d.ts +4 -1
- package/dist-server/service/reference/index.js +6 -1
- package/dist-server/service/reference/index.js.map +1 -1
- package/dist-server/service/reference/reference-adapter.d.ts +184 -3
- package/dist-server/service/reference/reference-adapter.js +28 -4
- package/dist-server/service/reference/reference-adapter.js.map +1 -1
- package/dist-server/service/reference/reference-assessment.d.ts +68 -0
- package/dist-server/service/reference/reference-assessment.js +136 -0
- package/dist-server/service/reference/reference-assessment.js.map +1 -0
- package/dist-server/service/reference/reference-live.d.ts +12 -1
- package/dist-server/service/reference/reference-live.js +116 -11
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-master.d.ts +47 -1
- package/dist-server/service/reference/reference-master.js +34 -4
- package/dist-server/service/reference/reference-master.js.map +1 -1
- package/dist-server/service/reference/reference-probe.d.ts +92 -0
- package/dist-server/service/reference/reference-probe.js +186 -0
- package/dist-server/service/reference/reference-probe.js.map +1 -0
- package/dist-server/service/reference/reference-progress-subscription.d.ts +17 -0
- package/dist-server/service/reference/reference-progress-subscription.js +94 -0
- package/dist-server/service/reference/reference-progress-subscription.js.map +1 -0
- package/dist-server/service/reference/reference-progress.d.ts +38 -0
- package/dist-server/service/reference/reference-progress.js +71 -0
- package/dist-server/service/reference/reference-progress.js.map +1 -0
- package/dist-server/service/reference/reference-resolver.js +210 -43
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/reference/twin-reference.d.ts +32 -0
- package/dist-server/service/reference/twin-reference.js +10 -0
- package/dist-server/service/reference/twin-reference.js.map +1 -1
- package/dist-server/service/twin-control/twin-control-mutation.js +4 -3
- package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
- package/dist-server/service/twin-event/journal-count.d.ts +21 -0
- package/dist-server/service/twin-event/journal-count.js +26 -0
- package/dist-server/service/twin-event/journal-count.js.map +1 -0
- package/dist-server/service/twin-event/twin-event-keys.d.ts +9 -0
- package/dist-server/service/twin-event/twin-event-keys.js +15 -18
- package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
- package/dist-server/service/twin-event/twin-event-type.d.ts +3 -1
- package/dist-server/service/twin-event/twin-event-type.js +18 -1
- package/dist-server/service/twin-event/twin-event-type.js.map +1 -1
- package/dist-server/service/twin-event/twin-event.d.ts +1 -0
- package/dist-server/service/twin-event/twin-event.js +9 -1
- package/dist-server/service/twin-event/twin-event.js.map +1 -1
- package/dist-server/service/twin-forecast/twin-forecast-query.js +2 -1
- package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
- package/dist-server/service/twin-ingest-window/index.d.ts +5 -0
- package/dist-server/service/twin-ingest-window/index.js +10 -0
- package/dist-server/service/twin-ingest-window/index.js.map +1 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-query.d.ts +12 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js +123 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js.map +1 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.d.ts +1 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js +51 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js.map +1 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window.d.ts +16 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window.js +108 -0
- package/dist-server/service/twin-ingest-window/twin-ingest-window.js.map +1 -0
- package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
- package/dist-server/service/twin-journal/twin-journal-query.js +58 -5
- package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +14 -0
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +72 -2
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
- package/dist-server/service/twin-metrics/twin-metrics-query.d.ts +1 -0
- package/dist-server/service/twin-metrics/twin-metrics-query.js +78 -3
- package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
- package/dist-server/service/twin-model/axis-journal-evidence.d.ts +2 -0
- package/dist-server/service/twin-model/axis-journal-evidence.js +89 -0
- package/dist-server/service/twin-model/axis-journal-evidence.js.map +1 -0
- package/dist-server/service/twin-model/epcis-coverage.d.ts +1 -1
- package/dist-server/service/twin-model/epcis-coverage.js +19 -7
- package/dist-server/service/twin-model/epcis-coverage.js.map +1 -1
- package/dist-server/service/twin-model/iec61850-coverage.d.ts +1 -1
- package/dist-server/service/twin-model/iec61850-coverage.js +18 -7
- package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -1
- package/dist-server/service/twin-model/isa95-coverage.d.ts +14 -0
- package/dist-server/service/twin-model/isa95-coverage.js +23 -9
- package/dist-server/service/twin-model/isa95-coverage.js.map +1 -1
- package/dist-server/service/twin-model/item-ref.d.ts +24 -0
- package/dist-server/service/twin-model/item-ref.js +88 -0
- package/dist-server/service/twin-model/item-ref.js.map +1 -0
- package/dist-server/service/twin-model/name-index.d.ts +36 -0
- package/dist-server/service/twin-model/name-index.js +116 -0
- package/dist-server/service/twin-model/name-index.js.map +1 -0
- package/dist-server/service/twin-model/project-structure.js +1 -1
- package/dist-server/service/twin-model/project-structure.js.map +1 -1
- package/dist-server/service/twin-model/status-tally.d.ts +9 -0
- package/dist-server/service/twin-model/status-tally.js +37 -0
- package/dist-server/service/twin-model/status-tally.js.map +1 -0
- package/dist-server/service/twin-model/twin-lineage-query.js +40 -12
- package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-item-query.js +38 -39
- package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-query.js +156 -4
- package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-tree-query.js +7 -0
- package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
- package/dist-server/service/twin-readiness/index.d.ts +2 -0
- package/dist-server/service/twin-readiness/index.js +6 -0
- package/dist-server/service/twin-readiness/index.js.map +1 -0
- package/dist-server/service/twin-readiness/twin-readiness-query.d.ts +3 -0
- package/dist-server/service/twin-readiness/twin-readiness-query.js +103 -0
- package/dist-server/service/twin-readiness/twin-readiness-query.js.map +1 -0
- package/dist-server/service/twin-space/twin-space-resolver.d.ts +2 -2
- package/dist-server/service/twin-space/twin-space-resolver.js +5 -4
- package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
- package/dist-shared/entity-delta.d.ts +23 -3
- package/dist-shared/entity-delta.js +12 -8
- package/dist-shared/entity-delta.js.map +1 -1
- package/dist-shared/kpi-broadcast.js +1 -1
- package/dist-shared/kpi-broadcast.js.map +1 -1
- package/dist-shared/touched-items.d.ts +7 -0
- package/dist-shared/touched-items.js +80 -0
- package/dist-shared/touched-items.js.map +1 -0
- package/package.json +7 -7
- package/server/engine/canonical-ingest.ts +92 -7
- package/server/engine/command-routing.ts +1 -1
- package/server/engine/index.ts +4 -0
- package/server/engine/ingest-health.ts +704 -0
- package/server/engine/integration-coverage.ts +147 -0
- package/server/engine/integration-probes.ts +144 -0
- package/server/engine/integration-runner.ts +95 -0
- package/server/engine/integration-target-profile.ts +103 -0
- package/server/engine/kpi-fold.ts +72 -4
- package/server/engine/kpi-query.ts +129 -15
- package/server/engine/live-feed-registry.ts +2 -1
- package/server/engine/load-meter.ts +12 -3
- package/server/engine/log.ts +72 -0
- package/server/engine/loop-lag.ts +120 -0
- package/server/engine/model-gap.ts +168 -0
- package/server/engine/restart-policy.ts +4 -4
- package/server/engine/runtime-key.ts +1 -1
- package/server/engine/stage-path.ts +172 -0
- package/server/engine/twin-engine.ts +1135 -162
- package/server/index.ts +35 -2
- package/server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.ts +54 -0
- package/server/migrations/1786200000000-CarryLiveFeedCursor.ts +53 -0
- package/server/migrations/index.ts +7 -1
- package/server/service/index.ts +11 -0
- package/server/service/reference/discovery-result.ts +1 -1
- package/server/service/reference/index.ts +6 -1
- package/server/service/reference/reference-adapter.ts +197 -5
- package/server/service/reference/reference-assessment.ts +215 -0
- package/server/service/reference/reference-live.ts +123 -11
- package/server/service/reference/reference-master.ts +64 -4
- package/server/service/reference/reference-probe.ts +264 -0
- package/server/service/reference/reference-progress-subscription.ts +73 -0
- package/server/service/reference/reference-progress.ts +95 -0
- package/server/service/reference/reference-resolver.ts +200 -12
- package/server/service/reference/twin-reference.ts +39 -1
- package/server/service/twin-control/twin-control-mutation.ts +4 -3
- package/server/service/twin-event/journal-count.ts +53 -0
- package/server/service/twin-event/twin-event-keys.ts +16 -1
- package/server/service/twin-event/twin-event-type.ts +26 -2
- package/server/service/twin-event/twin-event.ts +7 -0
- package/server/service/twin-forecast/twin-forecast-query.ts +2 -1
- package/server/service/twin-ingest-window/index.ts +7 -0
- package/server/service/twin-ingest-window/twin-ingest-window-query.ts +125 -0
- package/server/service/twin-ingest-window/twin-ingest-window-writer.ts +57 -0
- package/server/service/twin-ingest-window/twin-ingest-window.ts +113 -0
- package/server/service/twin-instance/twin-instance.ts +1 -1
- package/server/service/twin-journal/twin-journal-query.ts +59 -5
- package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +77 -3
- package/server/service/twin-metrics/twin-metrics-query.ts +81 -3
- package/server/service/twin-model/axis-journal-evidence.ts +57 -0
- package/server/service/twin-model/epcis-coverage.ts +2 -8
- package/server/service/twin-model/iec61850-coverage.ts +2 -8
- package/server/service/twin-model/isa95-coverage.ts +22 -9
- package/server/service/twin-model/item-ref.ts +80 -0
- package/server/service/twin-model/name-index.ts +94 -0
- package/server/service/twin-model/project-structure.ts +1 -1
- package/server/service/twin-model/status-tally.ts +37 -0
- package/server/service/twin-model/twin-lineage-query.ts +38 -9
- package/server/service/twin-model/twin-model-item-query.ts +26 -27
- package/server/service/twin-model/twin-model-query.ts +155 -4
- package/server/service/twin-model/twin-model-tree-query.ts +7 -0
- package/server/service/twin-readiness/index.ts +3 -0
- package/server/service/twin-readiness/twin-readiness-query.ts +96 -0
- package/server/service/twin-space/twin-space-resolver.ts +5 -4
- package/shared/entity-delta.ts +31 -7
- package/shared/kpi-broadcast.ts +1 -1
- package/shared/touched-items.ts +73 -0
- package/test/axis-journal-evidence.test.ts +71 -0
- package/test/axis-read.test.ts +101 -1
- package/test/boot-resume.test.ts +92 -26
- package/test/broadcast-cost-baseline.test.ts +200 -0
- package/test/broadcast-period.test.ts +58 -0
- package/test/canonical-ingest-vocabularies.test.ts +36 -1
- package/test/canonical-quantity-door.test.ts +61 -0
- package/test/command-routing.test.ts +1 -1
- package/test/declared-location-types.test.ts +89 -0
- package/test/discovery-result.test.ts +1 -1
- package/test/duration-estimators.test.ts +1 -1
- package/test/entity-delta.test.ts +2 -2
- package/test/ingest-bench.test.ts +3 -3
- package/test/ingest-health-engine.test.ts +248 -0
- package/test/ingest-health-wiring.test.ts +119 -0
- package/test/ingest-health.test.ts +306 -0
- package/test/ingest-history.test.ts +247 -0
- package/test/integration-probes.test.ts +103 -0
- package/test/integration-runner.test.ts +95 -0
- package/test/item-ref.test.ts +78 -0
- package/test/journal-read-discipline.test.ts +177 -0
- package/test/journal-retention.test.ts +133 -0
- package/test/journal-sort-axis.test.ts +142 -0
- package/test/journal-write-door.test.ts +110 -0
- package/test/journal-write-trend.test.ts +115 -0
- package/test/kernel-kind-guard.test.ts +4 -4
- package/test/kpi-fold.test.ts +90 -3
- package/test/kpi-query-bench.test.ts +2 -2
- package/test/lineage-survives-restart.test.ts +17 -1
- package/test/live-cursor-wiring.test.ts +87 -0
- package/test/live-feed-registry.test.ts +9 -1
- package/test/live-kernel-facts.test.ts +7 -7
- package/test/live-mirror-parity.test.ts +6 -0
- package/test/load-meter.test.ts +29 -15
- package/test/local-declarations.test.ts +1 -1
- package/test/log-stamp.test.ts +59 -0
- package/test/loop-lag.test.ts +82 -0
- package/test/model-gap.test.ts +153 -0
- package/test/oee-accumulator.test.ts +4 -0
- package/test/operational-vocabulary.test.ts +4 -4
- package/test/read-failure-visible.test.ts +145 -0
- package/test/reference-grounding.test.ts +70 -0
- package/test/resolve-ts-siblings.mjs +52 -0
- package/test/restart-policy.test.ts +7 -7
- package/test/revision-axis.test.ts +93 -0
- package/test/runtime-key.test.ts +2 -2
- package/test/scale-twin-bench.test.ts +2 -2
- package/test/spec-coverage.test.ts +1 -1
- package/test/stage-path.test.ts +95 -0
- package/test/standard-coverage.test.ts +15 -3
- package/test/status-tally.test.ts +55 -0
- package/test/structure-revision-db.test.ts +4 -0
- package/test/tenant-registry-db.test.ts +1 -1
- package/test/time-range.test.ts +152 -0
- package/test/touched-items.test.ts +61 -0
- package/test/twin-event-keys.test.ts +10 -2
- package/test/twin-model-item-db.test.ts +2 -0
- package/test/warm-start-seam.test.ts +4 -0
- package/test/yield-loop.test.ts +57 -2
- package/tsconfig.shared.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import type { IngestWarning, ReferenceMaster } from './reference-master.js'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* **원본에서 읽은 뒤의 총평** — 사람이 한 장으로 읽는다.
|
|
5
|
+
*
|
|
6
|
+
* ── 왜 필요한가 (2026-08-22) ────────────────────────────────────────────────
|
|
7
|
+
* 읽기가 끝나면 화면에 남는 것은 경고 열 줄이었고, 그 안에 이유가 문장으로 묻혀 있고 UUID 수백 개가
|
|
8
|
+
* 붙어 있었다. 그래서 사람이 「가져올 것은 다 들어왔나」·「무엇이 비었나」·「다음에 무엇을 하면
|
|
9
|
+
* 나아지나」를 스스로 세어야 했다 — 실제로 그 셋을 손으로 세는 대화가 하루를 채웠다.
|
|
10
|
+
*
|
|
11
|
+
* 그 셋은 이미 우리가 아는 사실이다. 세어 두었으니 **정리해서 내면 된다.**
|
|
12
|
+
*
|
|
13
|
+
* ── 두 층으로 나눈다 ────────────────────────────────────────────────────────
|
|
14
|
+
* 여기는 **사실 층**이다: 센 것과 그 사유 코드만 담고 문장을 만들지 않는다. 화면이 표로 그리고,
|
|
15
|
+
* 사람이 더 알고 싶으면 그때 AI 가 이 사실**만** 근거로 서술한다(말 층).
|
|
16
|
+
*
|
|
17
|
+
* **AI 는 로드베어링이 아니다.** 호출이 꺼져 있거나 실패해도 이 표가 그대로 화면에 남아야 한다 —
|
|
18
|
+
* 그래서 사실 층이 스스로 완결이다.
|
|
19
|
+
*
|
|
20
|
+
* ── 무엇을 지어내지 않는가 ──────────────────────────────────────────────────
|
|
21
|
+
* 「없음」의 뜻을 셋으로 갈라 둔다. 뭉치면 조치가 달라지는 것들이 한 칸에 섞인다.
|
|
22
|
+
* · `gaps` — 원본에 있는데 우리가 옮기지 못했다. 고칠 대상이 있다(원본의 빈 칸이거나 우리 매핑).
|
|
23
|
+
* · `absent` — 이 원본에 그 **개념이 없다.** 빠진 것이 아니므로 고칠 것이 없다.
|
|
24
|
+
* · `grounding` — 값이 들어왔지만 **근거가 저장된 값이 아니다**(원본의 계산 규칙에서 왔다).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** 센 것 — 화면이 그대로 표로 그린다. */
|
|
28
|
+
export interface AssessmentCounts {
|
|
29
|
+
locations: number
|
|
30
|
+
areas: number
|
|
31
|
+
equipment: number
|
|
32
|
+
materialDefinitions: number
|
|
33
|
+
operations: number
|
|
34
|
+
/** 생산 선언이 없으면 없다 — 0 과 구별한다(선언 자체가 없는 것과 비어 있는 것은 다르다). */
|
|
35
|
+
materials?: number
|
|
36
|
+
materialsWithHome?: number
|
|
37
|
+
routes?: number
|
|
38
|
+
recipes?: number
|
|
39
|
+
recipesWithRoute?: number
|
|
40
|
+
/** 공정을 말하는 투입 / 전체 투입 — 공정별 소요를 아는 정도. */
|
|
41
|
+
taggedInputs?: number
|
|
42
|
+
totalInputs?: number
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 옮기지 못한 것 한 갈래 — 건수 큰 것부터. */
|
|
46
|
+
export interface AssessmentGap {
|
|
47
|
+
/** 무엇을 옮기다 못 옮겼나 — 커넥터가 붙인 이름(`material home warehouse`). */
|
|
48
|
+
what: string
|
|
49
|
+
count: number
|
|
50
|
+
/** 못 옮긴 원본 값 몇 개 — 전부가 아니라 표본이다(수백 개가 붙던 자리다). */
|
|
51
|
+
sample?: string
|
|
52
|
+
/** 그 자리에 대신 들어간 값. */
|
|
53
|
+
fallback?: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ImportAssessment {
|
|
57
|
+
source: string
|
|
58
|
+
siteId: string
|
|
59
|
+
siteName?: string
|
|
60
|
+
system?: string
|
|
61
|
+
built: AssessmentCounts
|
|
62
|
+
/**
|
|
63
|
+
* **원본이 가지고 있던 수** — 커넥터가 말해 준 경우에만 있다.
|
|
64
|
+
*
|
|
65
|
+
* 이름을 `origin` 이라 하지 않는다 — 화면에서 그 낱말이 이미 **트윈의 출처**(`summary.origin`:
|
|
66
|
+
* 템플릿인가 원본인가)를 뜻한다. 같은 이름을 두 뜻으로 쓰면 읽는 사람이 헷갈린다.
|
|
67
|
+
*
|
|
68
|
+
* `built` 와 견주어 「전부 옮겼는가」를 사람이 직접 셀 수 있게 한다. 커넥터가 말하지 않으면 이 자리는
|
|
69
|
+
* 비어 있고, 화면은 분모 없이 옮긴 수만 보인다 — **분모를 지어내지 않는다.**
|
|
70
|
+
*/
|
|
71
|
+
originCounts?: Record<string, number>
|
|
72
|
+
gaps: AssessmentGap[]
|
|
73
|
+
/** 이 원본에 개념이 없는 자리 — 커널 낱말로 적는다(화면이 번역한다). */
|
|
74
|
+
absent: string[]
|
|
75
|
+
/**
|
|
76
|
+
* 값이 들어왔지만 **근거가 원본의 저장된 값이 아닌 것** — 파생·유추된 값이다.
|
|
77
|
+
*
|
|
78
|
+
* 이것을 `gaps` 에 섞으면 안 된다. 빈 자리가 아니라 **채워진 자리**이고, 사람이 물을 것은
|
|
79
|
+
* 「왜 비었나」가 아니라 「그 값을 믿어도 되나」다. 조치도 다르다 — 원본을 채우는 것이 아니라
|
|
80
|
+
* 그 규칙이 바뀌면 이 값도 바뀐다는 사실을 아는 것이다.
|
|
81
|
+
*/
|
|
82
|
+
derived: AssessmentGap[]
|
|
83
|
+
/** 되물은 횟수·잘린 목록 같은 운영 사실. */
|
|
84
|
+
notes: IngestWarning[]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 이 마스터가 개념을 선언했나 — 「없다」와 「비어 있다」를 가른다. */
|
|
88
|
+
const declared = (v: unknown): boolean => Array.isArray(v) ? v.length > 0 : v !== undefined && v !== null
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 마스터 한 벌 → 총평의 사실 층.
|
|
92
|
+
*
|
|
93
|
+
* 경고를 두 갈래로 나눈다: `connector-mapping-fallback` 은 **옮기지 못한 것**이므로 `gaps` 로,
|
|
94
|
+
* 나머지(파생된 값·되물은 횟수·잘린 목록)는 `notes` 로 그대로 나른다. 판단하지 않는다 — 무엇이
|
|
95
|
+
* 심각한지는 건수와 그 자리의 뜻이 말하고, 그 해석은 사람이나 말 층의 몫이다.
|
|
96
|
+
*/
|
|
97
|
+
export function assessMaster(master: ReferenceMaster, siteId: string): ImportAssessment {
|
|
98
|
+
const m: any = master
|
|
99
|
+
const d = m.productionSpec?.definition
|
|
100
|
+
const inputs = (d?.recipes ?? []).flatMap((r: any) => r.inputs ?? [])
|
|
101
|
+
|
|
102
|
+
const built: AssessmentCounts = {
|
|
103
|
+
locations: (m.locations ?? []).length,
|
|
104
|
+
areas: (m.locations ?? []).filter((l: any) => l.role === 'area' || l.role === 'both').length,
|
|
105
|
+
equipment: (m.equipment ?? []).length,
|
|
106
|
+
materialDefinitions: (m.materialDefinitions ?? []).length,
|
|
107
|
+
operations: (m.operations ?? []).length,
|
|
108
|
+
...(d
|
|
109
|
+
? {
|
|
110
|
+
materials: (d.materials ?? []).length,
|
|
111
|
+
materialsWithHome: (d.materials ?? []).filter((x: any) => x.locationType).length,
|
|
112
|
+
routes: (d.routes ?? []).length,
|
|
113
|
+
recipes: (d.recipes ?? []).length,
|
|
114
|
+
recipesWithRoute: (d.recipes ?? []).filter((x: any) => x.route).length,
|
|
115
|
+
totalInputs: inputs.length,
|
|
116
|
+
taggedInputs: inputs.filter((i: any) => i.operation).length
|
|
117
|
+
}
|
|
118
|
+
: {})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* 원본의 수는 커넥터가 말한 그대로 나른다 — 여기서 고치거나 채우지 않는다. */
|
|
122
|
+
const originCounts = m.originCounts && Object.keys(m.originCounts).length ? { ...m.originCounts } : undefined
|
|
123
|
+
|
|
124
|
+
const gaps: AssessmentGap[] = []
|
|
125
|
+
const derived: AssessmentGap[] = []
|
|
126
|
+
const notes: IngestWarning[] = []
|
|
127
|
+
/*
|
|
128
|
+
* 커넥터가 「대체」 수집기로 남기는 것 중 일부는 **빈 자리가 아니라 파생된 값**이다(산출량이 원본의
|
|
129
|
+
* 계산 규칙에서 온 경우처럼). 같은 통로로 오지만 뜻이 달라 갈라 담는다 — 이름에 그 사실이 적혀 있다.
|
|
130
|
+
*/
|
|
131
|
+
const isDerived = (field: string) => /derived|not a stored field/i.test(field)
|
|
132
|
+
for (const w of m.warnings ?? []) {
|
|
133
|
+
if (w?.code === 'connector-mapping-fallback' && w.params?.field) {
|
|
134
|
+
const values = String(w.params.values ?? '')
|
|
135
|
+
const bin = isDerived(String(w.params.field)) ? derived : gaps
|
|
136
|
+
bin.push({
|
|
137
|
+
what: String(w.params.field),
|
|
138
|
+
count: Number(w.params.count ?? 0),
|
|
139
|
+
/* 값은 표본만 — 이 자리에 UUID 수백 개가 붙어 화면을 밀어냈다. 전체는 경고 원문에 남는다. */
|
|
140
|
+
...(values ? { sample: values.length > 120 ? `${values.slice(0, 120)}…` : values } : {}),
|
|
141
|
+
...(w.params.fallback ? { fallback: String(w.params.fallback) } : {})
|
|
142
|
+
})
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
notes.push(w)
|
|
146
|
+
}
|
|
147
|
+
gaps.sort((a, b) => b.count - a.count)
|
|
148
|
+
derived.sort((a, b) => b.count - a.count)
|
|
149
|
+
|
|
150
|
+
/*
|
|
151
|
+
* 이 원본에 개념이 없는 자리. **커널 낱말로 적는다** — 화면이 그것으로 번역하고, 개념 지도가 이미
|
|
152
|
+
* 같은 낱말을 쓴다(두 화면이 다른 말을 하면 사람이 둘을 다른 사실로 읽는다).
|
|
153
|
+
*/
|
|
154
|
+
const absent: string[] = []
|
|
155
|
+
if (!declared(m.persons)) absent.push('persons')
|
|
156
|
+
if (!declared(m.assets)) absent.push('assets')
|
|
157
|
+
if (!declared(m.testSpecifications)) absent.push('testSpecifications')
|
|
158
|
+
if (!declared(m.personnelClasses)) absent.push('personnelClasses')
|
|
159
|
+
if (!declared(m.assetClasses)) absent.push('assetClasses')
|
|
160
|
+
if (!declared(m.materialClasses)) absent.push('materialClasses')
|
|
161
|
+
if (!d) absent.push('productionSpec')
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
source: m.source,
|
|
165
|
+
siteId,
|
|
166
|
+
siteName: m.siteName,
|
|
167
|
+
system: m.system,
|
|
168
|
+
built,
|
|
169
|
+
...(originCounts ? { originCounts } : {}),
|
|
170
|
+
gaps,
|
|
171
|
+
derived,
|
|
172
|
+
absent,
|
|
173
|
+
notes
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** 사람이 읽는 줄 — 터미널·로그가 읽는다(화면은 구조를 그대로 그린다). */
|
|
178
|
+
export function formatAssessment(a: ImportAssessment): string {
|
|
179
|
+
const c = a.built
|
|
180
|
+
const out: string[] = []
|
|
181
|
+
/**
|
|
182
|
+
* 분모가 있으면 함께 적는다 — 「1,261」과 「원본 1,408 중 1,261」은 다른 말이다.
|
|
183
|
+
* 커넥터가 그 수를 말하지 않았으면 옮긴 수만 적는다(지어내지 않는다).
|
|
184
|
+
*/
|
|
185
|
+
const n = (key: keyof AssessmentCounts, v?: number) => {
|
|
186
|
+
if (v === undefined) return '없음'
|
|
187
|
+
const o = a.originCounts?.[key as string]
|
|
188
|
+
return o === undefined ? String(v) : o === v ? `${v}(원본 전부)` : `${v} / 원본 ${o}`
|
|
189
|
+
}
|
|
190
|
+
out.push(`총평 ${a.siteName ?? a.siteId} (${a.system ?? '?'}) — 원본 ${a.source}`)
|
|
191
|
+
out.push(
|
|
192
|
+
` 선 것: 로케이션 ${n('locations', c.locations)}(존 ${c.areas}) · 설비 ${n('equipment', c.equipment)} · ` +
|
|
193
|
+
`품목 정의 ${n('materialDefinitions', c.materialDefinitions)} · 공정 ${n('operations', c.operations)}`
|
|
194
|
+
)
|
|
195
|
+
if (c.materials !== undefined) {
|
|
196
|
+
out.push(
|
|
197
|
+
` 자재 ${n('materials', c.materials)}(보관 로케이션 있는 것 ${c.materialsWithHome}) · ` +
|
|
198
|
+
`경로 ${n('routes', c.routes)} · 레시피 ${n('recipes', c.recipes)}(경로 붙은 것 ${c.recipesWithRoute}) · ` +
|
|
199
|
+
`투입 ${n('totalInputs', c.totalInputs)}(공정을 말하는 것 ${c.taggedInputs})`
|
|
200
|
+
)
|
|
201
|
+
} else {
|
|
202
|
+
out.push(' 생산 선언 없음 — 레시피가 하나도 성립하지 않았다')
|
|
203
|
+
}
|
|
204
|
+
if (a.gaps.length) {
|
|
205
|
+
out.push(' 옮기지 못한 것(건수 순):')
|
|
206
|
+
for (const g of a.gaps) out.push(` ${String(g.count).padStart(6)} ${g.what}${g.sample ? ` — ${g.sample}` : ''}`)
|
|
207
|
+
}
|
|
208
|
+
if (a.derived.length) {
|
|
209
|
+
out.push(' 파생된 값(저장된 값이 아니다):')
|
|
210
|
+
for (const g of a.derived) out.push(` ${g.what}${g.sample ? ` — ${g.sample}` : ''}`)
|
|
211
|
+
}
|
|
212
|
+
if (a.absent.length) out.push(` 이 원본에 개념이 없는 자리: ${a.absent.join(', ')}`)
|
|
213
|
+
for (const n of a.notes) out.push(` 참고 ${n.code}: ${n.message}`)
|
|
214
|
+
return out.join('\n')
|
|
215
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { twinLog, twinWarn, twinError } from '../../engine/log.js'
|
|
1
2
|
import { getRepository } from '@things-factory/shell'
|
|
2
3
|
|
|
3
|
-
import { getAdapter, type ConnectionConfig, type SiteDescriptor } from './reference-adapter.js'
|
|
4
|
+
import { getAdapter, type ConnectionConfig, type LiveFeedContinuity, type SiteDescriptor } from './reference-adapter.js'
|
|
4
5
|
import { TwinEngine, ingestCanonicalRecords, registerLiveFeedProbe, type CanonicalRecord } from '../../engine/index.js'
|
|
5
6
|
import { TwinInstance } from '../twin-instance/twin-instance.js'
|
|
6
7
|
import { TwinReference } from './twin-reference.js'
|
|
@@ -25,7 +26,18 @@ export async function startReferenceLiveFeed(
|
|
|
25
26
|
adapterType: string,
|
|
26
27
|
cfg: ConnectionConfig,
|
|
27
28
|
site: SiteDescriptor,
|
|
28
|
-
instanceId: string
|
|
29
|
+
instanceId: string,
|
|
30
|
+
/**
|
|
31
|
+
* **어느 레퍼런스에서 읽는 피드인가** — 읽기 커서를 저장할 자리를 가리킨다(§`loadLiveCursor`).
|
|
32
|
+
*
|
|
33
|
+
* 부르는 쪽이 `TwinReference` 를 이미 들고 있으므로 그 이름을 넘긴다. 처음에는 `cfg`·`site` 에서
|
|
34
|
+
* 찾으려 했는데 **둘 다 그 값을 가질 수 없다**: `cfg` 는 사람이 화면에서 입력한 접속 설정이고
|
|
35
|
+
* (`endpoint`·`token`…), `site` 는 커넥터가 원본에서 발견한 것이다 — **레퍼런스의 이름은 호스트가
|
|
36
|
+
* 붙이는 것이고 원본은 그것을 모른다.** 커넥터가 채울 수 없는 값을 커넥터에서 읽으려 한 것이 잘못이었다.
|
|
37
|
+
*
|
|
38
|
+
* 주지 않으면 커서를 잇지 않고 **그 사실을 말한다**(조용히 창이 미끄러지지 않게).
|
|
39
|
+
*/
|
|
40
|
+
source?: string
|
|
29
41
|
): Promise<boolean> {
|
|
30
42
|
const adapter = getAdapter(adapterType)
|
|
31
43
|
if (!adapter?.openLiveFeed) return false
|
|
@@ -38,29 +50,129 @@ export async function startReferenceLiveFeed(
|
|
|
38
50
|
TwinEngine.startLive(instanceId, domainId, det.kind, await TwinEngine.withSpaceTimeBase(det.model, domainId))
|
|
39
51
|
|
|
40
52
|
stopReferenceLiveFeed(instanceId) // 재기동 시 이전 피드 정리
|
|
53
|
+
/*
|
|
54
|
+
* **어디까지 읽었나를 이 층이 들고 건넨다**(§`LiveFeedContinuity`, 2026-08-23).
|
|
55
|
+
*
|
|
56
|
+
* 어댑터가 이 값을 자기 사물함에 두면 원본이 늘 때마다 저장 기제가 늘고, 그중 하나가 조용히 다르게
|
|
57
|
+
* 동작한다. 그리고 커널이 이미 갈라 두었다: 「원천이 애초에 다시 말해 주지 않는 축」은 재기동
|
|
58
|
+
* 연속성으로 이어받는다(§`hydrateContinuity`) — 읽기 커서가 정확히 그 성질이다.
|
|
59
|
+
*
|
|
60
|
+
* 없으면 `undefined` 를 건넨다 — 그것이 「첫 붙음」이라는 사실이고, 그때만 어댑터가 되돌아볼 날수로
|
|
61
|
+
* 첫 창을 만든다. 빈 객체로 메우지 않는다(그러면 첫 붙음과 이어 붙음이 같아진다).
|
|
62
|
+
*/
|
|
63
|
+
const continuity = await loadLiveCursor(domainId, instanceId, source)
|
|
41
64
|
const unsubscribe = adapter.openLiveFeed(cfg, site, (records: unknown[]) => {
|
|
42
65
|
if (!TwinEngine.owns(domainId, instanceId)) {
|
|
43
66
|
stopReferenceLiveFeed(instanceId) // 인스턴스 사라지면 자가 정지(안전망)
|
|
44
67
|
return
|
|
45
68
|
}
|
|
69
|
+
const offered = Array.isArray(records) ? records.length : records ? 1 : 0
|
|
46
70
|
const { accepted, rejected } = ingestCanonicalRecords(records as CanonicalRecord[], domainId, new Date().toISOString())
|
|
47
|
-
|
|
71
|
+
/*
|
|
72
|
+
* 커널이 **몇 건을 실제로 받았나**를 함께 본다. 트윈이 라이브로 돌지 않으면 `ingestLive` 가 0 을
|
|
73
|
+
* 돌려주고, 그 차이가 곧 「바닥에 버려진 수」다. 이것을 통과로 세면 멈춘 트윈이 100% 로 보인다.
|
|
74
|
+
*/
|
|
75
|
+
const applied = accepted.length ? TwinEngine.ingestLive(domainId, instanceId, accepted) : 0
|
|
76
|
+
const undelivered = accepted.length - applied
|
|
48
77
|
/*
|
|
49
78
|
* **거부한 것을 조용히 버리지 않는다.** 예전에는 `rejected` 를 받지도 않았다 — 커넥터의 매핑이
|
|
50
79
|
* 틀리면 트윈은 「아무 일도 일어나지 않는」 모습이 되고, 로그에도 아무 흔적이 없었다.
|
|
51
80
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
81
|
+
* 그다음 자리였던 「화면까지 나르기」를 이제 채운다. 건마다 브로드캐스팅하지 않고 **집계**한다 — 통과분까지
|
|
82
|
+
* 함께 세는 이유는, 거부만 세면 「유입 없음」과 「정상」이 둘 다 거부 0 으로 같아 보이기 때문이다.
|
|
83
|
+
* 조회는 `twinIngestHealth`.
|
|
84
|
+
*/
|
|
85
|
+
/*
|
|
86
|
+
* **읽었다는 것 자체가 사실이다** — 빈 읽기도 성공이므로 단절 기록을 지운다(§`onReadFailure`).
|
|
87
|
+
* 그 둘을 섞으면 조용한 원본이 끊긴 원본으로 보인다.
|
|
54
88
|
*/
|
|
89
|
+
TwinEngine.clearIngestReadFailure(domainId, instanceId)
|
|
90
|
+
TwinEngine.recordIngestResult(domainId, instanceId, offered, rejected, Date.now(), undelivered)
|
|
55
91
|
if (rejected.length) {
|
|
56
92
|
const why = rejected[0]?.errors?.join(' · ') ?? 'unknown'
|
|
57
|
-
|
|
93
|
+
twinWarn(`[twin-live] "${instanceId}": ${rejected.length}/${offered} record(s) rejected — e.g. ${why}`)
|
|
94
|
+
}
|
|
95
|
+
/* 버려진 것은 로그에도 남긴다 — 화면을 보지 않는 사람도 알아야 한다(사실이 사라지는 중이다). */
|
|
96
|
+
if (undelivered > 0) {
|
|
97
|
+
twinWarn(
|
|
98
|
+
`[twin-live] "${instanceId}": ${undelivered}/${offered} record(s) NOT delivered — the twin is not running live; ` +
|
|
99
|
+
`records are being discarded. Start the twin, or disconnect the feed.`
|
|
100
|
+
)
|
|
58
101
|
}
|
|
59
|
-
})
|
|
102
|
+
}, continuity)
|
|
60
103
|
feeds.set(instanceId, unsubscribe)
|
|
61
104
|
return true
|
|
62
105
|
}
|
|
63
106
|
|
|
107
|
+
/**
|
|
108
|
+
* 그 원본의 읽기 커서를 불러오고, 움직이면 저장하는 통로를 만든다.
|
|
109
|
+
*
|
|
110
|
+
* 커서는 **원본(`TwinReference`)의 것**이지 인스턴스의 것이 아니다 — 한 원본이 여러 인스턴스를 낳고
|
|
111
|
+
* (`scopeSpec.produced`) 읽는 것은 한 번이다. 인스턴스마다 커서를 두면 같은 표를 여러 번 읽는다.
|
|
112
|
+
*
|
|
113
|
+
* 저장 실패는 **삼키지 않고 말한다.** 조용히 실패하면 다음 재기동에서 창이 다시 미끄러지고, 그 손실은
|
|
114
|
+
* 오류 없이 사라진다 — 이 컬럼을 만든 이유가 바로 그것이다.
|
|
115
|
+
*/
|
|
116
|
+
async function loadLiveCursor(
|
|
117
|
+
domainId: string,
|
|
118
|
+
/** 읽기 실패를 적을 트윈 — 커서는 레퍼런스의 것이지만 **건강은 인스턴스의 것**이다(한 레퍼런스가 여럿을 낳는다). */
|
|
119
|
+
instanceId: string,
|
|
120
|
+
source?: string
|
|
121
|
+
): Promise<LiveFeedContinuity | undefined> {
|
|
122
|
+
if (!source) {
|
|
123
|
+
/* 원본을 가리킬 수 없으면 저장할 자리도 없다 — 그 사실을 말한다(조용히 커서 없이 돌지 않게). */
|
|
124
|
+
twinWarn('[twin-live] live cursor not carried — the caller did not say which reference this feed reads from')
|
|
125
|
+
return undefined
|
|
126
|
+
}
|
|
127
|
+
const repo = getRepository(TwinReference)
|
|
128
|
+
const row = await repo.findOne({ where: { domain: { id: domainId } as any, source } }).catch(() => null)
|
|
129
|
+
if (!row) {
|
|
130
|
+
twinWarn(`[twin-live] live cursor not carried — no reference row for source "${source}"`)
|
|
131
|
+
return undefined
|
|
132
|
+
}
|
|
133
|
+
const stored = row.liveCursor
|
|
134
|
+
if (stored?.firstAttachedAt) {
|
|
135
|
+
twinLog(`[twin-live] "${source}": resuming from stored cursor — knowledge starts ${stored.firstAttachedAt}`)
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
cursor: stored ?? undefined,
|
|
139
|
+
/*
|
|
140
|
+
* **원본에 닿지 못했다** — 어댑터가 알리면 상태에 적는다(§`onReadFailure`).
|
|
141
|
+
*
|
|
142
|
+
* 로그로는 어댑터가 이미 말하고 있었다(재시도 경고). 그러나 **로그는 사람이 볼 때만 값이 있다** —
|
|
143
|
+
* 화면이 말하려면 조회 가능한 상태에 있어야 한다.
|
|
144
|
+
*
|
|
145
|
+
* ── `status` 를 건드리지 않는다 (2026-08-23, 되돌린 자리) ────────────────────
|
|
146
|
+
* 처음에는 레퍼런스 행의 `status` 를 `'error'` 로도 적었다 — 「목록이 「연결됨」이라 말하면서 실제로는
|
|
147
|
+
* 못 읽는 상태를 없애려고」. **그것이 피드를 영구히 껐다.**
|
|
148
|
+
*
|
|
149
|
+
* 부팅 재부착은 `status: 'connected'` 인 레퍼런스만 되살린다(§`resumeReferenceLiveFeeds`). 그래서
|
|
150
|
+
* 원본이 **잠깐** 끊긴 것이 그 행을 `error` 로 만들고, 다음 재기동에서 그 피드는 아예 붙지 않았다 —
|
|
151
|
+
* 실측으로 그렇게 났다(09:25 판에 `[twin-live]` 줄이 한 줄도 없었다).
|
|
152
|
+
*
|
|
153
|
+
* 두 축을 한 칸에 섞은 것이 원인이다:
|
|
154
|
+
* `status` **연결 설정이 성립하나** — 접속 시험·마스터 동기가 정하는 축(재부착의 자격이다)
|
|
155
|
+
* 읽기 실패 **지금 닿나** — 오갈 수 있는 사실이고, 유입 장부가 그 축을 갖는다
|
|
156
|
+
*
|
|
157
|
+
* 그러니 여기서는 `lastError` 에만 적는다(사람이 목록에서 이유를 볼 수 있게). 「지금 닿나」는
|
|
158
|
+
* `ingestHealth().readFailure` 가 답하고, 성공한 주기가 그것을 지운다.
|
|
159
|
+
*/
|
|
160
|
+
onReadFailure: ({ reason, stream }) => {
|
|
161
|
+
TwinEngine.recordIngestReadFailure(domainId, instanceId, reason, Date.now(), stream)
|
|
162
|
+
twinWarn(`[twin-live] "${source}": cannot read from the source${stream ? ` (${stream})` : ''} — ${reason}`)
|
|
163
|
+
repo
|
|
164
|
+
.update({ id: row.id }, { lastError: `live: ${reason}` } as any)
|
|
165
|
+
.catch(err => twinWarn(`[twin-live] "${source}": read failure not recorded — ${err?.message ?? err}`))
|
|
166
|
+
},
|
|
167
|
+
onCursor: next => {
|
|
168
|
+
/* 어댑터가 준 모양을 그대로 적는다 — 이 층은 흐름의 뜻을 모른다(열쇠는 어댑터가 정한다). */
|
|
169
|
+
repo
|
|
170
|
+
.update({ id: row.id }, { liveCursor: next } as any)
|
|
171
|
+
.catch(err => twinWarn(`[twin-live] "${source}": live cursor not saved — ${err?.message ?? err}`))
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
64
176
|
/** 라이브 피드 중지(unsubscribe). */
|
|
65
177
|
export function stopReferenceLiveFeed(instanceId: string): boolean {
|
|
66
178
|
const unsub = feeds.get(instanceId)
|
|
@@ -151,7 +263,7 @@ export async function resumeReferenceLiveFeeds(): Promise<{ resumed: string[]; f
|
|
|
151
263
|
}
|
|
152
264
|
attached.set(p.instanceId, ref.source)
|
|
153
265
|
try {
|
|
154
|
-
const ok = await startReferenceLiveFeed(domainId, ref.adapterType, ref.connectionConfig ?? {}, site, p.instanceId)
|
|
266
|
+
const ok = await startReferenceLiveFeed(domainId, ref.adapterType, ref.connectionConfig ?? {}, site, p.instanceId, ref.source)
|
|
155
267
|
if (ok) resumed.push(p.instanceId)
|
|
156
268
|
else failed.push({ instanceId: p.instanceId, error: 'adapter has no openLiveFeed' })
|
|
157
269
|
} catch (e: any) {
|
|
@@ -160,12 +272,12 @@ export async function resumeReferenceLiveFeeds(): Promise<{ resumed: string[]; f
|
|
|
160
272
|
}
|
|
161
273
|
}
|
|
162
274
|
} catch (e: any) {
|
|
163
|
-
|
|
275
|
+
twinError('[twin-live] feed resume scan failed', e?.message)
|
|
164
276
|
}
|
|
165
277
|
|
|
166
|
-
if (resumed.length)
|
|
278
|
+
if (resumed.length) twinLog(`[twin-live] reattached ${resumed.length} feed(s): ${resumed.join(', ')}`)
|
|
167
279
|
/* **조용히 넘기지 않는다** — 붙지 않은 미러는 「도는데 아무것도 오지 않는」 트윈이다. */
|
|
168
|
-
for (const f of failed)
|
|
280
|
+
for (const f of failed) twinWarn(`[twin-live] "${f.instanceId}" feed NOT reattached — ${f.error}`)
|
|
169
281
|
return { resumed, failed }
|
|
170
282
|
}
|
|
171
283
|
|
|
@@ -407,6 +407,21 @@ export interface ReferenceMaster {
|
|
|
407
407
|
* BOM 을 설명 문서에만 적어 둔 채 키트를 못 만드는 트윈을 만든 원인). 근거는 커널 계약 주석에.
|
|
408
408
|
*/
|
|
409
409
|
productionSpec?: unknown
|
|
410
|
+
/**
|
|
411
|
+
* **이 원본이 식별자를 만드는 이름공간** — `TwinModelDef.identity` 로 통과한다.
|
|
412
|
+
*
|
|
413
|
+
* **선언이 없으면 커널이 거절한다** (0.7.45~). 예전에는 상수 프리픽스로 팔레트·트레일러·문서를
|
|
414
|
+
* 만들었는데(`0614141` — GS1 이 규격 예제로 쓰다 버린 값이고 `06…` 은 실 배정 대역이다), 그 값은
|
|
415
|
+
* 저널에 영원히 남으면서 지어낸 것이라고 말해 주는 데가 없었다. 그래서 커널이 만들지 않는다.
|
|
416
|
+
*
|
|
417
|
+
* 원본이 자기 도메인을 말하면 그 아래에서 만든다 — 그것이 표준 정합이다(CBV 2.0 §8.2.4·§8.5.5,
|
|
418
|
+
* 「그 인터넷 도메인 소유자가 배정한다」). GS1 키를 쓰려면 `documentTypes` + `companyPrefix` 이고,
|
|
419
|
+
* 그때 **자리 수가 맞아야 한다**(SGTIN 은 프리픽스 + 품목참조 = 13자리, GS1 TDS).
|
|
420
|
+
*
|
|
421
|
+
* 정체성은 생산과 무관하므로 `productionSpec` 이 아니라 **모델**이 갖는다 — 창고·야드는 아무것도
|
|
422
|
+
* 생산하지 않으면서 팔레트·트레일러를 식별한다.
|
|
423
|
+
*/
|
|
424
|
+
identity?: unknown
|
|
410
425
|
/**
|
|
411
426
|
* 사람 — 선언하지 않으면 **인원 제약이 없는 트윈**이 된다(설비만 있으면 언제나 가동되는 공장).
|
|
412
427
|
* 공정이 인원을 요구하는데 사람이 없으면 그 작업은 시작되지 않는다 — 그것이 사실이므로 그대로 둔다.
|
|
@@ -435,6 +450,21 @@ export interface ReferenceMaster {
|
|
|
435
450
|
* 놓고도 조용히 성공했다. 옮기는 사람만 아는 사실은 옮기는 자리에서 실어야 한다.
|
|
436
451
|
*/
|
|
437
452
|
warnings?: IngestWarning[]
|
|
453
|
+
/**
|
|
454
|
+
* **원본이 가진 수** — 옮긴 수의 분모다.
|
|
455
|
+
*
|
|
456
|
+
* 총평은 도착한 모델만 볼 수 있어서 「레시피 1,261」까지만 말할 수 있었다. 그런데 사람이 묻는 것은
|
|
457
|
+
* 「그게 전부인가」다 — 원본에 1,408개가 있었고 147개를 옮기지 못했다면 그 사실이 총평의 첫 줄에
|
|
458
|
+
* 있어야 한다. 실제로 사용자가 품목 2,789 와 자재 1,789 의 차이를 화면에서 읽어 내지 못해 직접
|
|
459
|
+
* 물었다(2026-08-22).
|
|
460
|
+
*
|
|
461
|
+
* 분모를 아는 것은 **커넥터뿐이다.** 그래서 커넥터가 실어 보낸다. 보내지 않으면 총평은 분모 없이
|
|
462
|
+
* 옮긴 수만 말한다 — **지어내지 않는다.**
|
|
463
|
+
*
|
|
464
|
+
* 열쇠는 총평이 세는 이름과 같다(`recipes`·`materials`·`totalInputs`·…). 값은 **원본을 끝까지 읽어서
|
|
465
|
+
* 센 것**이어야 한다. 잘린 읽기의 수를 여기 담으면 「전부 옮겼다」는 거짓이 된다.
|
|
466
|
+
*/
|
|
467
|
+
originCounts?: Record<string, number>
|
|
438
468
|
}
|
|
439
469
|
|
|
440
470
|
export interface IngestedTwin {
|
|
@@ -471,7 +501,7 @@ export function utcOffsetOf(timezone?: string, at: Date = new Date()): number |
|
|
|
471
501
|
const sign = m[1] === '-' ? -1 : 1
|
|
472
502
|
return sign * (Number(m[2]) * 60 + Number(m[3] ?? 0))
|
|
473
503
|
} catch {
|
|
474
|
-
return undefined // 알 수 없는 이름 — Intl 이
|
|
504
|
+
return undefined // 알 수 없는 이름 — Intl 이 오류를 낸다
|
|
475
505
|
}
|
|
476
506
|
}
|
|
477
507
|
|
|
@@ -524,13 +554,37 @@ export const generationWithoutTimeZone = (spaceId: string, equipmentId: string):
|
|
|
524
554
|
'so the generation peak will not land at local noon. Declare the space time zone (we do not guess one).'
|
|
525
555
|
})
|
|
526
556
|
|
|
527
|
-
/**
|
|
557
|
+
/**
|
|
558
|
+
* 아무도 선언하지 않은 자리 타입 — 그 자리는 흐름에 참여하지 못할 수 있다.
|
|
559
|
+
*
|
|
560
|
+
* **묻는 상대를 고쳤다 (2026-08-20).** 예전에는 내장 카탈로그에게만 물었다. 그런데 원본이 자기
|
|
561
|
+
* `DomainDefinition.locationTypes` 를 함께 보내면 **그 선언이 정본**이다 — 카탈로그는 우리가 아는
|
|
562
|
+
* 도메인들의 기본 어휘일 뿐이다. 물을 상대가 틀렸으니, 자기 어휘를 선언한 원본은 정상적인 타입마다
|
|
563
|
+
* 경고를 받았다(F&B 라인을 붙이면 자리마다 하나씩). 소음이 되면 사람은 경고를 읽지 않게 되고, 그때
|
|
564
|
+
* 진짜 경고도 함께 죽는다.
|
|
565
|
+
*/
|
|
528
566
|
export const unknownLocationType = (system: string, type: string, id: string): IngestWarning => ({
|
|
529
567
|
code: 'unknown-location-type',
|
|
530
568
|
params: { system, type, id },
|
|
531
|
-
message:
|
|
569
|
+
message:
|
|
570
|
+
`"${type}" is declared nowhere for ${system} (${id}) — neither in the master's own locationTypes nor in the built-in ` +
|
|
571
|
+
`catalog, so the kernel may not include this location in the flow`
|
|
532
572
|
})
|
|
533
573
|
|
|
574
|
+
/**
|
|
575
|
+
* 원본이 함께 보낸 자리 타입 선언 — 있으면 **이것이 정본**이다.
|
|
576
|
+
*
|
|
577
|
+
* 커널 계약의 `DomainDefinition.locationTypes` 는 `{ key, label, … }` 목록이다. 읽을 수 없는 모양이면
|
|
578
|
+
* `undefined` 를 돌려 **카탈로그로 떨어진다** — 빈 배열을 돌려주면 「아무 타입도 선언되지 않았다」가
|
|
579
|
+
* 되어 모든 자리가 경고를 받는다(없는 것과 못 읽은 것은 다르다).
|
|
580
|
+
*/
|
|
581
|
+
export function declaredLocationTypes(productionSpec: unknown): string[] | undefined {
|
|
582
|
+
const list = (productionSpec as any)?.definition?.locationTypes
|
|
583
|
+
if (!Array.isArray(list) || !list.length) return undefined
|
|
584
|
+
const keys = list.map((t: any) => (typeof t === 'string' ? t : t?.key)).filter((k: any) => typeof k === 'string' && k)
|
|
585
|
+
return keys.length ? keys : undefined
|
|
586
|
+
}
|
|
587
|
+
|
|
534
588
|
/** 시간대 문자열을 못 읽었다 — 시각 해석이 UTC 로 떨어진다(조용히 현지 시각으로 가정하지 않는다). */
|
|
535
589
|
export const unknownTimezone = (timezone: string): IngestWarning => ({
|
|
536
590
|
code: 'unknown-timezone',
|
|
@@ -638,7 +692,11 @@ export const siteEquipmentAdopted = (key: string, count: number): IngestWarning
|
|
|
638
692
|
export function masterToTwin(master: ReferenceMaster, catalog?: any): IngestedTwin {
|
|
639
693
|
/* 커넥터가 옮기며 알게 된 것을 먼저 싣는다 — 원인이 앞에 오는 편이 읽기 쉽다(대체가 먼저, 그 결과가 뒤). */
|
|
640
694
|
const warnings: IngestWarning[] = [...(master.warnings ?? [])]
|
|
641
|
-
|
|
695
|
+
/*
|
|
696
|
+
* 원본이 자기 어휘를 선언했으면 **그것에 대조한다.** 내장 카탈로그는 그 선언이 없을 때의 기본이다 —
|
|
697
|
+
* 상대를 바꾸지 않으면, 자기 어휘를 선언한 원본이 정상적인 자리마다 경고를 받는다.
|
|
698
|
+
*/
|
|
699
|
+
const known: string[] = declaredLocationTypes(master.productionSpec) ?? catalog?.[master.system]?.locationTypes ?? []
|
|
642
700
|
|
|
643
701
|
const locationEntries = master.locations.filter(l => l.role === 'location' || l.role === 'both')
|
|
644
702
|
const areaLocs = master.locations.filter(l => l.role === 'area' || l.role === 'both')
|
|
@@ -740,6 +798,8 @@ export function masterToTwin(master: ReferenceMaster, catalog?: any): IngestedTw
|
|
|
740
798
|
locations, equipment, layout, spaceId,
|
|
741
799
|
/* 생산 정의도 손대지 않고 통과 — 경계에서 어휘를 갈지 않는다(커널이 그대로 소비한다). */
|
|
742
800
|
...(master.productionSpec ? { productionSpec: master.productionSpec } : {}),
|
|
801
|
+
/* 정체성 선언도 그대로 통과한다 — 없으면 만들지 않는다(커널이 레거시 상수로 떨어진다). */
|
|
802
|
+
...(master.identity ? { identity: master.identity } : {}),
|
|
743
803
|
...(materialDefinitions?.length ? { materialDefinitions } : {}),
|
|
744
804
|
...(materialClasses?.length ? { materialClasses } : {}),
|
|
745
805
|
...(utcOffsetMinutes !== undefined ? { utcOffsetMinutes } : {}),
|