@things-factory/headless-twin 10.0.11 → 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.
Files changed (369) hide show
  1. package/dist-server/engine/canonical-ingest.d.ts +81 -3
  2. package/dist-server/engine/canonical-ingest.js +89 -10
  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/declared-stimulus.d.ts +43 -0
  7. package/dist-server/engine/declared-stimulus.js +57 -0
  8. package/dist-server/engine/declared-stimulus.js.map +1 -0
  9. package/dist-server/engine/index.d.ts +5 -0
  10. package/dist-server/engine/index.js +6 -0
  11. package/dist-server/engine/index.js.map +1 -1
  12. package/dist-server/engine/ingest-health.d.ts +335 -0
  13. package/dist-server/engine/ingest-health.js +434 -0
  14. package/dist-server/engine/ingest-health.js.map +1 -0
  15. package/dist-server/engine/integration-coverage.d.ts +76 -0
  16. package/dist-server/engine/integration-coverage.js +73 -0
  17. package/dist-server/engine/integration-coverage.js.map +1 -0
  18. package/dist-server/engine/integration-probes.d.ts +65 -0
  19. package/dist-server/engine/integration-probes.js +100 -0
  20. package/dist-server/engine/integration-probes.js.map +1 -0
  21. package/dist-server/engine/integration-runner.d.ts +45 -0
  22. package/dist-server/engine/integration-runner.js +59 -0
  23. package/dist-server/engine/integration-runner.js.map +1 -0
  24. package/dist-server/engine/integration-target-profile.d.ts +57 -0
  25. package/dist-server/engine/integration-target-profile.js +79 -0
  26. package/dist-server/engine/integration-target-profile.js.map +1 -0
  27. package/dist-server/engine/kpi-fold.d.ts +39 -0
  28. package/dist-server/engine/kpi-fold.js +68 -5
  29. package/dist-server/engine/kpi-fold.js.map +1 -1
  30. package/dist-server/engine/kpi-query.d.ts +3 -3
  31. package/dist-server/engine/kpi-query.js +132 -18
  32. package/dist-server/engine/kpi-query.js.map +1 -1
  33. package/dist-server/engine/live-feed-registry.d.ts +1 -1
  34. package/dist-server/engine/live-feed-registry.js +3 -17
  35. package/dist-server/engine/live-feed-registry.js.map +1 -1
  36. package/dist-server/engine/load-meter.d.ts +1 -1
  37. package/dist-server/engine/load-meter.js +12 -3
  38. package/dist-server/engine/load-meter.js.map +1 -1
  39. package/dist-server/engine/local-declarations.d.ts +3 -6
  40. package/dist-server/engine/local-declarations.js +97 -16
  41. package/dist-server/engine/local-declarations.js.map +1 -1
  42. package/dist-server/engine/log.d.ts +18 -0
  43. package/dist-server/engine/log.js +80 -0
  44. package/dist-server/engine/log.js.map +1 -0
  45. package/dist-server/engine/loop-lag.d.ts +54 -0
  46. package/dist-server/engine/loop-lag.js +87 -0
  47. package/dist-server/engine/loop-lag.js.map +1 -0
  48. package/dist-server/engine/measured-yield.d.ts +42 -0
  49. package/dist-server/engine/measured-yield.js +75 -0
  50. package/dist-server/engine/measured-yield.js.map +1 -0
  51. package/dist-server/engine/model-gap.d.ts +108 -0
  52. package/dist-server/engine/model-gap.js +95 -0
  53. package/dist-server/engine/model-gap.js.map +1 -0
  54. package/dist-server/engine/operation-basis.d.ts +16 -0
  55. package/dist-server/engine/operation-basis.js +20 -2
  56. package/dist-server/engine/operation-basis.js.map +1 -1
  57. package/dist-server/engine/property-effects.js +17 -0
  58. package/dist-server/engine/property-effects.js.map +1 -1
  59. package/dist-server/engine/restart-policy.d.ts +13 -0
  60. package/dist-server/engine/restart-policy.js +52 -0
  61. package/dist-server/engine/restart-policy.js.map +1 -0
  62. package/dist-server/engine/runtime-key.js +1 -1
  63. package/dist-server/engine/runtime-key.js.map +1 -1
  64. package/dist-server/engine/spec-coverage.d.ts +8 -0
  65. package/dist-server/engine/spec-coverage.js +3 -1
  66. package/dist-server/engine/spec-coverage.js.map +1 -1
  67. package/dist-server/engine/stage-path.d.ts +83 -0
  68. package/dist-server/engine/stage-path.js +118 -0
  69. package/dist-server/engine/stage-path.js.map +1 -0
  70. package/dist-server/engine/twin-engine.d.ts +400 -31
  71. package/dist-server/engine/twin-engine.js +1198 -187
  72. package/dist-server/engine/twin-engine.js.map +1 -1
  73. package/dist-server/index.js +35 -3
  74. package/dist-server/index.js.map +1 -1
  75. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.d.ts +5 -0
  76. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js +58 -0
  77. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js.map +1 -0
  78. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.d.ts +5 -0
  79. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js +55 -0
  80. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js.map +1 -0
  81. package/dist-server/migrations/index.js +7 -1
  82. package/dist-server/migrations/index.js.map +1 -1
  83. package/dist-server/service/index.d.ts +5 -2
  84. package/dist-server/service/index.js +18 -7
  85. package/dist-server/service/index.js.map +1 -1
  86. package/dist-server/service/reference/control-routing.d.ts +14 -0
  87. package/dist-server/service/reference/control-routing.js +64 -0
  88. package/dist-server/service/reference/control-routing.js.map +1 -0
  89. package/dist-server/service/reference/discovery-result.d.ts +1 -1
  90. package/dist-server/service/reference/discovery-result.js +1 -1
  91. package/dist-server/service/reference/discovery-result.js.map +1 -1
  92. package/dist-server/service/reference/index.d.ts +5 -1
  93. package/dist-server/service/reference/index.js +8 -1
  94. package/dist-server/service/reference/index.js.map +1 -1
  95. package/dist-server/service/reference/reference-adapter.d.ts +251 -3
  96. package/dist-server/service/reference/reference-adapter.js +43 -1
  97. package/dist-server/service/reference/reference-adapter.js.map +1 -1
  98. package/dist-server/service/reference/reference-assessment.d.ts +68 -0
  99. package/dist-server/service/reference/reference-assessment.js +136 -0
  100. package/dist-server/service/reference/reference-assessment.js.map +1 -0
  101. package/dist-server/service/reference/reference-live.d.ts +12 -1
  102. package/dist-server/service/reference/reference-live.js +119 -14
  103. package/dist-server/service/reference/reference-live.js.map +1 -1
  104. package/dist-server/service/reference/reference-master.d.ts +47 -1
  105. package/dist-server/service/reference/reference-master.js +34 -4
  106. package/dist-server/service/reference/reference-master.js.map +1 -1
  107. package/dist-server/service/reference/reference-probe.d.ts +92 -0
  108. package/dist-server/service/reference/reference-probe.js +186 -0
  109. package/dist-server/service/reference/reference-probe.js.map +1 -0
  110. package/dist-server/service/reference/reference-progress-subscription.d.ts +17 -0
  111. package/dist-server/service/reference/reference-progress-subscription.js +94 -0
  112. package/dist-server/service/reference/reference-progress-subscription.js.map +1 -0
  113. package/dist-server/service/reference/reference-progress.d.ts +38 -0
  114. package/dist-server/service/reference/reference-progress.js +71 -0
  115. package/dist-server/service/reference/reference-progress.js.map +1 -0
  116. package/dist-server/service/reference/reference-resolver.d.ts +3 -3
  117. package/dist-server/service/reference/reference-resolver.js +242 -58
  118. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  119. package/dist-server/service/reference/twin-reference.d.ts +32 -0
  120. package/dist-server/service/reference/twin-reference.js +10 -0
  121. package/dist-server/service/reference/twin-reference.js.map +1 -1
  122. package/dist-server/service/twin-audit/command-audit.d.ts +34 -0
  123. package/dist-server/service/twin-audit/command-audit.js +15 -1
  124. package/dist-server/service/twin-audit/command-audit.js.map +1 -1
  125. package/dist-server/service/twin-audit/twin-audit-event.d.ts +3 -0
  126. package/dist-server/service/twin-audit/twin-audit-event.js +10 -0
  127. package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -1
  128. package/dist-server/service/twin-control/twin-control-mutation.d.ts +15 -2
  129. package/dist-server/service/twin-control/twin-control-mutation.js +88 -36
  130. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  131. package/dist-server/service/twin-event/journal-count.d.ts +21 -0
  132. package/dist-server/service/twin-event/journal-count.js +26 -0
  133. package/dist-server/service/twin-event/journal-count.js.map +1 -0
  134. package/dist-server/service/twin-event/twin-event-keys.d.ts +9 -0
  135. package/dist-server/service/twin-event/twin-event-keys.js +15 -18
  136. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  137. package/dist-server/service/twin-event/twin-event-type.d.ts +3 -1
  138. package/dist-server/service/twin-event/twin-event-type.js +18 -1
  139. package/dist-server/service/twin-event/twin-event-type.js.map +1 -1
  140. package/dist-server/service/twin-event/twin-event.d.ts +1 -0
  141. package/dist-server/service/twin-event/twin-event.js +9 -1
  142. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  143. package/dist-server/service/twin-forecast/twin-forecast-query.js +2 -1
  144. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  145. package/dist-server/service/twin-ingest-window/index.d.ts +5 -0
  146. package/dist-server/service/twin-ingest-window/index.js +10 -0
  147. package/dist-server/service/twin-ingest-window/index.js.map +1 -0
  148. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.d.ts +12 -0
  149. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js +123 -0
  150. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js.map +1 -0
  151. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.d.ts +1 -0
  152. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js +51 -0
  153. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js.map +1 -0
  154. package/dist-server/service/twin-ingest-window/twin-ingest-window.d.ts +16 -0
  155. package/dist-server/service/twin-ingest-window/twin-ingest-window.js +108 -0
  156. package/dist-server/service/twin-ingest-window/twin-ingest-window.js.map +1 -0
  157. package/dist-server/service/twin-instance/twin-instance.d.ts +1 -1
  158. package/dist-server/service/twin-instance/twin-instance.js +2 -2
  159. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  160. package/dist-server/service/twin-journal/twin-journal-query.js +58 -5
  161. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  162. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +14 -0
  163. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +74 -3
  164. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  165. package/dist-server/service/twin-metrics/twin-metrics-query.d.ts +1 -0
  166. package/dist-server/service/twin-metrics/twin-metrics-query.js +78 -3
  167. package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
  168. package/dist-server/service/twin-model/axis-journal-evidence.d.ts +2 -0
  169. package/dist-server/service/twin-model/axis-journal-evidence.js +89 -0
  170. package/dist-server/service/twin-model/axis-journal-evidence.js.map +1 -0
  171. package/dist-server/service/twin-model/epcis-coverage.d.ts +1 -1
  172. package/dist-server/service/twin-model/epcis-coverage.js +19 -7
  173. package/dist-server/service/twin-model/epcis-coverage.js.map +1 -1
  174. package/dist-server/service/twin-model/iec61850-coverage.d.ts +1 -1
  175. package/dist-server/service/twin-model/iec61850-coverage.js +18 -7
  176. package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -1
  177. package/dist-server/service/twin-model/isa95-coverage.d.ts +14 -0
  178. package/dist-server/service/twin-model/isa95-coverage.js +23 -9
  179. package/dist-server/service/twin-model/isa95-coverage.js.map +1 -1
  180. package/dist-server/service/twin-model/item-ref.d.ts +24 -0
  181. package/dist-server/service/twin-model/item-ref.js +88 -0
  182. package/dist-server/service/twin-model/item-ref.js.map +1 -0
  183. package/dist-server/service/twin-model/name-index.d.ts +36 -0
  184. package/dist-server/service/twin-model/name-index.js +116 -0
  185. package/dist-server/service/twin-model/name-index.js.map +1 -0
  186. package/dist-server/service/twin-model/project-structure.js +1 -1
  187. package/dist-server/service/twin-model/project-structure.js.map +1 -1
  188. package/dist-server/service/twin-model/status-tally.d.ts +9 -0
  189. package/dist-server/service/twin-model/status-tally.js +37 -0
  190. package/dist-server/service/twin-model/status-tally.js.map +1 -0
  191. package/dist-server/service/twin-model/twin-lineage-query.js +40 -12
  192. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
  193. package/dist-server/service/twin-model/twin-model-item-query.js +38 -39
  194. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
  195. package/dist-server/service/twin-model/twin-model-query.js +157 -5
  196. package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
  197. package/dist-server/service/twin-model/twin-model-tree-query.js +7 -0
  198. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
  199. package/dist-server/service/twin-readiness/index.d.ts +2 -0
  200. package/dist-server/service/twin-readiness/index.js +6 -0
  201. package/dist-server/service/twin-readiness/index.js.map +1 -0
  202. package/dist-server/service/twin-readiness/twin-readiness-query.d.ts +3 -0
  203. package/dist-server/service/twin-readiness/twin-readiness-query.js +103 -0
  204. package/dist-server/service/twin-readiness/twin-readiness-query.js.map +1 -0
  205. package/dist-server/service/twin-space/twin-space-resolver.d.ts +2 -2
  206. package/dist-server/service/twin-space/twin-space-resolver.js +5 -4
  207. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  208. package/dist-shared/entity-delta.d.ts +23 -3
  209. package/dist-shared/entity-delta.js +12 -8
  210. package/dist-shared/entity-delta.js.map +1 -1
  211. package/dist-shared/kpi-broadcast.js +1 -1
  212. package/dist-shared/kpi-broadcast.js.map +1 -1
  213. package/dist-shared/touched-items.d.ts +7 -0
  214. package/dist-shared/touched-items.js +80 -0
  215. package/dist-shared/touched-items.js.map +1 -0
  216. package/package.json +7 -7
  217. package/server/engine/canonical-ingest.ts +164 -15
  218. package/server/engine/command-routing.ts +1 -1
  219. package/server/engine/declared-stimulus.ts +66 -0
  220. package/server/engine/index.ts +6 -0
  221. package/server/engine/ingest-health.ts +704 -0
  222. package/server/engine/integration-coverage.ts +147 -0
  223. package/server/engine/integration-probes.ts +144 -0
  224. package/server/engine/integration-runner.ts +95 -0
  225. package/server/engine/integration-target-profile.ts +103 -0
  226. package/server/engine/kpi-fold.ts +101 -5
  227. package/server/engine/kpi-query.ts +137 -23
  228. package/server/engine/live-feed-registry.ts +4 -3
  229. package/server/engine/load-meter.ts +12 -3
  230. package/server/engine/local-declarations.ts +97 -19
  231. package/server/engine/log.ts +72 -0
  232. package/server/engine/loop-lag.ts +120 -0
  233. package/server/engine/measured-yield.ts +89 -0
  234. package/server/engine/model-gap.ts +168 -0
  235. package/server/engine/operation-basis.ts +33 -2
  236. package/server/engine/property-effects.ts +17 -0
  237. package/server/engine/restart-policy.ts +55 -0
  238. package/server/engine/runtime-key.ts +1 -1
  239. package/server/engine/spec-coverage.ts +23 -3
  240. package/server/engine/stage-path.ts +172 -0
  241. package/server/engine/twin-engine.ts +1318 -186
  242. package/server/index.ts +36 -3
  243. package/server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.ts +54 -0
  244. package/server/migrations/1786200000000-CarryLiveFeedCursor.ts +53 -0
  245. package/server/migrations/index.ts +7 -1
  246. package/server/service/index.ts +11 -0
  247. package/server/service/reference/control-routing.ts +62 -0
  248. package/server/service/reference/discovery-result.ts +1 -1
  249. package/server/service/reference/index.ts +7 -1
  250. package/server/service/reference/reference-adapter.ts +275 -5
  251. package/server/service/reference/reference-assessment.ts +215 -0
  252. package/server/service/reference/reference-live.ts +126 -14
  253. package/server/service/reference/reference-master.ts +64 -4
  254. package/server/service/reference/reference-probe.ts +264 -0
  255. package/server/service/reference/reference-progress-subscription.ts +73 -0
  256. package/server/service/reference/reference-progress.ts +95 -0
  257. package/server/service/reference/reference-resolver.ts +246 -19
  258. package/server/service/reference/twin-reference.ts +39 -1
  259. package/server/service/twin-audit/command-audit.ts +34 -1
  260. package/server/service/twin-audit/twin-audit-event.ts +20 -0
  261. package/server/service/twin-control/twin-control-mutation.ts +78 -30
  262. package/server/service/twin-event/journal-count.ts +53 -0
  263. package/server/service/twin-event/twin-event-keys.ts +16 -1
  264. package/server/service/twin-event/twin-event-type.ts +26 -2
  265. package/server/service/twin-event/twin-event.ts +7 -0
  266. package/server/service/twin-forecast/twin-forecast-query.ts +2 -1
  267. package/server/service/twin-ingest-window/index.ts +7 -0
  268. package/server/service/twin-ingest-window/twin-ingest-window-query.ts +125 -0
  269. package/server/service/twin-ingest-window/twin-ingest-window-writer.ts +57 -0
  270. package/server/service/twin-ingest-window/twin-ingest-window.ts +113 -0
  271. package/server/service/twin-instance/twin-instance.ts +16 -10
  272. package/server/service/twin-journal/twin-journal-query.ts +59 -5
  273. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +79 -4
  274. package/server/service/twin-metrics/twin-metrics-query.ts +81 -3
  275. package/server/service/twin-model/axis-journal-evidence.ts +57 -0
  276. package/server/service/twin-model/epcis-coverage.ts +2 -8
  277. package/server/service/twin-model/iec61850-coverage.ts +2 -8
  278. package/server/service/twin-model/isa95-coverage.ts +22 -9
  279. package/server/service/twin-model/item-ref.ts +80 -0
  280. package/server/service/twin-model/name-index.ts +94 -0
  281. package/server/service/twin-model/project-structure.ts +1 -1
  282. package/server/service/twin-model/status-tally.ts +37 -0
  283. package/server/service/twin-model/twin-lineage-query.ts +38 -9
  284. package/server/service/twin-model/twin-model-item-query.ts +26 -27
  285. package/server/service/twin-model/twin-model-query.ts +156 -5
  286. package/server/service/twin-model/twin-model-tree-query.ts +7 -0
  287. package/server/service/twin-readiness/index.ts +3 -0
  288. package/server/service/twin-readiness/twin-readiness-query.ts +96 -0
  289. package/server/service/twin-space/twin-space-resolver.ts +5 -4
  290. package/shared/entity-delta.ts +31 -7
  291. package/shared/kpi-broadcast.ts +1 -1
  292. package/shared/touched-items.ts +73 -0
  293. package/test/adopt-structure-live.test.ts +7 -7
  294. package/test/axis-journal-evidence.test.ts +71 -0
  295. package/test/axis-read.test.ts +101 -1
  296. package/test/boot-resume.test.ts +93 -27
  297. package/test/broadcast-cost-baseline.test.ts +200 -0
  298. package/test/broadcast-period.test.ts +58 -0
  299. package/test/canonical-ingest-vocabularies.test.ts +36 -1
  300. package/test/canonical-quantity-door.test.ts +61 -0
  301. package/test/command-routing.test.ts +1 -1
  302. package/test/control-capability.test.ts +103 -0
  303. package/test/declared-location-types.test.ts +89 -0
  304. package/test/declared-stimulus.test.ts +88 -0
  305. package/test/discovery-result.test.ts +1 -1
  306. package/test/duration-estimators.test.ts +1 -1
  307. package/test/entity-delta.test.ts +2 -2
  308. package/test/ingest-bench.test.ts +3 -3
  309. package/test/ingest-health-engine.test.ts +248 -0
  310. package/test/ingest-health-wiring.test.ts +119 -0
  311. package/test/ingest-health.test.ts +306 -0
  312. package/test/ingest-history.test.ts +247 -0
  313. package/test/ingest-running-guard.test.ts +5 -5
  314. package/test/instance-cache-lifecycle.test.ts +1 -1
  315. package/test/integration-probes.test.ts +103 -0
  316. package/test/integration-runner.test.ts +95 -0
  317. package/test/item-ref.test.ts +78 -0
  318. package/test/journal-read-discipline.test.ts +177 -0
  319. package/test/journal-retention.test.ts +133 -0
  320. package/test/journal-sort-axis.test.ts +142 -0
  321. package/test/journal-write-door.test.ts +110 -0
  322. package/test/journal-write-trend.test.ts +115 -0
  323. package/test/kernel-kind-guard.test.ts +4 -4
  324. package/test/kpi-baseline-db.test.ts +1 -1
  325. package/test/kpi-fold.test.ts +90 -3
  326. package/test/kpi-query-bench.test.ts +3 -3
  327. package/test/lineage-survives-restart.test.ts +20 -4
  328. package/test/live-cursor-wiring.test.ts +87 -0
  329. package/test/live-feed-registry.test.ts +15 -7
  330. package/test/live-kernel-facts.test.ts +7 -7
  331. package/test/live-mirror-parity.test.ts +6 -0
  332. package/test/load-meter.test.ts +29 -15
  333. package/test/local-declarations.test.ts +109 -2
  334. package/test/log-stamp.test.ts +59 -0
  335. package/test/loop-lag.test.ts +82 -0
  336. package/test/measured-yield.test.ts +90 -0
  337. package/test/model-gap.test.ts +153 -0
  338. package/test/oee-accumulator.test.ts +4 -0
  339. package/test/operation-basis.test.ts +28 -1
  340. package/test/operational-vocabulary.test.ts +108 -0
  341. package/test/operations-capability-db.test.ts +4 -4
  342. package/test/project-structure-db.test.ts +1 -1
  343. package/test/projection-reaches-screen.test.ts +1 -1
  344. package/test/property-effects.test.ts +28 -0
  345. package/test/read-failure-visible.test.ts +145 -0
  346. package/test/reference-grounding.test.ts +70 -0
  347. package/test/resolve-ts-siblings.mjs +52 -0
  348. package/test/restart-policy.test.ts +111 -0
  349. package/test/resync-origin-site.test.ts +7 -1
  350. package/test/revision-axis.test.ts +93 -0
  351. package/test/runtime-key.test.ts +2 -2
  352. package/test/scale-twin-bench.test.ts +2 -2
  353. package/test/source-outcome-audit.test.ts +104 -0
  354. package/test/spec-coverage.test.ts +1 -1
  355. package/test/stage-path.test.ts +95 -0
  356. package/test/standard-coverage.test.ts +15 -3
  357. package/test/status-tally.test.ts +55 -0
  358. package/test/structure-revision-db.test.ts +31 -27
  359. package/test/tenant-registry-db.test.ts +3 -3
  360. package/test/time-range.test.ts +152 -0
  361. package/test/touched-items.test.ts +61 -0
  362. package/test/twin-event-keys.test.ts +10 -2
  363. package/test/twin-model-item-db.test.ts +5 -3
  364. package/test/twin-model-tree-db.test.ts +7 -7
  365. package/test/twin-origin-resync.test.ts +4 -4
  366. package/test/warm-start-seam.test.ts +4 -0
  367. package/test/yield-loop.test.ts +199 -0
  368. package/tsconfig.shared.tsbuildinfo +1 -1
  369. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,103 @@
1
+ /*
2
+ * **구동은 선언된 능력이다** — 그리고 원본을 지난다 (ADR-0029 §8, 2026-08-19).
3
+ *
4
+ * ── 무엇이 뚫려 있었나 ──────────────────────────────────────────────────────
5
+ * `controlTwinScenario` 가 트윈의 커널 런타임(`runtime.scenario`)을 **직접** 잡았다. 시뮬만 할 수 있는 일
6
+ * (자극·배속·정지)이 계약 밖의 지름길로 들어온 셈이고, 실 시스템에는 그 자리가 아예 없었다. ADR-0029 의
7
+ * 판정 문장(*"실물이 같은 계약으로 할 수 있는가"*)을 그대로 대면 그 통제면은 영원히 「규약의 결손」이 된다.
8
+ *
9
+ * 뒤집는다: 통제는 **선언된 어댑터 능력**이다. 선언하지 않은 원본에는 문이 열리지 않고, 그 사실이 결손이
10
+ * 아니라 사실이다(실 WMS 는 정지되지 않는다).
11
+ *
12
+ * 이 시험이 지키는 것:
13
+ * ① 선언과 구현이 **함께** 있어야 능력이다 — 선언만 있으면 화면이 거짓말을 하고, 구현만 있으면 닫아 둔
14
+ * 문을 여는 것이다(선언이 권위다).
15
+ * ② 능력 없는 원본은 **사유 코드로** 거절한다(조용한 실패 금지).
16
+ * ③ 없는 기능을 있는 것처럼 답하지 않는다(내장 시뮬은 아직 재씨앗을 못 한다).
17
+ */
18
+ import { test } from 'node:test'
19
+ import assert from 'node:assert/strict'
20
+ import { readFileSync } from 'node:fs'
21
+ import { fileURLToPath } from 'node:url'
22
+
23
+ import { capabilitiesOf, type ReferenceAdapter } from '../server/service/reference/reference-adapter.ts'
24
+
25
+ const base: ReferenceAdapter = {
26
+ type: 'probe',
27
+ async testConnection() {
28
+ return { ok: true }
29
+ },
30
+ async discoverSites() {
31
+ return []
32
+ },
33
+ async fetchMaster() {
34
+ return { source: 'p', system: 'wms', space: { spaceId: 'p-site' }, locations: [] } as any
35
+ }
36
+ }
37
+
38
+ test('선언도 구현도 없으면 아무 능력이 없다 — 구조만 주는 원본이 정상이다', () => {
39
+ assert.deepEqual(capabilitiesOf(base), [])
40
+ })
41
+
42
+ test('선언만 있으면 능력이 아니다 — 화면이 「할 수 있다」고 말한 뒤 아무 일도 없게 두지 않는다', () => {
43
+ assert.deepEqual(capabilitiesOf({ ...base, capabilities: ['control'] }), [])
44
+ assert.deepEqual(capabilitiesOf({ ...base, capabilities: ['live'] }), [])
45
+ })
46
+
47
+ test('구현만 있으면 능력이 아니다 — 선언이 권위다(닫아 둔 문을 열지 않는다)', () => {
48
+ assert.deepEqual(capabilitiesOf({ ...base, control: async () => ({ ok: true }) }), [])
49
+ assert.deepEqual(capabilitiesOf({ ...base, openLiveFeed: () => () => {} }), [])
50
+ })
51
+
52
+ test('선언과 구현이 함께 있으면 그 능력이다', () => {
53
+ assert.deepEqual(capabilitiesOf({ ...base, capabilities: ['control'], control: async () => ({ ok: true }) }), ['control'])
54
+ assert.deepEqual(
55
+ capabilitiesOf({
56
+ ...base,
57
+ capabilities: ['live', 'control'],
58
+ openLiveFeed: () => () => {},
59
+ control: async () => ({ ok: true })
60
+ }),
61
+ ['live', 'control']
62
+ )
63
+ })
64
+
65
+ test('어댑터가 없으면 빈손이다 — 지어내지 않는다', () => {
66
+ assert.deepEqual(capabilitiesOf(undefined), [])
67
+ })
68
+
69
+ /*
70
+ * ── 길이 하나뿐인지 (소스로 지킨다) ────────────────────────────────────────
71
+ * 지름길이 남아 있으면 이 결정은 문서상의 것이 된다. 그래서 ① 구동 문이 라우터를 지나고 ② 라우터가 능력을
72
+ * 보고 ③ 트윈 런타임을 직접 잡는 옛 경로가 사라졌는지 본다.
73
+ */
74
+ test('구동 문이 원본을 지난다 — 트윈 런타임을 직접 잡지 않는다', () => {
75
+ const src = readFileSync(fileURLToPath(new URL('../server/service/twin-control/twin-control-mutation.ts', import.meta.url)), 'utf-8')
76
+ const code = src
77
+ .split('\n')
78
+ .filter(line => !/^\s*(\*|\/\/|\/\*)/.test(line))
79
+ .join('\n')
80
+ assert.match(code, /routeControl\(domainId, instanceId/, '구동은 라우터를 지난다')
81
+ assert.equal(/runtime\.scenario\./.test(code), false, '트윈의 시나리오 엔진을 직접 잡는 지름길이 없다')
82
+ assert.equal(/controlTwinScenario/.test(code), false, '옛 문이 남아 있지 않다')
83
+ assert.match(code, /twinSourceCapability/, '화면이 먼저 물을 자리가 있다')
84
+ })
85
+
86
+ test('라우터가 선언을 본다 — 없으면 사유 코드로 거절한다', () => {
87
+ const src = readFileSync(fileURLToPath(new URL('../server/service/reference/control-routing.ts', import.meta.url)), 'utf-8')
88
+ assert.match(src, /capabilitiesOf\(adapter\)\.includes\('control'\)/)
89
+ assert.match(src, /'not-declared'/)
90
+ assert.match(src, /'no-source'/, '원본을 모르는 트윈은 구동 대상이 아니다')
91
+ })
92
+
93
+ test('내장 시뮬은 자기가 못 하는 것을 못 한다고 말한다 — 재씨앗은 아직 없다', () => {
94
+ const src = readFileSync(
95
+ fileURLToPath(new URL('../../operato-twin/server/board/virtual-adapter.ts', import.meta.url)),
96
+ 'utf-8'
97
+ )
98
+ /* §4 이후 이 원본은 둘을 선언한다(`live`·`control`) — 순서가 아니라 **있음**을 본다. */
99
+ assert.match(src, /capabilities: \[[^\]]*'control'[^\]]*\]/, '구동 능력을 선언한다')
100
+ assert.match(src, /capabilities: \[[^\]]*'live'[^\]]*\]/, '피드 능력도 선언한다(§4)')
101
+ assert.match(src, /cannot re-seed itself yet/, '없는 기능을 있는 것처럼 답하지 않는다')
102
+ assert.match(src, /not-running/, '아직 서지 않은 트윈에는 자극을 지어 싣지 않는다')
103
+ })
@@ -0,0 +1,89 @@
1
+ /*
2
+ * ★ 자리 타입을 **누구에게 묻는가** — 원본이 선언했으면 그 선언이 정본이다.
3
+ *
4
+ * ── 무엇이 틀렸나 (2026-08-20) ──────────────────────────────────────────────
5
+ * 호스트는 「이 자리 타입을 아는가」를 **내장 카탈로그**에게만 물었다(`DOMAIN_CATALOG[system]`).
6
+ * 그런데 원본은 자기 `DomainDefinition.locationTypes` 를 함께 보낼 수 있고, 그러면 **그 선언이
7
+ * 정본**이다 — 카탈로그는 우리가 아는 도메인들의 기본 어휘일 뿐이다.
8
+ *
9
+ * 상대가 틀렸으니 결과가 이랬다: 자기 어휘를 선언한 원본(예: F&B 라인 — 배합·가열·세척·금속검출·
10
+ * 소분·포장)은 **정상적인 자리마다 경고를 하나씩** 받는다. 경고가 소음이 되면 사람은 그것을 읽지 않게
11
+ * 되고, 그때 **진짜 경고도 함께 죽는다.**
12
+ *
13
+ * ── 지키는 것 ───────────────────────────────────────────────────────────────
14
+ * ① 선언이 있으면 선언에 대조한다 ② 선언이 없으면 카탈로그로 떨어진다 ③ 선언을 **못 읽은 것**과
15
+ * 「아무것도 선언하지 않은 것」을 가른다(못 읽었으면 카탈로그로 떨어진다 — 빈 목록으로 읽으면 모든
16
+ * 자리가 경고를 받는다) ④ 아무 데도 없는 타입은 여전히 경고한다.
17
+ */
18
+ import { test } from 'node:test'
19
+ import assert from 'node:assert/strict'
20
+ import { createRequire } from 'node:module'
21
+
22
+ const req = createRequire(import.meta.url)
23
+ const { masterToTwin, declaredLocationTypes } = req('../dist-server/service/reference/reference-master.js')
24
+
25
+ /** F&B 라인 — 내장 MES 카탈로그(트레일러 제조)에는 없는 어휘. */
26
+ const FNB = ['mixing', 'cooking', 'washing', 'metal-detect', 'portioning', 'packing']
27
+
28
+ const CATALOG = { mes: { locationTypes: ['raw-store', 'cut-station', 'fg-store'] } }
29
+
30
+ const master = (types: string[], productionSpec?: unknown) => ({
31
+ source: 'chef-1',
32
+ system: 'mes',
33
+ siteName: '식품 공장',
34
+ /* `masterToTwin` 이 공간에서 spaceId 를 꺼낸다 — 최소 모양을 준다. */
35
+ space: { spaceId: 'chef-site', width: 60, depth: 30, unit: 'm' },
36
+ locations: types.map((t, i) => ({ id: `loc-${i}`, type: t, role: 'location' as const })),
37
+ equipment: [],
38
+ ...(productionSpec ? { productionSpec } : {})
39
+ })
40
+
41
+ const specWith = (keys: string[]) => ({ definition: { locationTypes: keys.map(k => ({ key: k, label: k })) } })
42
+
43
+ const unknownWarnings = (r: any) => (r.warnings ?? []).filter((w: any) => w.code === 'unknown-location-type')
44
+
45
+ test('원본이 선언했으면 그 선언에 대조한다 — 정상적인 자리에 경고를 주지 않는다', () => {
46
+ const r = masterToTwin(master(FNB, specWith(FNB)), CATALOG)
47
+
48
+ assert.deepEqual(unknownWarnings(r), [], 'F&B 여섯 자리 모두 선언돼 있다')
49
+ })
50
+
51
+ test('선언이 없으면 카탈로그로 떨어진다 — 예전 거동 그대로', () => {
52
+ const r = masterToTwin(master(['raw-store', 'mixing']), CATALOG)
53
+ const w = unknownWarnings(r)
54
+
55
+ assert.equal(w.length, 1, '카탈로그에 없는 하나만')
56
+ assert.equal(w[0].params.type, 'mixing')
57
+ })
58
+
59
+ test('선언에도 카탈로그에도 없으면 경고한다 — 진짜 경고는 죽이지 않는다', () => {
60
+ const r = masterToTwin(master([...FNB, 'ghost-station'], specWith(FNB)), CATALOG)
61
+ const w = unknownWarnings(r)
62
+
63
+ assert.equal(w.length, 1)
64
+ assert.equal(w[0].params.type, 'ghost-station')
65
+ })
66
+
67
+ test('선언을 못 읽으면 카탈로그로 떨어진다 — 빈 목록으로 읽지 않는다', () => {
68
+ /*
69
+ * 「없는 것」과 「못 읽은 것」은 다르다. 못 읽은 것을 빈 목록으로 읽으면 **모든 자리가 경고**를 받고,
70
+ * 그것이 바로 이 시험이 막으려는 소음이다.
71
+ */
72
+ for (const broken of [{}, { definition: {} }, { definition: { locationTypes: [] } }, { definition: { locationTypes: 'nope' } }]) {
73
+ assert.equal(declaredLocationTypes(broken), undefined, `못 읽은 모양: ${JSON.stringify(broken)}`)
74
+ }
75
+
76
+ const r = masterToTwin(master(['raw-store', 'mixing'], { definition: { locationTypes: [] } }), CATALOG)
77
+ assert.equal(unknownWarnings(r).length, 1, '카탈로그 기준으로 하나만 경고한다')
78
+ })
79
+
80
+ test('선언 목록의 모양을 너그럽게 읽는다 — 문자열 목록도 받는다', () => {
81
+ assert.deepEqual(declaredLocationTypes({ definition: { locationTypes: ['mixing', 'packing'] } }), ['mixing', 'packing'])
82
+ assert.deepEqual(declaredLocationTypes({ definition: { locationTypes: [{ key: 'mixing' }, { nope: 1 }] } }), ['mixing'])
83
+ })
84
+
85
+ test('카탈로그도 선언도 없으면 아무 경고도 하지 않는다 — 모르는 것을 문제로 만들지 않는다', () => {
86
+ const r = masterToTwin(master(FNB), undefined)
87
+
88
+ assert.deepEqual(unknownWarnings(r), [], '견줄 상대가 없으면 판단하지 않는다')
89
+ })
@@ -0,0 +1,88 @@
1
+ /*
2
+ * 자극은 **원본이 선언한다** — 그리고 재기동에도 살아 있다.
3
+ *
4
+ * ── 무엇이 났나 (2026-08-19) ────────────────────────────────────────────────
5
+ * 데모의 시나리오는 시드 코드가 **메모리에만** 실었다(`runtime.scenario.load`). 그래서 트윈을 재기동하면
6
+ * 자극이 사라지고 구조만 서 있는 트윈이 남았다 — 작업 0·오더 0. 「살아 있는 데모」가 첫 재기동까지만
7
+ * 살았고, 오늘 서버를 여러 번 띄우는 동안 데모 트윈들이 실제로 그 상태였다.
8
+ *
9
+ * ── 집을 트윈이 아니라 원본으로 (ADR-0029 · plans/simulator-as-source.md §4) ─
10
+ * 무엇이 들어오고 무슨 주문이 나는지는 **시뮬레이터(원본)가 정하는 사실**이고 트윈은 반영한다. 트윈에
11
+ * 축을 만들면 ADR-0029 가 나중에 걷어내야 하고, 원본(`TwinReference.connectionConfig`)에 두면 그 계획의
12
+ * 첫 조각(`openLiveFeed`)이 같은 선언을 그대로 읽는다.
13
+ *
14
+ * 이 시험이 지키는 것:
15
+ * ① 선언이 없으면 조용히 지난다(정적 구조만 반영하는 트윈이 정상이다).
16
+ * ② **미러에는 싣지 않는다** — 외부 실물이 진실인 트윈에 도착을 지어 주면 그것은 현실이 아니다.
17
+ * ③ 검사를 통과하지 못한 선언은 **태우지 않고** 이유를 남긴다(잘못된 선언 하나가 서버를 내린 이력이 있다).
18
+ * ④ 검사기가 없으면 싣지 않는다 — 검사할 수 없는 선언을 태우는 것이 가장 비싼 선택이다.
19
+ * ⑤ 선언을 얹을 때 **옆 설정을 지우지 않는다**(접속 정보가 같은 자리에 산다).
20
+ */
21
+ import { test } from 'node:test'
22
+ import assert from 'node:assert/strict'
23
+ import { readFileSync } from 'node:fs'
24
+ import { fileURLToPath } from 'node:url'
25
+
26
+ import { planStimulus, withStimulus } from '../server/engine/declared-stimulus.ts'
27
+
28
+ const scenario = { seed: 1, generators: [{ kind: 'inbound-arrival', rate: { distribution: 'constant', meanPerHour: 60 }, content: {} }] }
29
+ const ok = () => ({ ok: true as const })
30
+ const bad = () => ({ ok: false as const, errorCode: 'scenario-sku-mix-required' })
31
+ const sim = { hasScenarioEngine: true, mode: 'sim' as const }
32
+
33
+ test('선언이 없으면 조용히 지난다 — 구조만 반영하는 트윈이 정상이다', () => {
34
+ assert.deepEqual(planStimulus(undefined, sim, ok), { action: 'skip', reason: 'none' })
35
+ assert.deepEqual(planStimulus({ endpoint: 'x' }, sim, ok), { action: 'skip', reason: 'none' })
36
+ /* 시나리오 자리에 객체가 아닌 것이 오면 선언이 아니다(문자열을 시나리오로 읽지 않는다). */
37
+ assert.deepEqual(planStimulus({ scenario: 'daily' }, sim, ok), { action: 'skip', reason: 'none' })
38
+ })
39
+
40
+ test('관측으로 도는 트윈에는 싣지 않는다 — 현실을 지어내지 않는다', () => {
41
+ const plan = planStimulus({ scenario }, { hasScenarioEngine: true, mode: 'live' }, ok)
42
+ assert.deepEqual(plan, { action: 'skip', reason: 'observed' })
43
+ })
44
+
45
+ test('시나리오 엔진이 없으면 싣지 않는다 — 조용히 아무 일도 없게 두지 않는다', () => {
46
+ const plan = planStimulus({ scenario }, { hasScenarioEngine: false, mode: 'sim' }, ok)
47
+ assert.deepEqual(plan, { action: 'skip', reason: 'no-engine' })
48
+ })
49
+
50
+ test('검사를 통과하지 못한 선언은 태우지 않는다 — 사유를 든다', () => {
51
+ const plan = planStimulus({ scenario }, sim, bad)
52
+ assert.deepEqual(plan, { action: 'skip', reason: 'invalid', detail: 'scenario-sku-mix-required' })
53
+ })
54
+
55
+ test('검사기가 없으면 싣지 않는다 — 검사할 수 없는 선언이 가장 위험하다', () => {
56
+ const plan = planStimulus({ scenario }, sim, undefined)
57
+ assert.equal(plan.action, 'skip')
58
+ assert.equal((plan as any).reason, 'invalid')
59
+ })
60
+
61
+ test('통과한 선언은 그대로 싣는다 — 값을 손대지 않는다', () => {
62
+ const plan = planStimulus({ scenario }, sim, ok)
63
+ assert.deepEqual(plan, { action: 'load', scenario })
64
+ })
65
+
66
+ test('선언을 얹으면서 접속 정보를 지우지 않는다', () => {
67
+ const merged = withStimulus({ endpoint: 'https://sim', token: 't' }, scenario)
68
+ assert.deepEqual(merged, { endpoint: 'https://sim', token: 't', scenario })
69
+ /* 설정이 없던 원본에도 얹힌다(빈 객체에서 시작). */
70
+ assert.deepEqual(withStimulus(undefined, scenario), { scenario })
71
+ })
72
+
73
+ /*
74
+ * ── 배선 (소스로 지킨다) ────────────────────────────────────────────────────
75
+ * 판정은 순수 함수가 하지만, 그것을 **기동 때 부르지 않으면** 아무 일도 일어나지 않는다. 그리고 못 실은
76
+ * 이유는 말해야 한다 — 「선언했는데 안 실렸다」가 조용하면 사용자는 트윈이 고장 난 줄로 읽는다.
77
+ */
78
+ test('두 기동 경로에서 부른다 — 시뮬은 싣고, 미러는 그 사실을 말한다', () => {
79
+ const src = readFileSync(fileURLToPath(new URL('../server/engine/twin-engine.ts', import.meta.url)), 'utf-8')
80
+ const calls = src.match(/this\.installStimulus\(domainId, id, inst\)/g) ?? []
81
+ assert.equal(calls.length, 2, `기동 경로 둘에서 불러야 한다(발견 ${calls.length})`)
82
+ assert.match(src, /planStimulus\(config, \{ hasScenarioEngine/, '판정은 순수 함수에 맡긴다')
83
+ assert.match(src, /validateScenario/, '커널 검사기를 그대로 쓴다(규칙을 두 벌로 만들지 않는다)')
84
+ assert.match(src, /a stimulus is declared on its source but was not loaded/, '못 실은 이유를 말한다')
85
+ /* 선언을 적는 문도 있어야 한다 — 읽기만 있으면 아무도 선언할 수 없다. */
86
+ assert.match(src, /static async declareStimulus/)
87
+ assert.match(src, /no source reference — a stimulus has nowhere to be declared/, '원본이 없으면 지어 만들지 않는다')
88
+ })
@@ -65,7 +65,7 @@ test('타임아웃은 초 단위로 알린다 — 얼마를 기다렸는지 알
65
65
  assert.match(e.message, /15s/)
66
66
  })
67
67
 
68
- test('어댑터가 던진 것에서 사유를 뽑는다 — 형태를 가정하지 않는다', () => {
68
+ test('어댑터가 오류에서 사유를 뽑는다 — 형태를 가정하지 않는다', () => {
69
69
  assert.equal(detailOf(new Error('ECONNREFUSED')), 'ECONNREFUSED')
70
70
  assert.equal(detailOf('404 Not Found'), '404 Not Found')
71
71
  /* 무엇이 왔는지 모르면 그렇게 말한다 — 빈 문장으로 "이유 없음" 처럼 보이게 하지 않는다. */
@@ -123,7 +123,7 @@ test('p50 이 0 이면 쓰지 않는다 — 0 초 작업은 측정 실패다', (
123
123
  assert.deepEqual(r.learned, {})
124
124
  })
125
125
 
126
- test('그룹이 없으면 빈 결과 — 던지지 않는다', () => {
126
+ test('그룹이 없으면 빈 결과 — 오류를 내지 않는다', () => {
127
127
  assert.equal(buildMeasuredEstimator(undefined).estimator, undefined)
128
128
  assert.equal(buildMeasuredEstimator([]).estimator, undefined)
129
129
  })
@@ -118,7 +118,7 @@ test('물품 집약: 상한을 넘으면 잘린 사실을 밝힌다 — 조용
118
118
  assert.equal(agg.items.length, ITEMS_AGGREGATE_LIMIT)
119
119
  assert.equal(agg.total, ITEMS_AGGREGATE_LIMIT + 7)
120
120
  assert.equal(agg.truncated, 7)
121
- /* 개별 물품은 상한과 무관하게 전부 태그별로 나간다(시그니처 dedup 이 변한 것만 재방송한다). */
121
+ /* 개별 물품은 상한과 무관하게 전부 태그별로 나간다(시그니처 dedup 이 변한 것만 재브로드캐스팅한다). */
122
122
  assert.equal(d.filter(x => x.data?.entity === 'item').length, ITEMS_AGGREGATE_LIMIT + 7)
123
123
  })
124
124
 
@@ -234,7 +234,7 @@ test('수요 구간·피크·계약은 집약 태그로 나간다 — 개체의
234
234
  assert.ok(agg, '집약 태그가 있어야 한다')
235
235
  assert.equal(agg.data.contractKW, 4000)
236
236
  assert.equal(agg.data.peakSince.kW, 4200)
237
- /* 하루치를 매 방송에 실으면 채널이 그것으로 찬다 — 최근 것만 내고 전체 수를 함께 낸다. */
237
+ /* 하루치를 매 브로드캐스팅에 실으면 채널이 그것으로 찬다 — 최근 것만 내고 전체 수를 함께 낸다. */
238
238
  assert.equal(agg.data.recent.length, 8)
239
239
  assert.equal(agg.data.closedTotal, 137, '자른 사실을 조용히 두지 않는다')
240
240
  })
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  * 커널 인제스트 인터페이스 부하 실측(가정 아닌 측정) — Face1/커널 동기 인터페이스의 처리량 특성.
3
- * apply(이벤트 흡수) · snapshot(전체 투영) · buildEntityDeltas(방송 payload, 't1') 의 비용을 규모별로 잰다.
3
+ * apply(이벤트 흡수) · snapshot(전체 투영) · buildEntityDeltas(브로드캐스팅 payload, 't1') 의 비용을 규모별로 잰다.
4
4
  * 결론은 design/integration/ingest-scale.md §커널 인터페이스에 스펙으로 반영.
5
5
  * (순수 커널+호스트 매핑, shell 무관 → node:test.)
6
6
  */
@@ -56,7 +56,7 @@ test('bench: buildEntityDeltas 비용 vs 엔티티 수 (전 엔티티 매번 방
56
56
  }
57
57
  })
58
58
 
59
- test('bench: 방송 1회 총비용(snapshot + buildEntityDeltas) vs 상태 규모', () => {
59
+ test('bench: 브로드캐스팅 1회 총비용(snapshot + buildEntityDeltas) vs 상태 규모', () => {
60
60
  for (const [locations, items] of [[200, 5000], [1000, 50000], [2000, 100000]]) {
61
61
  const proj = new StateProjector(model(locations))
62
62
  for (let i = 0; i < items; i++) proj.apply(envelope(`urn:epc:id:sscc:0614141.${i}`, `bin-${i % locations}`))
@@ -64,6 +64,6 @@ test('bench: 방송 1회 총비용(snapshot + buildEntityDeltas) vs 상태 규
64
64
  const t0 = performance.now()
65
65
  for (let k = 0; k < iters; k++) buildEntityDeltas(proj.snapshot(), 't1')
66
66
  const per = (performance.now() - t0) / iters
67
- console.log(` [방송1회] ${locations.toLocaleString()} locations / ${items.toLocaleString()} items: ${per.toFixed(3)}ms → 최대 ${Math.round(1000 / per).toLocaleString()} 방송/s`)
67
+ console.log(` [브로드캐스팅1회] ${locations.toLocaleString()} locations / ${items.toLocaleString()} items: ${per.toFixed(3)}ms → 최대 ${Math.round(1000 / per).toLocaleString()} 브로드캐스팅/s`)
68
68
  }
69
69
  })
@@ -0,0 +1,248 @@
1
+ /*
2
+ * ★ 동기화 판정이 **실제 트윈 위에서** 옳게 나온다 — 엔진 이음새.
3
+ *
4
+ * 순수 모듈 시험(`ingest-health.test.ts`)은 「이런 장부가 주어지면 이런 판정」을 지킨다. 그런데 화면이
5
+ * 실제로 받는 판정은 그 앞에 한 층이 더 있다: **피드 사실을 어디서 얻는가**(`liveFeedStateOf` — 이 트윈이
6
+ * 미러인가 시뮬인가, 지금 도는가) 그리고 **장부를 어느 트윈의 것으로 찾는가**(도메인+인스턴스 키).
7
+ *
8
+ * 그 층이 틀리면 순수 시험은 전부 초록인 채로 화면만 거짓말한다. 실제로 두 가지가 조용히 틀릴 수 있다:
9
+ * ① 시뮬 트윈이 「연결 안 됨」으로 보인다 — 거짓 경보이고, 거짓 경보는 참 경보를 죽인다
10
+ * ② 남의 도메인 장부를 집는다 — 키를 도메인 없이 만들면 그렇게 된다
11
+ *
12
+ * 그래서 실 DataSource 로 트윈을 세우고, 미러 하나·시뮬 하나로 판정 여섯을 이 자리에서 확인한다.
13
+ * 목 HTTP 게이트웨이는 쓰지 않는다(그것은 데모 환경변수에 딸린 것이고, 여기서 볼 것은 장부와 판정이다).
14
+ */
15
+ import { test, before, after } from 'node:test'
16
+ import assert from 'node:assert/strict'
17
+ import { createRequire } from 'node:module'
18
+ import type { DataSource } from 'typeorm'
19
+
20
+ const req = createRequire(import.meta.url)
21
+ const { NamingStrategy, addDataSource, entities: shellEntities, Domain } = req('@things-factory/shell')
22
+ const { entities: authEntities } = req('@things-factory/auth-base')
23
+ const { entities: twinEntities } = req('../dist-server/service/index.js')
24
+ const { TwinEngine } = req('../dist-server/engine/index.js')
25
+ const { INGEST_WINDOW_MS, STALE_AFTER_MS } = req('../dist-server/engine/ingest-health.js')
26
+ const { registerLiveFeedProbe } = req('../dist-server/engine/live-feed-registry.js')
27
+ const { DataSource: TypeOrmDataSource } = req('typeorm')
28
+
29
+ /*
30
+ * 피드 사실을 이 시험이 쥔다.
31
+ *
32
+ * 실제 커넥터 피드는 원천에 붙어야 서고, 붙지 못하면 비동기로 떨어진다 — 그 타이밍에 판정이 흔들리면
33
+ * 이 시험은 **판정을 재는 게 아니라 커넥터를 재는 것**이 된다(실제로 그렇게 깨졌다: 세 번째 검사까지는
34
+ * 붙어 있다가 네 번째에서 떨어져 「연결 안 됨」이 나왔다). 그래서 `registerLiveFeedProbe` — 계층이 자기
35
+ * 장부를 등록하도록 만들어 둔 그 확장점 — 으로 시험의 장부를 하나 더 얹는다. 커넥터 거동은 커넥터
36
+ * 시험이 지킨다.
37
+ */
38
+ let feedUp = new Set<string>()
39
+
40
+ let ds: DataSource
41
+ let domainId: string
42
+ let otherDomainId: string
43
+
44
+ /*
45
+ * 시나리오마다 트윈을 따로 둔다.
46
+ *
47
+ * 장부는 트윈마다 하나이고 **창은 이어진다.** 트윈 하나로 여러 시나리오를 돌리면 앞 검사가 남긴 창에
48
+ * 뒤 검사의 건수가 섞여 「최근 창」이 엉뚱한 것을 가리킨다(실제로 그렇게 깨졌다: 200건을 기대한 자리에
49
+ * 50건이 나왔다). 트윈을 나누면 각 검사가 자기 창만 본다.
50
+ */
51
+ const MIRROR = 'sync-mirror' /* 붙었는데 아직 안 받음 → 창이 닫히면 동기화 중 */
52
+ const BLOCKED = 'sync-blocked' /* 거부가 문턱을 넘음 */
53
+ const STALE = 'sync-stale' /* 오래 끊김 */
54
+ const SIM = 'sync-sim'
55
+ const MIRRORS = [MIRROR, BLOCKED, STALE]
56
+
57
+ /**
58
+ * 이 검사가 쓰는 창의 시작점.
59
+ *
60
+ * 창 하나를 닫고도 **읽는 시점이 다음 창 안에** 있어야 한다. `1.4창` 전에서 시작해 `창+1` 에서 닫으면
61
+ * 읽을 때까지 `0.4창`밖에 지나지 않아 두 번째 창이 닫히지 않는다 — 정확히 `2창` 전으로 잡았을 때
62
+ * 밀리초 몇 개 때문에 한 창이 더 닫혀 깨졌다.
63
+ */
64
+ const windowStart = () => Date.now() - Math.floor(INGEST_WINDOW_MS * 1.4)
65
+
66
+ const master = (source: string): any => ({
67
+ source,
68
+ system: 'wms',
69
+ siteName: '동기화 검사 창고',
70
+ space: { width: 40, depth: 20, unit: 'm' },
71
+ locations: [
72
+ { id: 'dock-recv', type: 'dock', role: 'location', name: '입고 도크', capacity: 100 },
73
+ { id: 'bin-1', type: 'storage', role: 'location', name: '빈1', capacity: 20 }
74
+ ],
75
+ equipment: [{ id: 'fk-1', kind: 'forklift', homeLocationId: 'dock-recv' }]
76
+ })
77
+
78
+ /** 거부 하나 — 사유는 커널이 주는 모양을 그대로 흉내낸다(레코드 원문 포함). */
79
+ const reject = (why: string) => ({ errors: [why], record: { epc: 'urn:epc:id:sgtin:0614141.107346.2018', bizStep: '?' } })
80
+
81
+ before(async () => {
82
+ ds = new TypeOrmDataSource({
83
+ type: 'sqlite',
84
+ database: ':memory:',
85
+ synchronize: true,
86
+ dropSchema: true,
87
+ namingStrategy: new NamingStrategy(),
88
+ entities: [...shellEntities, ...authEntities, ...twinEntities],
89
+ logging: false
90
+ })
91
+ await ds.initialize()
92
+ addDataSource('default', ds)
93
+ registerLiveFeedProbe(() => [...feedUp])
94
+
95
+ const d = await ds.getRepository(Domain).save({ name: 'sync-test', subdomain: 'sync-test' } as any)
96
+ domainId = d.id
97
+ const o = await ds.getRepository(Domain).save({ name: 'sync-other', subdomain: 'sync-other' } as any)
98
+ otherDomainId = o.id
99
+
100
+ for (const id of [...MIRRORS, SIM]) await TwinEngine.ingestMaster(domainId, master(id), 'reset')
101
+ })
102
+
103
+ after(async () => {
104
+ for (const id of [...MIRRORS, SIM]) await TwinEngine.stop(domainId, id).catch(() => {})
105
+ await ds?.destroy()
106
+ })
107
+
108
+ /** 미러로 세우고 피드를 붙인 상태로 만든다 — 각 검사가 자기 트윈을 자기 손으로 세운다. */
109
+ async function bootMirror(instanceId: string): Promise<void> {
110
+ const rt = await TwinEngine.bootDeclared(domainId, instanceId, 'resync')
111
+ assert.equal(rt.mode, 'live', `${instanceId} 는 미러로 떠야 이 검사가 뜻이 있다`)
112
+ feedUp.add(instanceId)
113
+ }
114
+
115
+ test('시뮬 트윈은 「해당 없음」이다 — 물을 대상이 아닌 것에 경고색을 주지 않는다', async () => {
116
+ const rt = await TwinEngine.bootDeclared(domainId, SIM, 'reset')
117
+ assert.equal(rt.mode, 'sim', '시뮬로 떠야 이 검사가 뜻이 있다')
118
+
119
+ assert.equal(TwinEngine.ingestHealthOf(domainId, SIM).verdict, 'not-applicable')
120
+ })
121
+
122
+ test('미러가 붙었지만 아직 아무것도 안 받았으면 「모름」이다 — 정상도 비정상도 아니다', async () => {
123
+ await bootMirror(MIRROR)
124
+
125
+ const v = TwinEngine.ingestHealthOf(domainId, MIRROR)
126
+ assert.equal(v.verdict, 'unknown')
127
+ assert.equal(v.lastAt, null, '받은 적이 없다 — 0 이 아니다')
128
+ assert.equal(v.total.acceptedRatio, null, '비율이 없다 — 1 이 아니다')
129
+ })
130
+
131
+ test('창이 닫히면 「동기화 중」이 되고, 통과와 거부를 함께 낸다', () => {
132
+ const t0 = windowStart()
133
+
134
+ TwinEngine.recordIngestResult(domainId, MIRROR, 200, [reject('unknown bizStep "?"')], t0)
135
+ /* 창을 닫는다 — 창은 **다음 유입이 있을 때** 닫힌다. */
136
+ TwinEngine.recordIngestResult(domainId, MIRROR, 50, [], t0 + INGEST_WINDOW_MS + 1)
137
+
138
+ const v = TwinEngine.ingestHealthOf(domainId, MIRROR)
139
+ assert.equal(v.verdict, 'syncing')
140
+ assert.equal(v.recent.offered, 200)
141
+ assert.equal(v.recent.rejected, 1)
142
+ assert.equal(v.recent.accepted, 199)
143
+ assert.equal(v.recent.acceptedRatio, 0.995)
144
+ assert.equal(v.total.offered, 250, '누적은 두 창을 합한다')
145
+ })
146
+
147
+ test('거부가 문턱을 넘으면 「매핑이 막고 있음」이고, 사유와 레코드 원문이 함께 나온다', async () => {
148
+ await bootMirror(BLOCKED)
149
+ const t0 = windowStart()
150
+
151
+ TwinEngine.recordIngestResult(
152
+ domainId,
153
+ BLOCKED,
154
+ 100,
155
+ Array.from({ length: 40 }, () => reject('unknown location "A-99"')),
156
+ t0
157
+ )
158
+ TwinEngine.recordIngestResult(domainId, BLOCKED, 1, [], t0 + INGEST_WINDOW_MS + 1)
159
+
160
+ const v = TwinEngine.ingestHealthOf(domainId, BLOCKED)
161
+ assert.equal(v.verdict, 'mapping-blocked')
162
+ assert.equal(v.recent.acceptedRatio, 0.6, '100건 중 40건이 거부됐다')
163
+
164
+ const reason = v.reasons.find((r: any) => r.reason === 'unknown location "A-99"')
165
+ assert.ok(reason, '사유를 묶어서 낸다')
166
+ assert.equal(reason.count, 40)
167
+
168
+ /* 표본은 커널이 낸 사유를 그대로 들고 있다(`errors`) — 우리가 다시 쓰지 않는다. */
169
+ const sample = v.samples.find((s: any) => s.errors.includes('unknown location "A-99"'))
170
+ assert.ok(sample, '사유만으로는 원천이 무엇을 보냈는지 알 수 없다 — 원문이 있어야 고친다')
171
+ assert.match(sample.record, /urn:epc:id:sgtin/, '레코드 원문이 그대로 들어 있다')
172
+ })
173
+
174
+ test('오래 끊긴 미러는 「유입 없음」이다 — 건강했던 옛 창으로 초록을 내지 않는다', async () => {
175
+ /*
176
+ * 이것이 이 기능이 있는 이유다. 창은 다음 유입이 있을 때 닫히므로, 유입이 멈추면 「최근 창」은 멈추기
177
+ * 직전의 건강했던 창으로 남는다. 그 창만 보면 어제 죽은 피드가 오늘도 「동기화 중」이다.
178
+ */
179
+ await bootMirror(STALE)
180
+ const long = Date.now() - STALE_AFTER_MS * 5
181
+
182
+ TwinEngine.recordIngestResult(domainId, STALE, 1000, [], long)
183
+ TwinEngine.recordIngestResult(domainId, STALE, 1000, [], long + INGEST_WINDOW_MS + 1) // 건강한 창을 닫는다
184
+
185
+ const v = TwinEngine.ingestHealthOf(domainId, STALE)
186
+ assert.equal(v.verdict, 'no-traffic', '마지막 유입이 한 창보다 오래 전이다')
187
+ assert.equal(v.recent.acceptedRatio, 1, '(옛 창 자체는 건강했다 — 그래서 창만 보면 속는다)')
188
+ })
189
+
190
+ test('피드가 떨어진 미러는 「연결 안 됨」이다 — 「유입 없음」과 처방이 다르다', () => {
191
+ /* 「유입 없음」은 원천·망을 보라는 말이고, 「연결 안 됨」은 결선하라는 말이다. 섞으면 엉뚱한 곳을 본다. */
192
+ feedUp.delete(MIRROR)
193
+
194
+ assert.equal(TwinEngine.ingestHealthOf(domainId, MIRROR).verdict, 'not-connected')
195
+
196
+ feedUp.add(MIRROR)
197
+ })
198
+
199
+ test('멈춘 미러는 「해당 없음」이다 — 고치려고 멈춰 둔 트윈에 「연결 안 됨」이라 하지 않는다', async () => {
200
+ await TwinEngine.stop(domainId, MIRROR)
201
+
202
+ const v = TwinEngine.ingestHealthOf(domainId, MIRROR)
203
+ assert.equal(v.verdict, 'not-applicable')
204
+ assert.ok(v.total.offered > 0, '멈춰도 장부는 남는다 — 마지막에 무엇이 거부됐는지 볼 수 있어야 한다')
205
+ })
206
+
207
+ test('멈춘 트윈에 계속 들어오면 「버려지는 중」이라 말한다 — 실제로 겪은 거짓 안심', async () => {
208
+ /*
209
+ * 겪은 모습: 트윈이 멈췄는데 피드는 살아 레코드를 나르고, 커널은 조용히 버렸다. 화면은 「해당 없음 ·
210
+ * 150 통과 · 100%」였다. 여기서는 그 상황을 그대로 만들어 판정과 수치를 확인한다.
211
+ */
212
+ /* 사람이 트윈을 멈춘다 — 그런데 피드는 남는다. 그 조합이 사실을 잃게 만든다. */
213
+ await TwinEngine.stop(domainId, STALE)
214
+ assert.equal(TwinEngine.instances[`${domainId}:${STALE}`], undefined, '멈췄다')
215
+
216
+ /* 커널이 받지 않는다 — `ingestLive` 가 0 을 돌려준다. */
217
+ const applied = TwinEngine.ingestLive(domainId, STALE, [{ any: 'envelope' } as any])
218
+ assert.equal(applied, 0, '멈춘 트윈은 봉투를 받지 않는다')
219
+
220
+ TwinEngine.recordIngestResult(domainId, STALE, 10, [], Date.now(), 10)
221
+
222
+ const v = TwinEngine.ingestHealthOf(domainId, STALE)
223
+ assert.equal(v.verdict, 'twin-not-running', '「해당 없음」으로 덮지 않는다')
224
+ assert.equal(v.total.undelivered >= 10, true, '버려진 수를 센다')
225
+ })
226
+
227
+ test('장부는 도메인으로 갈린다 — 같은 instanceId 라도 남의 것을 집지 않는다', () => {
228
+ const v = TwinEngine.ingestHealthOf(otherDomainId, MIRROR)
229
+
230
+ assert.equal(v.total.offered, 0, '다른 도메인에는 이 트윈의 장부가 없다')
231
+ assert.equal(v.total.acceptedRatio, null)
232
+ assert.equal(v.samples.length, 0, '남의 레코드 원문이 새지 않는다')
233
+ })
234
+
235
+ test('목록 요약은 숫자만 낸다 — 원문을 스무 벌 실어 조회를 무겁게 하지 않는다', () => {
236
+ const brief = TwinEngine.ingestHealthBrief(domainId, [MIRROR, SIM])
237
+
238
+ assert.equal(brief.length, 2)
239
+ const m = brief.find((b: any) => b.instanceId === MIRROR)
240
+ assert.ok(m, '멈춘 트윈도 목록에 남는다')
241
+ assert.equal((m as any).samples, undefined, '표본은 목록에 없다')
242
+ assert.equal((m as any).reasons, undefined, '사유도 목록에 없다')
243
+ assert.ok(Array.isArray((m as any).trend), '스파크라인용 추이는 있다')
244
+
245
+ const s = brief.find((b: any) => b.instanceId === SIM)
246
+ assert.equal((s as any).verdict, 'not-applicable')
247
+ assert.equal((s as any).acceptedRatio, null, '시뮬은 비율이 없다 — 0 이 아니다')
248
+ })