@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.
Files changed (315) hide show
  1. package/dist-server/engine/canonical-ingest.d.ts +57 -2
  2. package/dist-server/engine/canonical-ingest.js +43 -2
  3. package/dist-server/engine/canonical-ingest.js.map +1 -1
  4. package/dist-server/engine/command-routing.js +1 -1
  5. package/dist-server/engine/command-routing.js.map +1 -1
  6. package/dist-server/engine/index.d.ts +3 -0
  7. package/dist-server/engine/index.js +4 -0
  8. package/dist-server/engine/index.js.map +1 -1
  9. package/dist-server/engine/ingest-health.d.ts +376 -0
  10. package/dist-server/engine/ingest-health.js +482 -0
  11. package/dist-server/engine/ingest-health.js.map +1 -0
  12. package/dist-server/engine/integration-coverage.d.ts +76 -0
  13. package/dist-server/engine/integration-coverage.js +73 -0
  14. package/dist-server/engine/integration-coverage.js.map +1 -0
  15. package/dist-server/engine/integration-probes.d.ts +65 -0
  16. package/dist-server/engine/integration-probes.js +100 -0
  17. package/dist-server/engine/integration-probes.js.map +1 -0
  18. package/dist-server/engine/integration-runner.d.ts +45 -0
  19. package/dist-server/engine/integration-runner.js +59 -0
  20. package/dist-server/engine/integration-runner.js.map +1 -0
  21. package/dist-server/engine/integration-target-profile.d.ts +57 -0
  22. package/dist-server/engine/integration-target-profile.js +79 -0
  23. package/dist-server/engine/integration-target-profile.js.map +1 -0
  24. package/dist-server/engine/kpi-fold.d.ts +27 -0
  25. package/dist-server/engine/kpi-fold.js +47 -4
  26. package/dist-server/engine/kpi-fold.js.map +1 -1
  27. package/dist-server/engine/kpi-query.js +128 -14
  28. package/dist-server/engine/kpi-query.js.map +1 -1
  29. package/dist-server/engine/live-feed-registry.js +2 -16
  30. package/dist-server/engine/live-feed-registry.js.map +1 -1
  31. package/dist-server/engine/load-meter.d.ts +1 -1
  32. package/dist-server/engine/load-meter.js +12 -3
  33. package/dist-server/engine/load-meter.js.map +1 -1
  34. package/dist-server/engine/log.d.ts +18 -0
  35. package/dist-server/engine/log.js +80 -0
  36. package/dist-server/engine/log.js.map +1 -0
  37. package/dist-server/engine/loop-lag.d.ts +54 -0
  38. package/dist-server/engine/loop-lag.js +87 -0
  39. package/dist-server/engine/loop-lag.js.map +1 -0
  40. package/dist-server/engine/model-gap.d.ts +108 -0
  41. package/dist-server/engine/model-gap.js +95 -0
  42. package/dist-server/engine/model-gap.js.map +1 -0
  43. package/dist-server/engine/restart-policy.d.ts +3 -3
  44. package/dist-server/engine/restart-policy.js +4 -4
  45. package/dist-server/engine/restart-policy.js.map +1 -1
  46. package/dist-server/engine/runtime-key.js +1 -1
  47. package/dist-server/engine/runtime-key.js.map +1 -1
  48. package/dist-server/engine/stage-path.d.ts +83 -0
  49. package/dist-server/engine/stage-path.js +118 -0
  50. package/dist-server/engine/stage-path.js.map +1 -0
  51. package/dist-server/engine/twin-engine.d.ts +386 -16
  52. package/dist-server/engine/twin-engine.js +1217 -172
  53. package/dist-server/engine/twin-engine.js.map +1 -1
  54. package/dist-server/index.js +34 -2
  55. package/dist-server/index.js.map +1 -1
  56. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.d.ts +5 -0
  57. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js +58 -0
  58. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js.map +1 -0
  59. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.d.ts +5 -0
  60. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js +55 -0
  61. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js.map +1 -0
  62. package/dist-server/migrations/index.js +7 -1
  63. package/dist-server/migrations/index.js.map +1 -1
  64. package/dist-server/service/index.d.ts +5 -2
  65. package/dist-server/service/index.js +18 -7
  66. package/dist-server/service/index.js.map +1 -1
  67. package/dist-server/service/reference/discovery-result.d.ts +1 -1
  68. package/dist-server/service/reference/discovery-result.js +1 -1
  69. package/dist-server/service/reference/discovery-result.js.map +1 -1
  70. package/dist-server/service/reference/index.d.ts +4 -1
  71. package/dist-server/service/reference/index.js +6 -1
  72. package/dist-server/service/reference/index.js.map +1 -1
  73. package/dist-server/service/reference/reference-adapter.d.ts +210 -3
  74. package/dist-server/service/reference/reference-adapter.js +28 -4
  75. package/dist-server/service/reference/reference-adapter.js.map +1 -1
  76. package/dist-server/service/reference/reference-assessment.d.ts +78 -0
  77. package/dist-server/service/reference/reference-assessment.js +156 -0
  78. package/dist-server/service/reference/reference-assessment.js.map +1 -0
  79. package/dist-server/service/reference/reference-live.d.ts +12 -1
  80. package/dist-server/service/reference/reference-live.js +134 -11
  81. package/dist-server/service/reference/reference-live.js.map +1 -1
  82. package/dist-server/service/reference/reference-master.d.ts +59 -3
  83. package/dist-server/service/reference/reference-master.js +34 -4
  84. package/dist-server/service/reference/reference-master.js.map +1 -1
  85. package/dist-server/service/reference/reference-probe.d.ts +92 -0
  86. package/dist-server/service/reference/reference-probe.js +186 -0
  87. package/dist-server/service/reference/reference-probe.js.map +1 -0
  88. package/dist-server/service/reference/reference-progress-subscription.d.ts +17 -0
  89. package/dist-server/service/reference/reference-progress-subscription.js +94 -0
  90. package/dist-server/service/reference/reference-progress-subscription.js.map +1 -0
  91. package/dist-server/service/reference/reference-progress.d.ts +38 -0
  92. package/dist-server/service/reference/reference-progress.js +71 -0
  93. package/dist-server/service/reference/reference-progress.js.map +1 -0
  94. package/dist-server/service/reference/reference-resolver.js +210 -43
  95. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  96. package/dist-server/service/reference/twin-reference.d.ts +32 -0
  97. package/dist-server/service/reference/twin-reference.js +10 -0
  98. package/dist-server/service/reference/twin-reference.js.map +1 -1
  99. package/dist-server/service/twin-control/twin-control-mutation.js +4 -3
  100. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  101. package/dist-server/service/twin-event/journal-count.d.ts +21 -0
  102. package/dist-server/service/twin-event/journal-count.js +26 -0
  103. package/dist-server/service/twin-event/journal-count.js.map +1 -0
  104. package/dist-server/service/twin-event/twin-event-keys.d.ts +9 -0
  105. package/dist-server/service/twin-event/twin-event-keys.js +15 -18
  106. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  107. package/dist-server/service/twin-event/twin-event-type.d.ts +3 -1
  108. package/dist-server/service/twin-event/twin-event-type.js +18 -1
  109. package/dist-server/service/twin-event/twin-event-type.js.map +1 -1
  110. package/dist-server/service/twin-event/twin-event.d.ts +1 -0
  111. package/dist-server/service/twin-event/twin-event.js +9 -1
  112. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  113. package/dist-server/service/twin-forecast/twin-forecast-query.js +26 -2
  114. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  115. package/dist-server/service/twin-ingest-window/index.d.ts +5 -0
  116. package/dist-server/service/twin-ingest-window/index.js +10 -0
  117. package/dist-server/service/twin-ingest-window/index.js.map +1 -0
  118. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.d.ts +12 -0
  119. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js +123 -0
  120. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js.map +1 -0
  121. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.d.ts +1 -0
  122. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js +51 -0
  123. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js.map +1 -0
  124. package/dist-server/service/twin-ingest-window/twin-ingest-window.d.ts +16 -0
  125. package/dist-server/service/twin-ingest-window/twin-ingest-window.js +108 -0
  126. package/dist-server/service/twin-ingest-window/twin-ingest-window.js.map +1 -0
  127. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  128. package/dist-server/service/twin-journal/twin-journal-query.js +58 -5
  129. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  130. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +14 -0
  131. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +72 -2
  132. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  133. package/dist-server/service/twin-metrics/twin-metrics-query.d.ts +1 -0
  134. package/dist-server/service/twin-metrics/twin-metrics-query.js +78 -3
  135. package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
  136. package/dist-server/service/twin-model/axis-journal-evidence.d.ts +2 -0
  137. package/dist-server/service/twin-model/axis-journal-evidence.js +89 -0
  138. package/dist-server/service/twin-model/axis-journal-evidence.js.map +1 -0
  139. package/dist-server/service/twin-model/epcis-coverage.d.ts +1 -1
  140. package/dist-server/service/twin-model/epcis-coverage.js +46 -7
  141. package/dist-server/service/twin-model/epcis-coverage.js.map +1 -1
  142. package/dist-server/service/twin-model/iec61850-coverage.d.ts +1 -1
  143. package/dist-server/service/twin-model/iec61850-coverage.js +18 -7
  144. package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -1
  145. package/dist-server/service/twin-model/isa95-coverage.d.ts +14 -0
  146. package/dist-server/service/twin-model/isa95-coverage.js +108 -14
  147. package/dist-server/service/twin-model/isa95-coverage.js.map +1 -1
  148. package/dist-server/service/twin-model/item-ref.d.ts +24 -0
  149. package/dist-server/service/twin-model/item-ref.js +88 -0
  150. package/dist-server/service/twin-model/item-ref.js.map +1 -0
  151. package/dist-server/service/twin-model/name-index.d.ts +36 -0
  152. package/dist-server/service/twin-model/name-index.js +116 -0
  153. package/dist-server/service/twin-model/name-index.js.map +1 -0
  154. package/dist-server/service/twin-model/project-structure.js +1 -1
  155. package/dist-server/service/twin-model/project-structure.js.map +1 -1
  156. package/dist-server/service/twin-model/status-tally.d.ts +9 -0
  157. package/dist-server/service/twin-model/status-tally.js +37 -0
  158. package/dist-server/service/twin-model/status-tally.js.map +1 -0
  159. package/dist-server/service/twin-model/twin-lineage-query.js +40 -12
  160. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
  161. package/dist-server/service/twin-model/twin-model-item-query.js +38 -39
  162. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
  163. package/dist-server/service/twin-model/twin-model-query.js +156 -4
  164. package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
  165. package/dist-server/service/twin-model/twin-model-tree-query.js +7 -0
  166. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
  167. package/dist-server/service/twin-readiness/index.d.ts +2 -0
  168. package/dist-server/service/twin-readiness/index.js +6 -0
  169. package/dist-server/service/twin-readiness/index.js.map +1 -0
  170. package/dist-server/service/twin-readiness/twin-readiness-query.d.ts +3 -0
  171. package/dist-server/service/twin-readiness/twin-readiness-query.js +103 -0
  172. package/dist-server/service/twin-readiness/twin-readiness-query.js.map +1 -0
  173. package/dist-server/service/twin-space/twin-space-resolver.d.ts +2 -2
  174. package/dist-server/service/twin-space/twin-space-resolver.js +5 -4
  175. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  176. package/dist-shared/entity-delta.d.ts +23 -3
  177. package/dist-shared/entity-delta.js +12 -8
  178. package/dist-shared/entity-delta.js.map +1 -1
  179. package/dist-shared/kpi-broadcast.js +1 -1
  180. package/dist-shared/kpi-broadcast.js.map +1 -1
  181. package/dist-shared/touched-items.d.ts +7 -0
  182. package/dist-shared/touched-items.js +80 -0
  183. package/dist-shared/touched-items.js.map +1 -0
  184. package/package.json +7 -7
  185. package/server/engine/canonical-ingest.ts +92 -7
  186. package/server/engine/command-routing.ts +1 -1
  187. package/server/engine/index.ts +4 -0
  188. package/server/engine/ingest-health.ts +782 -0
  189. package/server/engine/integration-coverage.ts +147 -0
  190. package/server/engine/integration-probes.ts +144 -0
  191. package/server/engine/integration-runner.ts +95 -0
  192. package/server/engine/integration-target-profile.ts +103 -0
  193. package/server/engine/kpi-fold.ts +72 -4
  194. package/server/engine/kpi-query.ts +129 -15
  195. package/server/engine/live-feed-registry.ts +2 -1
  196. package/server/engine/load-meter.ts +12 -3
  197. package/server/engine/log.ts +72 -0
  198. package/server/engine/loop-lag.ts +120 -0
  199. package/server/engine/model-gap.ts +168 -0
  200. package/server/engine/restart-policy.ts +4 -4
  201. package/server/engine/runtime-key.ts +1 -1
  202. package/server/engine/stage-path.ts +172 -0
  203. package/server/engine/twin-engine.ts +1334 -168
  204. package/server/index.ts +35 -2
  205. package/server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.ts +54 -0
  206. package/server/migrations/1786200000000-CarryLiveFeedCursor.ts +53 -0
  207. package/server/migrations/index.ts +7 -1
  208. package/server/service/index.ts +11 -0
  209. package/server/service/reference/discovery-result.ts +1 -1
  210. package/server/service/reference/index.ts +6 -1
  211. package/server/service/reference/reference-adapter.ts +220 -5
  212. package/server/service/reference/reference-assessment.ts +250 -0
  213. package/server/service/reference/reference-live.ts +143 -11
  214. package/server/service/reference/reference-master.ts +76 -6
  215. package/server/service/reference/reference-probe.ts +264 -0
  216. package/server/service/reference/reference-progress-subscription.ts +73 -0
  217. package/server/service/reference/reference-progress.ts +95 -0
  218. package/server/service/reference/reference-resolver.ts +200 -12
  219. package/server/service/reference/twin-reference.ts +39 -1
  220. package/server/service/twin-control/twin-control-mutation.ts +4 -3
  221. package/server/service/twin-event/journal-count.ts +53 -0
  222. package/server/service/twin-event/twin-event-keys.ts +16 -1
  223. package/server/service/twin-event/twin-event-type.ts +26 -2
  224. package/server/service/twin-event/twin-event.ts +7 -0
  225. package/server/service/twin-forecast/twin-forecast-query.ts +29 -2
  226. package/server/service/twin-ingest-window/index.ts +7 -0
  227. package/server/service/twin-ingest-window/twin-ingest-window-query.ts +125 -0
  228. package/server/service/twin-ingest-window/twin-ingest-window-writer.ts +57 -0
  229. package/server/service/twin-ingest-window/twin-ingest-window.ts +113 -0
  230. package/server/service/twin-instance/twin-instance.ts +1 -1
  231. package/server/service/twin-journal/twin-journal-query.ts +59 -5
  232. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +77 -3
  233. package/server/service/twin-metrics/twin-metrics-query.ts +81 -3
  234. package/server/service/twin-model/axis-journal-evidence.ts +57 -0
  235. package/server/service/twin-model/epcis-coverage.ts +29 -8
  236. package/server/service/twin-model/iec61850-coverage.ts +2 -8
  237. package/server/service/twin-model/isa95-coverage.ts +107 -14
  238. package/server/service/twin-model/item-ref.ts +80 -0
  239. package/server/service/twin-model/name-index.ts +94 -0
  240. package/server/service/twin-model/project-structure.ts +1 -1
  241. package/server/service/twin-model/status-tally.ts +37 -0
  242. package/server/service/twin-model/twin-lineage-query.ts +38 -9
  243. package/server/service/twin-model/twin-model-item-query.ts +26 -27
  244. package/server/service/twin-model/twin-model-query.ts +155 -4
  245. package/server/service/twin-model/twin-model-tree-query.ts +7 -0
  246. package/server/service/twin-readiness/index.ts +3 -0
  247. package/server/service/twin-readiness/twin-readiness-query.ts +96 -0
  248. package/server/service/twin-space/twin-space-resolver.ts +5 -4
  249. package/shared/entity-delta.ts +31 -7
  250. package/shared/kpi-broadcast.ts +1 -1
  251. package/shared/touched-items.ts +73 -0
  252. package/test/axis-journal-evidence.test.ts +71 -0
  253. package/test/axis-read.test.ts +101 -1
  254. package/test/boot-resume.test.ts +92 -26
  255. package/test/broadcast-cost-baseline.test.ts +200 -0
  256. package/test/broadcast-period.test.ts +58 -0
  257. package/test/canonical-ingest-vocabularies.test.ts +36 -1
  258. package/test/canonical-quantity-door.test.ts +61 -0
  259. package/test/checkpoint-refuses-empty.test.ts +103 -0
  260. package/test/command-routing.test.ts +1 -1
  261. package/test/cursor-stall-not-read-failure.test.ts +126 -0
  262. package/test/declared-location-types.test.ts +89 -0
  263. package/test/discovery-result.test.ts +1 -1
  264. package/test/duration-estimators.test.ts +1 -1
  265. package/test/entity-delta.test.ts +2 -2
  266. package/test/ingest-bench.test.ts +3 -3
  267. package/test/ingest-health-engine.test.ts +248 -0
  268. package/test/ingest-health-wiring.test.ts +119 -0
  269. package/test/ingest-health.test.ts +306 -0
  270. package/test/ingest-history.test.ts +247 -0
  271. package/test/integration-probes.test.ts +103 -0
  272. package/test/integration-runner.test.ts +95 -0
  273. package/test/item-ref.test.ts +78 -0
  274. package/test/journal-read-discipline.test.ts +177 -0
  275. package/test/journal-retention.test.ts +133 -0
  276. package/test/journal-sort-axis.test.ts +142 -0
  277. package/test/journal-write-door.test.ts +110 -0
  278. package/test/journal-write-trend.test.ts +115 -0
  279. package/test/kernel-kind-guard.test.ts +4 -4
  280. package/test/kpi-fold.test.ts +90 -3
  281. package/test/kpi-query-bench.test.ts +2 -2
  282. package/test/lineage-survives-restart.test.ts +17 -1
  283. package/test/live-cursor-wiring.test.ts +87 -0
  284. package/test/live-feed-registry.test.ts +9 -1
  285. package/test/live-kernel-facts.test.ts +7 -7
  286. package/test/live-mirror-parity.test.ts +6 -0
  287. package/test/load-meter.test.ts +29 -15
  288. package/test/local-declarations.test.ts +1 -1
  289. package/test/log-stamp.test.ts +59 -0
  290. package/test/loop-lag.test.ts +82 -0
  291. package/test/mirror-resumes-from-checkpoint.test.ts +192 -0
  292. package/test/model-gap.test.ts +153 -0
  293. package/test/oee-accumulator.test.ts +4 -0
  294. package/test/operational-vocabulary.test.ts +4 -4
  295. package/test/read-failure-visible.test.ts +145 -0
  296. package/test/reference-grounding.test.ts +70 -0
  297. package/test/resolve-ts-siblings.mjs +52 -0
  298. package/test/restart-policy.test.ts +7 -7
  299. package/test/revision-axis.test.ts +104 -0
  300. package/test/runtime-key.test.ts +2 -2
  301. package/test/scale-twin-bench.test.ts +2 -2
  302. package/test/spec-coverage.test.ts +1 -1
  303. package/test/stage-path.test.ts +95 -0
  304. package/test/standard-coverage.test.ts +15 -3
  305. package/test/status-tally.test.ts +55 -0
  306. package/test/structure-revision-db.test.ts +4 -0
  307. package/test/tenant-registry-db.test.ts +1 -1
  308. package/test/time-range.test.ts +152 -0
  309. package/test/touched-items.test.ts +61 -0
  310. package/test/twin-event-keys.test.ts +10 -2
  311. package/test/twin-model-item-db.test.ts +2 -0
  312. package/test/warm-start-seam.test.ts +4 -0
  313. package/test/yield-loop.test.ts +57 -2
  314. package/tsconfig.shared.tsbuildinfo +1 -1
  315. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,250 @@
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
+ /** 시험 명세(점검표)의 수 — 선언되지 않았으면 없다(0 과 구별한다). */
44
+ testSpecifications?: number
45
+ /** 그 명세들이 선언한 판정 기준의 수. */
46
+ criteria?: number
47
+ /**
48
+ * 그중 **아무 한계도 말하지 않는** 기준의 수 — 커널의 `criterionSaysNothing` 과 같은 판정이다.
49
+ *
50
+ * 이 수가 0 이 아니면 「관리점이 있다」가 「판정된다」를 뜻하지 않는다. 세지 않으면 그 구별이 사라진다.
51
+ */
52
+ criteriaWithoutLimit?: number
53
+ }
54
+
55
+ /** 옮기지 못한 것 한 갈래 — 건수 큰 것부터. */
56
+ export interface AssessmentGap {
57
+ /** 무엇을 옮기다 못 옮겼나 — 커넥터가 붙인 이름(`material home warehouse`). */
58
+ what: string
59
+ count: number
60
+ /** 못 옮긴 원본 값 몇 개 — 전부가 아니라 표본이다(수백 개가 붙던 자리다). */
61
+ sample?: string
62
+ /** 그 자리에 대신 들어간 값. */
63
+ fallback?: string
64
+ }
65
+
66
+ export interface ImportAssessment {
67
+ source: string
68
+ siteId: string
69
+ siteName?: string
70
+ system?: string
71
+ built: AssessmentCounts
72
+ /**
73
+ * **원본이 가지고 있던 수** — 커넥터가 말해 준 경우에만 있다.
74
+ *
75
+ * 이름을 `origin` 이라 하지 않는다 — 화면에서 그 낱말이 이미 **트윈의 출처**(`summary.origin`:
76
+ * 템플릿인가 원본인가)를 뜻한다. 같은 이름을 두 뜻으로 쓰면 읽는 사람이 헷갈린다.
77
+ *
78
+ * `built` 와 비교해 「전부 옮겼는가」를 사람이 직접 셀 수 있게 한다. 커넥터가 말하지 않으면 이 자리는
79
+ * 비어 있고, 화면은 분모 없이 옮긴 수만 보인다 — **분모를 지어내지 않는다.**
80
+ */
81
+ originCounts?: Record<string, number>
82
+ gaps: AssessmentGap[]
83
+ /** 이 원본에 개념이 없는 자리 — 커널 낱말로 적는다(화면이 번역한다). */
84
+ absent: string[]
85
+ /**
86
+ * 값이 들어왔지만 **근거가 원본의 저장된 값이 아닌 것** — 파생·유추된 값이다.
87
+ *
88
+ * 이것을 `gaps` 에 섞으면 안 된다. 빈 자리가 아니라 **채워진 자리**이고, 사람이 물을 것은
89
+ * 「왜 비었나」가 아니라 「그 값을 믿어도 되나」다. 조치도 다르다 — 원본을 채우는 것이 아니라
90
+ * 그 규칙이 바뀌면 이 값도 바뀐다는 사실을 아는 것이다.
91
+ */
92
+ derived: AssessmentGap[]
93
+ /** 되물은 횟수·잘린 목록 같은 운영 사실. */
94
+ notes: IngestWarning[]
95
+ }
96
+
97
+ /** 이 마스터가 개념을 선언했나 — 「없다」와 「비어 있다」를 구분한다. */
98
+ const declared = (v: unknown): boolean => Array.isArray(v) ? v.length > 0 : v !== undefined && v !== null
99
+
100
+ /**
101
+ * 마스터 한 벌 → 총평의 사실 층.
102
+ *
103
+ * 경고를 두 갈래로 나눈다: `connector-mapping-fallback` 은 **옮기지 못한 것**이므로 `gaps` 로,
104
+ * 나머지(파생된 값·되물은 횟수·잘린 목록)는 `notes` 로 그대로 나른다. 판단하지 않는다 — 무엇이
105
+ * 심각한지는 건수와 그 자리의 뜻이 말하고, 그 해석은 사람이나 말 층의 몫이다.
106
+ */
107
+ export function assessMaster(master: ReferenceMaster, siteId: string): ImportAssessment {
108
+ const m: any = master
109
+ const d = m.productionSpec?.definition
110
+ const inputs = (d?.recipes ?? []).flatMap((r: any) => r.inputs ?? [])
111
+
112
+ const built: AssessmentCounts = {
113
+ locations: (m.locations ?? []).length,
114
+ areas: (m.locations ?? []).filter((l: any) => l.role === 'area' || l.role === 'both').length,
115
+ equipment: (m.equipment ?? []).length,
116
+ materialDefinitions: (m.materialDefinitions ?? []).length,
117
+ operations: (m.operations ?? []).length,
118
+ ...(declared(m.testSpecifications)
119
+ ? {
120
+ testSpecifications: m.testSpecifications!.length,
121
+ criteria: m.testSpecifications!.reduce((n, t) => n + (t.criteria?.length ?? 0), 0),
122
+ /*
123
+ * **한계를 말하지 않는 기준** — 커널의 `criterionSaysNothing` 과 같은 판정이다. 기준이
124
+ * 선언됐다는 사실만 있고 판정할 재료가 없는 자리이고, 그것을 세지 않으면 「관리점이 있다」가
125
+ * 「판정된다」로 읽힌다.
126
+ */
127
+ criteriaWithoutLimit: m.testSpecifications!.reduce(
128
+ (n, t) => n + (t.criteria ?? []).filter(c => !c.expression && c.limit?.minimum === undefined && c.limit?.maximum === undefined).length,
129
+ 0
130
+ )
131
+ }
132
+ : {}),
133
+ ...(d
134
+ ? {
135
+ materials: (d.materials ?? []).length,
136
+ materialsWithHome: (d.materials ?? []).filter((x: any) => x.locationType).length,
137
+ routes: (d.routes ?? []).length,
138
+ recipes: (d.recipes ?? []).length,
139
+ recipesWithRoute: (d.recipes ?? []).filter((x: any) => x.route).length,
140
+ totalInputs: inputs.length,
141
+ taggedInputs: inputs.filter((i: any) => i.operation).length
142
+ }
143
+ : {})
144
+ }
145
+
146
+ /* 원본의 수는 커넥터가 말한 그대로 나른다 — 여기서 고치거나 채우지 않는다. */
147
+ const originCounts = m.originCounts && Object.keys(m.originCounts).length ? { ...m.originCounts } : undefined
148
+
149
+ const gaps: AssessmentGap[] = []
150
+ const derived: AssessmentGap[] = []
151
+ const notes: IngestWarning[] = []
152
+ /*
153
+ * 커넥터가 「대체」 수집기로 남기는 것 중 일부는 **빈 자리가 아니라 파생된 값**이다(산출량이 원본의
154
+ * 계산 규칙에서 온 경우처럼). 같은 통로로 오지만 뜻이 달라 구분해 담는다 — 이름에 그 사실이 적혀 있다.
155
+ */
156
+ const isDerived = (field: string) => /derived|not a stored field/i.test(field)
157
+ for (const w of m.warnings ?? []) {
158
+ if (w?.code === 'connector-mapping-fallback' && w.params?.field) {
159
+ const values = String(w.params.values ?? '')
160
+ const bin = isDerived(String(w.params.field)) ? derived : gaps
161
+ bin.push({
162
+ what: String(w.params.field),
163
+ count: Number(w.params.count ?? 0),
164
+ /* 값은 표본만 — 이 자리에 UUID 수백 개가 붙어 화면을 밀어냈다. 전체는 경고 원문에 남는다. */
165
+ ...(values ? { sample: values.length > 120 ? `${values.slice(0, 120)}…` : values } : {}),
166
+ ...(w.params.fallback ? { fallback: String(w.params.fallback) } : {})
167
+ })
168
+ continue
169
+ }
170
+ notes.push(w)
171
+ }
172
+ gaps.sort((a, b) => b.count - a.count)
173
+ derived.sort((a, b) => b.count - a.count)
174
+
175
+ /*
176
+ * 이 원본에 개념이 없는 자리. **커널 낱말로 적는다** — 화면이 그것으로 번역하고, 개념 지도가 이미
177
+ * 같은 낱말을 쓴다(두 화면이 다른 말을 하면 사람이 둘을 다른 사실로 읽는다).
178
+ */
179
+ const absent: string[] = []
180
+ if (!declared(m.persons)) absent.push('persons')
181
+ if (!declared(m.assets)) absent.push('assets')
182
+ if (!declared(m.testSpecifications)) absent.push('testSpecifications')
183
+ if (!declared(m.personnelClasses)) absent.push('personnelClasses')
184
+ if (!declared(m.assetClasses)) absent.push('assetClasses')
185
+ if (!declared(m.materialClasses)) absent.push('materialClasses')
186
+ if (!d) absent.push('productionSpec')
187
+
188
+ return {
189
+ source: m.source,
190
+ siteId,
191
+ siteName: m.siteName,
192
+ system: m.system,
193
+ built,
194
+ ...(originCounts ? { originCounts } : {}),
195
+ gaps,
196
+ derived,
197
+ absent,
198
+ notes
199
+ }
200
+ }
201
+
202
+ /** 사람이 읽는 줄 — 터미널·로그가 읽는다(화면은 구조를 그대로 그린다). */
203
+ export function formatAssessment(a: ImportAssessment): string {
204
+ const c = a.built
205
+ const out: string[] = []
206
+ /**
207
+ * 분모가 있으면 함께 적는다 — 「1,261」과 「원본 1,408 중 1,261」은 다른 말이다.
208
+ * 커넥터가 그 수를 말하지 않았으면 옮긴 수만 적는다(지어내지 않는다).
209
+ */
210
+ const n = (key: keyof AssessmentCounts, v?: number) => {
211
+ if (v === undefined) return '없음'
212
+ const o = a.originCounts?.[key as string]
213
+ return o === undefined ? String(v) : o === v ? `${v}(원본 전부)` : `${v} / 원본 ${o}`
214
+ }
215
+ out.push(`총평 ${a.siteName ?? a.siteId} (${a.system ?? '?'}) — 원본 ${a.source}`)
216
+ out.push(
217
+ ` 선 것: 로케이션 ${n('locations', c.locations)}(존 ${c.areas}) · 설비 ${n('equipment', c.equipment)} · ` +
218
+ `품목 정의 ${n('materialDefinitions', c.materialDefinitions)} · 공정 ${n('operations', c.operations)}`
219
+ )
220
+ if (c.materials !== undefined) {
221
+ out.push(
222
+ ` 자재 ${n('materials', c.materials)}(보관 로케이션 있는 것 ${c.materialsWithHome}) · ` +
223
+ `경로 ${n('routes', c.routes)} · 레시피 ${n('recipes', c.recipes)}(경로 붙은 것 ${c.recipesWithRoute}) · ` +
224
+ `투입 ${n('totalInputs', c.totalInputs)}(공정을 말하는 것 ${c.taggedInputs})`
225
+ )
226
+ } else {
227
+ out.push(' 생산 선언 없음 — 레시피가 하나도 성립하지 않았다')
228
+ }
229
+ if (a.gaps.length) {
230
+ out.push(' 옮기지 못한 것(건수 순):')
231
+ for (const g of a.gaps) out.push(` ${String(g.count).padStart(6)} ${g.what}${g.sample ? ` — ${g.sample}` : ''}`)
232
+ }
233
+ if (a.derived.length) {
234
+ out.push(' 파생된 값(저장된 값이 아니다):')
235
+ for (const g of a.derived) out.push(` ${g.what}${g.sample ? ` — ${g.sample}` : ''}`)
236
+ }
237
+ if (c.testSpecifications) {
238
+ /*
239
+ * 관리 기준은 계측이 오는지와 무관하게 값이 있다 — 「관리점이 셋인데 둘은 아무 한계도 말하지
240
+ * 않는다」를 여기서 볼 수 있어야 판정에 근거가 있는지 사람이 안다.
241
+ */
242
+ out.push(
243
+ ` 관리 기준: 점검표 ${n('testSpecifications', c.testSpecifications)} · 기준 ${c.criteria ?? 0}` +
244
+ (c.criteriaWithoutLimit ? ` (한계를 말하지 않는 것 ${c.criteriaWithoutLimit})` : '')
245
+ )
246
+ }
247
+ if (a.absent.length) out.push(` 이 원본에 개념이 없는 자리: ${a.absent.join(', ')}`)
248
+ for (const n of a.notes) out.push(` 참고 ${n.code}: ${n.message}`)
249
+ return out.join('\n')
250
+ }
@@ -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,149 @@ 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
- if (accepted.length) TwinEngine.ingestLive(domainId, instanceId, accepted)
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`.
54
84
  */
85
+ /*
86
+ * **읽었다는 것 자체가 사실이다** — 빈 읽기도 성공이므로 단절 기록을 지운다(§`onReadFailure`).
87
+ * 그 둘을 섞으면 조용한 원본이 끊긴 원본으로 보인다.
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
- console.warn(`[twin-live] "${instanceId}": ${rejected.length} record(s) rejected — e.g. ${why}`)
93
+ twinWarn(`[twin-live] "${instanceId}": ${rejected.length}/${offered} record(s) rejected — e.g. ${why}`)
58
94
  }
59
- })
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
+ )
101
+ }
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
+ /**
168
+ * **읽었는데 창을 넘길 수 없다** — 위와 조치가 반대인 사실이다 (2026-08-24).
169
+ *
170
+ * 이 문을 만들기 전에는 이 사실이 `onReadFailure` 로 나갔다. 그래서 화면이 「원본에 닿지 못한다」고
171
+ * 말했는데 실제로는 원본이 **답한** 상태였고, 그 답의 모양이 커서를 이긴 것이었다. 사람은 원본을
172
+ * 의심하고 기다리는데 기다림으로는 영원히 풀리지 않는다 — **원인을 반대 방향으로 가리켰다.**
173
+ *
174
+ * `status` 를 건드리지 않는 규율은 위와 같다(재부착의 자격이다). `lastError` 에는 적는다 — 목록에서
175
+ * 이유를 볼 수 있어야 하고, 「닿지 못한다」와 다른 문장이어야 한다.
176
+ */
177
+ onCursorStall: ({ reason, stream }) => {
178
+ TwinEngine.recordIngestCursorStall(domainId, instanceId, reason, Date.now(), stream)
179
+ twinWarn(
180
+ `[twin-live] "${source}": read the source but the cursor cannot advance` +
181
+ `${stream ? ` (${stream})` : ''} — ${reason}. Waiting will not clear this.`
182
+ )
183
+ repo
184
+ .update({ id: row.id }, { lastError: `live cursor stalled: ${reason}` } as any)
185
+ .catch(err => twinWarn(`[twin-live] "${source}": cursor stall not recorded — ${err?.message ?? err}`))
186
+ },
187
+ onCursor: next => {
188
+ /* 어댑터가 준 모양을 그대로 적는다 — 이 층은 흐름의 뜻을 모른다(열쇠는 어댑터가 정한다). */
189
+ repo
190
+ .update({ id: row.id }, { liveCursor: next } as any)
191
+ .catch(err => twinWarn(`[twin-live] "${source}": live cursor not saved — ${err?.message ?? err}`))
192
+ }
193
+ }
194
+ }
195
+
64
196
  /** 라이브 피드 중지(unsubscribe). */
65
197
  export function stopReferenceLiveFeed(instanceId: string): boolean {
66
198
  const unsub = feeds.get(instanceId)
@@ -151,7 +283,7 @@ export async function resumeReferenceLiveFeeds(): Promise<{ resumed: string[]; f
151
283
  }
152
284
  attached.set(p.instanceId, ref.source)
153
285
  try {
154
- const ok = await startReferenceLiveFeed(domainId, ref.adapterType, ref.connectionConfig ?? {}, site, p.instanceId)
286
+ const ok = await startReferenceLiveFeed(domainId, ref.adapterType, ref.connectionConfig ?? {}, site, p.instanceId, ref.source)
155
287
  if (ok) resumed.push(p.instanceId)
156
288
  else failed.push({ instanceId: p.instanceId, error: 'adapter has no openLiveFeed' })
157
289
  } catch (e: any) {
@@ -160,12 +292,12 @@ export async function resumeReferenceLiveFeeds(): Promise<{ resumed: string[]; f
160
292
  }
161
293
  }
162
294
  } catch (e: any) {
163
- console.error('[twin-live] feed resume scan failed', e?.message)
295
+ twinError('[twin-live] feed resume scan failed', e?.message)
164
296
  }
165
297
 
166
- if (resumed.length) console.log(`[twin-live] reattached ${resumed.length} feed(s): ${resumed.join(', ')}`)
298
+ if (resumed.length) twinLog(`[twin-live] reattached ${resumed.length} feed(s): ${resumed.join(', ')}`)
167
299
  /* **조용히 넘기지 않는다** — 붙지 않은 미러는 「도는데 아무것도 오지 않는」 트윈이다. */
168
- for (const f of failed) console.warn(`[twin-live] "${f.instanceId}" feed NOT reattached — ${f.error}`)
300
+ for (const f of failed) twinWarn(`[twin-live] "${f.instanceId}" feed NOT reattached — ${f.error}`)
169
301
  return { resumed, failed }
170
302
  }
171
303
 
@@ -7,7 +7,7 @@
7
7
  * 좌표·representations 는 트윈 공간층(보완/추가). 실 시스템이 좌표를 안 주면 layout 없이 위상만 인제스트.
8
8
  */
9
9
 
10
- import type { DomainSystem, EquipmentLevel, WorkCalendarEntry } from '@operato/twin-kernel'
10
+ import type { DomainSystem, EquipmentLevel, TestSpecificationCriterion, WorkCalendarEntry } from '@operato/twin-kernel'
11
11
 
12
12
  export interface RefLocation {
13
13
  id: string
@@ -127,6 +127,16 @@ export interface RefTestSpecification {
127
127
  /** 표시명 — i18n 키일 수 있다. 읽는 어휘를 하나로 맞추려고 인제스트가 `name` 으로 옮긴다. */
128
128
  description?: string
129
129
  version?: string
130
+ /**
131
+ * 판정 기준들 — 커널 계약의 `TestSpecification.criteria` 를 그대로 나른다.
132
+ *
133
+ * **경계에서 어휘를 갈지 않는다.** 커널 타입을 그대로 쓰므로 `limit.{minimum,maximum,uom}` ·
134
+ * `expression` · `evaluatedPropertyId` 가 원본에서 커널까지 한 이름으로 간다.
135
+ *
136
+ * 선언하지 않아도 명세는 성립한다(이름만으로 「무엇으로 검증했나」에 답한다). 선언하면 **판정에
137
+ * 근거가 있는지**를 물을 수 있게 된다 — 커널의 `criterionSaysNothing` 이 그것을 센다.
138
+ */
139
+ criteria?: TestSpecificationCriterion[]
130
140
  }
131
141
 
132
142
  export interface RefEquipment {
@@ -350,7 +360,7 @@ export type MasterOrigin =
350
360
  * `source` 는 **레퍼런스 등록 이름**이고 그 참조가 보고하는 사이트 전부에 같다. 그래서 어느 사이트에서
351
361
  * 온 트윈인지는 `siteId` 가 있어야 안다 — 없으면 다시 읽기가 사이트를 찾지 못한다(2026-08-18 실측:
352
362
  * 사이트가 여러 개인 참조에서 온 트윈은 재동기가 전부 거절됐다. 참조 이름을 사이트별 마스터의
353
- * `source`(=인스턴스 id)와 견주고 있었다 — 서로 다른 것을 비교한 것이다).
363
+ * `source`(=인스턴스 id)와 비교하고 있었다 — 서로 다른 것을 비교한 것이다).
354
364
  */
355
365
  | { kind: 'reference'; source: string; siteId?: string; spaceId?: string }
356
366
  /**
@@ -407,6 +417,21 @@ export interface ReferenceMaster {
407
417
  * BOM 을 설명 문서에만 적어 둔 채 키트를 못 만드는 트윈을 만든 원인). 근거는 커널 계약 주석에.
408
418
  */
409
419
  productionSpec?: unknown
420
+ /**
421
+ * **이 원본이 식별자를 만드는 이름공간** — `TwinModelDef.identity` 로 통과한다.
422
+ *
423
+ * **선언이 없으면 커널이 거절한다** (0.7.45~). 예전에는 상수 프리픽스로 팔레트·트레일러·문서를
424
+ * 만들었는데(`0614141` — GS1 이 규격 예제로 쓰다 버린 값이고 `06…` 은 실 배정 대역이다), 그 값은
425
+ * 저널에 영원히 남으면서 지어낸 것이라고 말해 주는 데가 없었다. 그래서 커널이 만들지 않는다.
426
+ *
427
+ * 원본이 자기 도메인을 말하면 그 아래에서 만든다 — 그것이 표준 정합이다(CBV 2.0 §8.2.4·§8.5.5,
428
+ * 「그 인터넷 도메인 소유자가 배정한다」). GS1 키를 쓰려면 `documentTypes` + `companyPrefix` 이고,
429
+ * 그때 **자리 수가 맞아야 한다**(SGTIN 은 프리픽스 + 품목참조 = 13자리, GS1 TDS).
430
+ *
431
+ * 정체성은 생산과 무관하므로 `productionSpec` 이 아니라 **모델**이 갖는다 — 창고·야드는 아무것도
432
+ * 생산하지 않으면서 팔레트·트레일러를 식별한다.
433
+ */
434
+ identity?: unknown
410
435
  /**
411
436
  * 사람 — 선언하지 않으면 **인원 제약이 없는 트윈**이 된다(설비만 있으면 언제나 가동되는 공장).
412
437
  * 공정이 인원을 요구하는데 사람이 없으면 그 작업은 시작되지 않는다 — 그것이 사실이므로 그대로 둔다.
@@ -435,6 +460,21 @@ export interface ReferenceMaster {
435
460
  * 놓고도 조용히 성공했다. 옮기는 사람만 아는 사실은 옮기는 자리에서 실어야 한다.
436
461
  */
437
462
  warnings?: IngestWarning[]
463
+ /**
464
+ * **원본이 가진 수** — 옮긴 수의 분모다.
465
+ *
466
+ * 총평은 도착한 모델만 볼 수 있어서 「레시피 1,261」까지만 말할 수 있었다. 그런데 사람이 묻는 것은
467
+ * 「그게 전부인가」다 — 원본에 1,408개가 있었고 147개를 옮기지 못했다면 그 사실이 총평의 첫 줄에
468
+ * 있어야 한다. 실제로 사용자가 품목 2,789 와 자재 1,789 의 차이를 화면에서 읽어 내지 못해 직접
469
+ * 물었다(2026-08-22).
470
+ *
471
+ * 분모를 아는 것은 **커넥터뿐이다.** 그래서 커넥터가 실어 보낸다. 보내지 않으면 총평은 분모 없이
472
+ * 옮긴 수만 말한다 — **지어내지 않는다.**
473
+ *
474
+ * 열쇠는 총평이 세는 이름과 같다(`recipes`·`materials`·`totalInputs`·…). 값은 **원본을 끝까지 읽어서
475
+ * 센 것**이어야 한다. 잘린 읽기의 수를 여기 담으면 「전부 옮겼다」는 거짓이 된다.
476
+ */
477
+ originCounts?: Record<string, number>
438
478
  }
439
479
 
440
480
  export interface IngestedTwin {
@@ -471,7 +511,7 @@ export function utcOffsetOf(timezone?: string, at: Date = new Date()): number |
471
511
  const sign = m[1] === '-' ? -1 : 1
472
512
  return sign * (Number(m[2]) * 60 + Number(m[3] ?? 0))
473
513
  } catch {
474
- return undefined // 알 수 없는 이름 — Intl 이 던진다
514
+ return undefined // 알 수 없는 이름 — Intl 이 오류를 낸다
475
515
  }
476
516
  }
477
517
 
@@ -524,13 +564,37 @@ export const generationWithoutTimeZone = (spaceId: string, equipmentId: string):
524
564
  'so the generation peak will not land at local noon. Declare the space time zone (we do not guess one).'
525
565
  })
526
566
 
527
- /** 커널 카탈로그가 모르는 자리 타입 — 그 자리는 흐름에 참여하지 못할 수 있다. */
567
+ /**
568
+ * 아무도 선언하지 않은 자리 타입 — 그 자리는 흐름에 참여하지 못할 수 있다.
569
+ *
570
+ * **묻는 상대를 고쳤다 (2026-08-20).** 예전에는 내장 카탈로그에게만 물었다. 그런데 원본이 자기
571
+ * `DomainDefinition.locationTypes` 를 함께 보내면 **그 선언이 정본**이다 — 카탈로그는 우리가 아는
572
+ * 도메인들의 기본 어휘일 뿐이다. 물을 상대가 틀렸으니, 자기 어휘를 선언한 원본은 정상적인 타입마다
573
+ * 경고를 받았다(F&B 라인을 붙이면 자리마다 하나씩). 소음이 되면 사람은 경고를 읽지 않게 되고, 그때
574
+ * 진짜 경고도 함께 죽는다.
575
+ */
528
576
  export const unknownLocationType = (system: string, type: string, id: string): IngestWarning => ({
529
577
  code: 'unknown-location-type',
530
578
  params: { system, type, id },
531
- message: `"${type}" is not a known ${system} location type (${id}) — the kernel may not include this location in the flow`
579
+ message:
580
+ `"${type}" is declared nowhere for ${system} (${id}) — neither in the master's own locationTypes nor in the built-in ` +
581
+ `catalog, so the kernel may not include this location in the flow`
532
582
  })
533
583
 
584
+ /**
585
+ * 원본이 함께 보낸 자리 타입 선언 — 있으면 **이것이 정본**이다.
586
+ *
587
+ * 커널 계약의 `DomainDefinition.locationTypes` 는 `{ key, label, … }` 목록이다. 읽을 수 없는 모양이면
588
+ * `undefined` 를 돌려 **카탈로그로 떨어진다** — 빈 배열을 돌려주면 「아무 타입도 선언되지 않았다」가
589
+ * 되어 모든 자리가 경고를 받는다(없는 것과 못 읽은 것은 다르다).
590
+ */
591
+ export function declaredLocationTypes(productionSpec: unknown): string[] | undefined {
592
+ const list = (productionSpec as any)?.definition?.locationTypes
593
+ if (!Array.isArray(list) || !list.length) return undefined
594
+ const keys = list.map((t: any) => (typeof t === 'string' ? t : t?.key)).filter((k: any) => typeof k === 'string' && k)
595
+ return keys.length ? keys : undefined
596
+ }
597
+
534
598
  /** 시간대 문자열을 못 읽었다 — 시각 해석이 UTC 로 떨어진다(조용히 현지 시각으로 가정하지 않는다). */
535
599
  export const unknownTimezone = (timezone: string): IngestWarning => ({
536
600
  code: 'unknown-timezone',
@@ -638,7 +702,11 @@ export const siteEquipmentAdopted = (key: string, count: number): IngestWarning
638
702
  export function masterToTwin(master: ReferenceMaster, catalog?: any): IngestedTwin {
639
703
  /* 커넥터가 옮기며 알게 된 것을 먼저 싣는다 — 원인이 앞에 오는 편이 읽기 쉽다(대체가 먼저, 그 결과가 뒤). */
640
704
  const warnings: IngestWarning[] = [...(master.warnings ?? [])]
641
- const known: string[] = catalog?.[master.system]?.locationTypes ?? []
705
+ /*
706
+ * 원본이 자기 어휘를 선언했으면 **그것에 대조한다.** 내장 카탈로그는 그 선언이 없을 때의 기본이다 —
707
+ * 상대를 바꾸지 않으면, 자기 어휘를 선언한 원본이 정상적인 자리마다 경고를 받는다.
708
+ */
709
+ const known: string[] = declaredLocationTypes(master.productionSpec) ?? catalog?.[master.system]?.locationTypes ?? []
642
710
 
643
711
  const locationEntries = master.locations.filter(l => l.role === 'location' || l.role === 'both')
644
712
  const areaLocs = master.locations.filter(l => l.role === 'area' || l.role === 'both')
@@ -740,6 +808,8 @@ export function masterToTwin(master: ReferenceMaster, catalog?: any): IngestedTw
740
808
  locations, equipment, layout, spaceId,
741
809
  /* 생산 정의도 손대지 않고 통과 — 경계에서 어휘를 갈지 않는다(커널이 그대로 소비한다). */
742
810
  ...(master.productionSpec ? { productionSpec: master.productionSpec } : {}),
811
+ /* 정체성 선언도 그대로 통과한다 — 없으면 만들지 않는다(커널이 레거시 상수로 떨어진다). */
812
+ ...(master.identity ? { identity: master.identity } : {}),
743
813
  ...(materialDefinitions?.length ? { materialDefinitions } : {}),
744
814
  ...(materialClasses?.length ? { materialClasses } : {}),
745
815
  ...(utcOffsetMinutes !== undefined ? { utcOffsetMinutes } : {}),