@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
@@ -41,6 +41,10 @@ const GTIN = 'urn:epc:idpat:sgtin:0614141.012345.*'
41
41
  /** 저장 자리 `bins` 개 + 지게차 `forklifts` 대 — 구조를 다이얼한다. */
42
42
  const boardWith = (bins: number, forklifts: number): any => ({
43
43
  spaceId: 'struct-space',
44
+ /* 이 픽스처가 식별자를 만드는 이름공간 — **픽스처마다 다르게** 둔다(같은 접두 아래에서 서로 다른
45
+ 시험의 번호가 부딪치면 표준의 유일성 요구를 깬다, CBV 2.0 §8.2.4). 커널은 선언 없이 팔레트·문서를
46
+ 만들지 않는다(0.7.45 에서 날조 프리픽스를 지웠다). */
47
+ identity: { namespaces: ['https://twin.test/structure-revision'] },
44
48
  utcOffsetMinutes: 0,
45
49
  locations: [
46
50
  { id: 'dock-recv', type: 'dock', capacity: 100 },
@@ -95,9 +99,9 @@ beforeEach(async () => {
95
99
 
96
100
  test('같은 구조로 다시 프로비저닝하면 리비전이 늘지 않는다 — 부팅마다 늘면 이력이 잘게 쪼개진다', async () => {
97
101
  const model = boardWith(2, 1)
98
- await TwinEngine.provision(domainId, INSTANCE, 'wms', model)
99
- await TwinEngine.provision(domainId, INSTANCE, 'wms', model)
100
- await TwinEngine.provision(domainId, INSTANCE, 'wms', JSON.parse(JSON.stringify(model)))
102
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', model, 'reset')
103
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', model, 'reset')
104
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', JSON.parse(JSON.stringify(model)), 'reset')
101
105
 
102
106
  const revs = await ds.getRepository(TwinStructure).find({ where: { domain: { id: domainId }, instanceId: INSTANCE } })
103
107
  assert.equal(revs.length, 1)
@@ -105,12 +109,12 @@ test('같은 구조로 다시 프로비저닝하면 리비전이 늘지 않는
105
109
  })
106
110
 
107
111
  test('구조가 바뀌면 새 리비전이 생기고 **저널은 살아남는다**', async () => {
108
- await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(2, 1))
112
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(2, 1), 'reset')
109
113
  const first = realEvents(boardWith(2, 1), 300)
110
114
  assert.ok(first.length > 0, '이벤트가 있어야 "살아남는지" 를 물을 수 있다')
111
115
  await TwinEngine.persistBatch(domainId, INSTANCE, first, 0)
112
116
 
113
- await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(6, 3))
117
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(6, 3), 'reset')
114
118
 
115
119
  const kept = await ds.getRepository(TwinEvent).count({ where: { domain: { id: domainId }, instanceId: INSTANCE } })
116
120
  assert.equal(kept, first.length, '구조가 바뀌었다고 이력을 지우면 안 된다 — 그것이 이 작업의 전부다')
@@ -121,9 +125,9 @@ test('구조가 바뀌면 새 리비전이 생기고 **저널은 살아남는다
121
125
  })
122
126
 
123
127
  test('이벤트가 자기 구조 번호를 달고 저장된다 — 안 달면 나중에 어느 공장인지 알 길이 없다', async () => {
124
- await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(2, 1))
128
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(2, 1), 'reset')
125
129
  await TwinEngine.persistBatch(domainId, INSTANCE, realEvents(boardWith(2, 1), 200), 0)
126
- await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(6, 3))
130
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(6, 3), 'reset')
127
131
  const before = await ds.getRepository(TwinEvent).count({ where: { domain: { id: domainId }, instanceId: INSTANCE } })
128
132
  await TwinEngine.persistBatch(domainId, INSTANCE, realEvents(boardWith(6, 3), 200), before)
129
133
 
@@ -135,11 +139,11 @@ test('이벤트가 자기 구조 번호를 달고 저장된다 — 안 달면
135
139
  })
136
140
 
137
141
  test('★ 재생이 경계를 넘는다 — 앞선 사실은 남고, 새 구조의 설비가 들어온다', async () => {
138
- await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(2, 1))
142
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(2, 1), 'reset')
139
143
  const first = realEvents(boardWith(2, 1), 300)
140
144
  await TwinEngine.persistBatch(domainId, INSTANCE, first, 0)
141
145
 
142
- await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(6, 3))
146
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(6, 3), 'reset')
143
147
  await TwinEngine.persistBatch(domainId, INSTANCE, realEvents(boardWith(6, 3), 300), first.length)
144
148
 
145
149
  const state = await TwinEngine.recover(domainId, INSTANCE)
@@ -153,20 +157,20 @@ test('구조 번호가 없는 옛 행은 **가장 오래된 구조**로 접는
153
157
  * 컬럼이 생기기 전에 쓰인 행은 `structureRev` 가 비어 있다. 그것을 현재 보드로 접으면 그때 없던
154
158
  * 설비에서 일이 있었던 것처럼 보인다 — 이 규칙이 그 행들을 제자리에 붙인다.
155
159
  */
156
- await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(2, 1))
160
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(2, 1), 'reset')
157
161
  const legacy = realEvents(boardWith(2, 1), 300)
158
162
  await TwinEngine.persistBatch(domainId, INSTANCE, legacy, 0)
159
163
  /* 컬럼이 없던 시절을 흉내 낸다 — 번호를 지운다. */
160
164
  await ds.query(`UPDATE "${ds.getMetadata(TwinEvent).tableName}" SET structure_rev = NULL`)
161
165
 
162
- await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(6, 3))
166
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(6, 3), 'reset')
163
167
  const state = await TwinEngine.recover(domainId, INSTANCE)
164
168
  assert.ok(state.items.length > 0, '번호 없는 옛 행도 읽혀야 한다 — 조용히 버리지 않는다')
165
169
  assert.equal(state.equipment.length, 3, '마지막 구조가 자원의 진실이다')
166
170
  })
167
171
 
168
172
  test('리비전이 하나뿐이면 예전과 똑같이 접는다 — 구조 전환이 결과를 바꾸지 않는다', async () => {
169
- await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(3, 2))
173
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(3, 2), 'reset')
170
174
  const events = realEvents(boardWith(3, 2), 300)
171
175
  await TwinEngine.persistBatch(domainId, INSTANCE, events, 0)
172
176
 
@@ -177,7 +181,7 @@ test('리비전이 하나뿐이면 예전과 똑같이 접는다 — 구조 전
177
181
  })
178
182
 
179
183
  test('꺼진 트윈의 이력도 읽을 수 있다 — 저널은 트윈이 꺼져 있을 때 가장 필요하다', async () => {
180
- await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(2, 1))
184
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(2, 1), 'reset')
181
185
  assert.equal(TwinEngine.owns(domainId, INSTANCE), false, '떠 있지 않다')
182
186
  assert.equal(await TwinEngine.ownsRegistered(domainId, INSTANCE), true, '그래도 이 테넌트 것이다')
183
187
  assert.equal(await TwinEngine.ownsRegistered(domainId, 'someone-elses-twin'), false)
@@ -189,11 +193,11 @@ test('시간여행은 **그때의 공장**을 보여준다 — 과거에 최신
189
193
  * 그런데 그 규칙을 과거 조회에도 적용하면, 그 시점에 존재하지도 않던 설비가 화면에 선다 —
190
194
  * 이 작업이 막으려던 바로 그 거짓말이다. 규칙이 "지금" 에만 걸리는지 못박는다.
191
195
  */
192
- await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(2, 1))
196
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(2, 1), 'reset')
193
197
  const first = realEvents(boardWith(2, 1), 300)
194
198
  await TwinEngine.persistBatch(domainId, INSTANCE, first, 0)
195
199
 
196
- await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(6, 3))
200
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(6, 3), 'reset')
197
201
 
198
202
  const now = await TwinEngine.recover(domainId, INSTANCE)
199
203
  assert.equal(now.equipment.length, 3, '"지금" 은 늘린 설비를 보여준다')
@@ -204,10 +208,10 @@ test('시간여행은 **그때의 공장**을 보여준다 — 과거에 최신
204
208
  })
205
209
 
206
210
  test('구조 이력이 **무엇이 달라졌는지** 알린다 — 보드 두 개를 눈으로 비교할 수는 없다', async () => {
207
- await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(2, 1))
211
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(2, 1), 'reset')
208
212
  const first = realEvents(boardWith(2, 1), 200)
209
213
  await TwinEngine.persistBatch(domainId, INSTANCE, first, 0)
210
- await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(6, 3), '도장 능력 보강')
214
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(6, 3), 'reset', '도장 능력 보강')
211
215
  await TwinEngine.persistBatch(domainId, INSTANCE, realEvents(boardWith(6, 3), 100), first.length)
212
216
 
213
217
  const history = await TwinEngine.structureHistory(domainId, INSTANCE)
@@ -232,11 +236,11 @@ test('구조 이력이 **무엇이 달라졌는지** 알린다 — 보드 두
232
236
  })
233
237
 
234
238
  test('번호 없는 옛 행도 가장 오래된 리비전의 몫으로 센다 — 재생과 같은 규칙', async () => {
235
- await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(2, 1))
239
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(2, 1), 'reset')
236
240
  const legacy = realEvents(boardWith(2, 1), 200)
237
241
  await TwinEngine.persistBatch(domainId, INSTANCE, legacy, 0)
238
242
  await ds.query(`UPDATE "${ds.getMetadata(TwinEvent).tableName}" SET structure_rev = NULL`)
239
- await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(6, 3))
243
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(6, 3), 'reset')
240
244
 
241
245
  const history = await TwinEngine.structureHistory(domainId, INSTANCE)
242
246
  assert.equal(history[0].events, legacy.length, '재생이 rev 1 로 접는 행들을 세는 쪽만 빠뜨리면 두 화면이 다른 말을 한다')
@@ -250,11 +254,11 @@ test('지문 표기가 낡았을 뿐이면 리비전을 만들지 않는다 —
250
254
  * 그런 일이 없다.
251
255
  */
252
256
  const model = boardWith(2, 1)
253
- await TwinEngine.provision(domainId, INSTANCE, 'wms', model)
257
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', model, 'reset')
254
258
  const table = ds.getMetadata(TwinStructure).tableName
255
259
  await ds.query(`UPDATE "${table}" SET signature = 'old-style-signature-from-a-previous-algorithm'`)
256
260
 
257
- await TwinEngine.provision(domainId, INSTANCE, 'wms', model)
261
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', model, 'reset')
258
262
 
259
263
  const revs = await ds.getRepository(TwinStructure).find({ where: { domain: { id: domainId }, instanceId: INSTANCE } })
260
264
  assert.equal(revs.length, 1, '공장은 그대로인데 리비전이 늘면 이력이 거짓으로 부푼다')
@@ -269,11 +273,11 @@ test('지문 표기가 낡았을 뿐이면 리비전을 만들지 않는다 —
269
273
  * 행과 겹치는 문제였다. 그 겹침은 오류를 내지 않고 조용히 재생 순서를 뒤섞는다.
270
274
  */
271
275
  test('sim-world 로 다시 띄워도 저널이 살아남는다 — 선언이 지키는 약속', async () => {
272
- await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(2, 1))
276
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(2, 1), 'reset')
273
277
  const first = realEvents(boardWith(2, 1), 300)
274
278
  await TwinEngine.persistBatch(domainId, INSTANCE, first, 0)
275
279
 
276
- const rt = await TwinEngine.bootDeclared(domainId, INSTANCE, 'sim-world')
280
+ const rt = await TwinEngine.bootDeclared(domainId, INSTANCE, 'resume')
277
281
  try {
278
282
  const kept = await ds.getRepository(TwinEvent).count({ where: { domain: { id: domainId }, instanceId: INSTANCE } })
279
283
  assert.equal(kept, first.length, 'sim-world 는 이어지는 현실이다 — 지우면 선언이 거짓이 된다')
@@ -283,11 +287,11 @@ test('sim-world 로 다시 띄워도 저널이 살아남는다 — 선언이 지
283
287
  })
284
288
 
285
289
  test('★ 재기동 후 새 이벤트는 마지막 번호 뒤에 온다 — 겹치면 재생 순서가 뒤섞인다', async () => {
286
- await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(2, 1))
290
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(2, 1), 'reset')
287
291
  const first = realEvents(boardWith(2, 1), 300)
288
292
  await TwinEngine.persistBatch(domainId, INSTANCE, first, 0)
289
293
 
290
- const rt = await TwinEngine.bootDeclared(domainId, INSTANCE, 'sim-world')
294
+ const rt = await TwinEngine.bootDeclared(domainId, INSTANCE, 'resume')
291
295
  try {
292
296
  const kernel: any = rt.kernel
293
297
  assert.ok(kernel, '커널이 떠야 이어 세기를 물을 수 있다')
@@ -301,8 +305,8 @@ test('★ 재기동 후 새 이벤트는 마지막 번호 뒤에 온다 — 겹
301
305
  })
302
306
 
303
307
  test('저널이 비어 있으면 이어 셀 것이 없다 — 새로 시작하는 트윈에 영향 없음', async () => {
304
- await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(2, 1))
305
- const rt = await TwinEngine.bootDeclared(domainId, INSTANCE, 'sim-world')
308
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(2, 1), 'reset')
309
+ const rt = await TwinEngine.bootDeclared(domainId, INSTANCE, 'resume')
306
310
  try {
307
311
  assert.equal((rt.kernel as any).getSnapshot().revision, 0)
308
312
  } finally {
@@ -61,13 +61,13 @@ before(async () => {
61
61
  beta = b.id
62
62
 
63
63
  /* 두 테넌트가 **같은 id** 로 각자 프로비저닝한다 — DB 유일성이 `[domain, instanceId]` 라 허용된다. */
64
- await TwinEngine.provision(alpha, SHARED_ID, 'wms', model())
65
- await TwinEngine.provision(beta, SHARED_ID, 'wms', model())
64
+ await TwinEngine.provision(alpha, SHARED_ID, 'wms', model(), 'reset')
65
+ await TwinEngine.provision(beta, SHARED_ID, 'wms', model(), 'reset')
66
66
  })
67
67
 
68
68
  /**
69
69
  * **스펙마다 반드시 멈춘다.** 남기면 두 가지가 따라온다: 틱 타이머 때문에 러너가 끝나지 않고, 틱이
70
- * 뒤늦게 던지는 DB 쓰기가 teardown 이후에 도착해 `SQLITE_MISUSE` 로 스위트를 오염시킨다.
70
+ * 뒤늦게 오류를 내는 DB 쓰기가 teardown 이후에 도착해 `SQLITE_MISUSE` 로 스위트를 오염시킨다.
71
71
  */
72
72
  afterEach(async () => {
73
73
  await TwinEngine.stop(alpha, SHARED_ID).catch(() => {})
@@ -0,0 +1,152 @@
1
+ /*
2
+ * ★ 시각축은 **두 수만 묻는다** — 저널을 다 읽어 오지 않는다.
3
+ *
4
+ * ── 무엇을 겪었나 (2026-08-20 실측) ─────────────────────────────────────────
5
+ * `TwinEngine.timeRange` 는 공간의 인스턴스마다 `find()` 로 **행을 전부 엔티티로 하이드레이션**한 뒤
6
+ * JS 에서 min/max 를 골랐다. 그런데 이 함수는 화면 상단의 컨텍스트 띠가 **4초마다** 부른다. 개발
7
+ * 서버에서 `order-check` 한 트윈이 275,882행 · `payload` 131MB 였다 — 4초마다 그 JSON 을 전부 파싱했다.
8
+ *
9
+ * 결과: 프로세스 CPU 81~152%, 아무 일도 하지 않는 질의가 8~14초. JS 프로파일 상위가 TypeORM 의
10
+ * `RelationIdLoader`·`RawSqlResultsToEntityTransformer`·`stringToSimpleJson` 이고 GC 가 16.7% 였다.
11
+ * 트윈 틱(계기가 16%)도 저널 쓰기도 아니라 **이 읽기**였다.
12
+ *
13
+ * ── 이 시험이 지키는 것 ─────────────────────────────────────────────────────
14
+ * ① 답이 맞다(공간의 모든 트윈에 걸친 min/max, 남의 공간은 안 섞인다)
15
+ * ② 「없음」을 0 으로 떨어뜨리지 않는다(시각축이 1970년에서 시작하면 스크러버가 못 쓰게 된다)
16
+ * ③ **행을 실어 오지 않는다** — 고친 방식이 되돌아가면(다시 `find()`) 답은 그대로 맞고 느려지기만
17
+ * 하므로, 결과만 보는 시험으로는 회귀를 잡을 수 없다. 그래서 소스를 함께 본다.
18
+ */
19
+ import { test, before, after } from 'node:test'
20
+ import assert from 'node:assert/strict'
21
+ import { createRequire } from 'node:module'
22
+ import { readFileSync } from 'node:fs'
23
+ import type { DataSource } from 'typeorm'
24
+
25
+ const req = createRequire(import.meta.url)
26
+ const { NamingStrategy, addDataSource, entities: shellEntities, Domain } = req('@things-factory/shell')
27
+ const { entities: authEntities } = req('@things-factory/auth-base')
28
+ const { entities: twinEntities } = req('../dist-server/service/index.js')
29
+ const { TwinEngine } = req('../dist-server/engine/index.js')
30
+ const { TwinEvent } = req('../dist-server/service/twin-event/twin-event.js')
31
+ const { TwinInstance } = req('../dist-server/service/twin-instance/twin-instance.js')
32
+ const { DataSource: TypeOrmDataSource } = req('typeorm')
33
+
34
+ let ds: DataSource
35
+ let domainId: string
36
+
37
+ const SPACE = 'space-yard'
38
+ const OTHER_SPACE = 'space-elsewhere'
39
+
40
+ /** 저널 행 하나 — 시각축이 보는 것은 `eventTime` 뿐이다. */
41
+ async function event(instanceId: string, eventTime: string | null, revision: number): Promise<void> {
42
+ await ds.getRepository(TwinEvent).save(
43
+ ds.getRepository(TwinEvent).create({
44
+ domain: { id: domainId } as any,
45
+ instanceId,
46
+ eventType: 'test.event',
47
+ revision,
48
+ eventTime: eventTime === null ? null : new Date(eventTime),
49
+ payload: { note: 'x' }
50
+ } as any)
51
+ )
52
+ }
53
+
54
+ async function instance(instanceId: string, spaceId: string): Promise<void> {
55
+ await ds.getRepository(TwinInstance).save(
56
+ ds.getRepository(TwinInstance).create({
57
+ domain: { id: domainId } as any,
58
+ instanceId,
59
+ spaceId,
60
+ kind: 'yms',
61
+ status: 'stopped',
62
+ restartPolicy: 'reset',
63
+ purpose: 'operational'
64
+ } as any)
65
+ )
66
+ }
67
+
68
+ before(async () => {
69
+ ds = new TypeOrmDataSource({
70
+ type: 'sqlite',
71
+ database: ':memory:',
72
+ synchronize: true,
73
+ dropSchema: true,
74
+ namingStrategy: new NamingStrategy(),
75
+ entities: [...shellEntities, ...authEntities, ...twinEntities],
76
+ logging: false
77
+ })
78
+ await ds.initialize()
79
+ addDataSource('default', ds)
80
+
81
+ const d = await ds.getRepository(Domain).save({ name: 'range-test', subdomain: 'range-test' } as any)
82
+ domainId = d.id
83
+ })
84
+
85
+ after(async () => {
86
+ await ds?.destroy()
87
+ })
88
+
89
+ test('공간의 여러 트윈에 걸쳐 가장 이른 것과 가장 늦은 것을 낸다', async () => {
90
+ await instance('yard-a', SPACE)
91
+ await instance('yard-b', SPACE)
92
+
93
+ /* 가장 이른 것은 b 에, 가장 늦은 것은 a 에 있다 — 한 트윈만 보면 답이 틀린다. */
94
+ await event('yard-a', '2026-03-01T00:00:00.000Z', 1)
95
+ await event('yard-a', '2026-06-30T12:00:00.000Z', 2)
96
+ await event('yard-b', '2026-01-15T09:00:00.000Z', 1)
97
+
98
+ const r = await TwinEngine.timeRange(domainId, SPACE)
99
+ assert.equal(r.minTime, '2026-01-15T09:00:00.000Z')
100
+ assert.equal(r.maxTime, '2026-06-30T12:00:00.000Z')
101
+ })
102
+
103
+ test('다른 공간의 저널은 섞이지 않는다 — 시각축은 그 현장의 것이다', async () => {
104
+ await instance('far-c', OTHER_SPACE)
105
+ await event('far-c', '2020-01-01T00:00:00.000Z', 1)
106
+ await event('far-c', '2030-01-01T00:00:00.000Z', 2)
107
+
108
+ const r = await TwinEngine.timeRange(domainId, SPACE)
109
+ assert.equal(r.minTime, '2026-01-15T09:00:00.000Z', '2020년이 새어 들어오지 않는다')
110
+ assert.equal(r.maxTime, '2026-06-30T12:00:00.000Z', '2030년도 그렇다')
111
+ })
112
+
113
+ test('시각이 빈 행이 시각축을 1970년으로 끌어내리지 않는다', async () => {
114
+ /* 옛 행에는 `eventTime` 이 없을 수 있다. 그것을 0 으로 읽으면 스크러버가 56년을 그린다. */
115
+ await event('yard-a', null, 3)
116
+
117
+ const r = await TwinEngine.timeRange(domainId, SPACE)
118
+ assert.equal(r.minTime, '2026-01-15T09:00:00.000Z', '빈 시각은 「없음」이고 0 이 아니다')
119
+ })
120
+
121
+ test('이벤트가 없으면 「없음」이다 — 0 이나 지금이 아니다', async () => {
122
+ await instance('empty-d', 'space-empty')
123
+
124
+ const r = await TwinEngine.timeRange(domainId, 'space-empty')
125
+ assert.equal(r.minTime, null)
126
+ assert.equal(r.maxTime, null)
127
+ })
128
+
129
+ test('그 공간에 트윈이 없으면 「없음」이다', async () => {
130
+ const r = await TwinEngine.timeRange(domainId, 'space-nobody')
131
+ assert.equal(r.minTime, null)
132
+ assert.equal(r.maxTime, null)
133
+ })
134
+
135
+ test('두 수를 구하려고 행을 실어 오지 않는다 — 되돌아가면 답은 맞고 느려지기만 한다', () => {
136
+ /*
137
+ * 결과만 보는 시험으로는 이 회귀를 잡을 수 없다(`find()` 로 돌려도 min/max 는 맞다). 그래서 함수의
138
+ * 소스를 본다: 집계(MIN/MAX)를 쓰고, 저널을 `find()` 로 긁지 않는다.
139
+ */
140
+ const src = readFileSync(new URL('../server/engine/twin-engine.ts', import.meta.url).pathname, 'utf-8')
141
+ const at = src.indexOf('static async timeRange(')
142
+ assert.ok(at > 0, 'timeRange 를 찾지 못했다 — 이름이 바뀌면 이 검사도 함께 옮긴다')
143
+
144
+ /* 함수 본문만 떼어 본다 — 다음 멤버 선언까지. */
145
+ const body = src.slice(at, src.indexOf('\n /** 정지 훅', at) > 0 ? src.indexOf('\n /** 정지 훅', at) : at + 4000)
146
+
147
+ assert.ok(body.includes("MIN(e.eventTime)") && body.includes("MAX(e.eventTime)"), '집계는 DB 가 한다')
148
+ assert.ok(
149
+ !/getRepository\(TwinEvent\)\s*\.?\s*\n?\s*\.find\(/.test(body),
150
+ '저널을 행 단위로 실어 오면 안 된다 — 275,882행 · payload 131MB 를 4초마다 파싱했던 자리다'
151
+ )
152
+ })
@@ -0,0 +1,61 @@
1
+ /*
2
+ * 사건이 **어느 물품을 건드렸나** — 브로드캐스팅이 바뀐 것만 만들 수 있게 (2026-08-21).
3
+ *
4
+ * 이 판정이 틀리면 **화면이 낡는다.** 그래서 지키는 것을 값으로 못 박는다:
5
+ * ① 개체를 말한 자리를 전부 읽는다 — 관측·집약(부모/자식)·변환(입력/출력).
6
+ * ② 부류 수량만 말한 사건은 「모른다」다 — 그 부류의 어느 개체인지 말하지 않았다.
7
+ * ③ 물품을 만지지 않는다고 **선언된** 사건은 빈 범위다(작업·설비·인원·자산·오더·품질·확인·에너지).
8
+ * ④ 낯선 낱말은 「모른다」다 — 커널이 사건을 늘렸을 때 조용히 빠뜨리지 않는다.
9
+ *
10
+ * 「모른다」(`null`)와 「하나도 없다」(빈 집합)는 **다른 사실**이다. 앞은 전부 다시 만들게 하고 뒤는
11
+ * 물품을 건너뛰게 한다 — 둘을 같게 두면 둘 중 하나가 반드시 틀린다.
12
+ */
13
+ import { test } from 'node:test'
14
+ import assert from 'node:assert/strict'
15
+
16
+ import { touchedItemKeys } from '../shared/touched-items.ts'
17
+
18
+ const epcis = (data: any) => ({ eventType: 'epcis.object', data })
19
+
20
+ test('관측 사건은 말한 개체를 모두 낸다', () => {
21
+ const got = touchedItemKeys(epcis({ epcList: ['urn:epc:id:sgtin:952.1.A', 'urn:epc:id:sgtin:952.1.B'] }))
22
+ assert.deepEqual([...got!].sort(), ['urn:epc:id:sgtin:952.1.A', 'urn:epc:id:sgtin:952.1.B'])
23
+ })
24
+
25
+ test('집약은 부모와 자식을 함께 낸다 — 부모만 내면 팔레트에 담긴 것이 낡는다', () => {
26
+ const got = touchedItemKeys({
27
+ eventType: 'epcis.aggregation',
28
+ data: { parentID: 'urn:epc:id:sscc:952.P1', childEPCs: ['urn:epc:id:sgtin:952.1.A'] }
29
+ })
30
+ assert.deepEqual([...got!].sort(), ['urn:epc:id:sgtin:952.1.A', 'urn:epc:id:sscc:952.P1'])
31
+ })
32
+
33
+ test('변환은 입력과 출력을 함께 낸다 — 소비된 것도 값이 바뀐다', () => {
34
+ const got = touchedItemKeys({
35
+ eventType: 'epcis.transformation',
36
+ data: { inputEPCList: ['in-1', 'in-2'], outputEPCList: ['out-1'] }
37
+ })
38
+ assert.deepEqual([...got!].sort(), ['in-1', 'in-2', 'out-1'])
39
+ })
40
+
41
+ test('부류 수량만 말한 사건은 「모른다」다 — 그 부류의 어느 개체인지 말하지 않았다', () => {
42
+ const got = touchedItemKeys(epcis({ quantityList: [{ epcClass: 'urn:epc:idpat:sgtin:952.1.*', quantity: 3 }] }))
43
+ assert.equal(got, null, '개체를 특정하지 못하면 전부 다시 만들어야 한다')
44
+ })
45
+
46
+ test('물품을 만지지 않는다고 선언된 사건은 빈 범위다 — 전부 다시 만들 이유가 없다', () => {
47
+ for (const eventType of ['task.status', 'equipment.status', 'person.status', 'asset.status', 'order.status', 'quality.output', 'attention.acked']) {
48
+ const got = touchedItemKeys({ eventType, data: { taskId: 't1', moverId: 'fk-1' } })
49
+ assert.ok(got, `${eventType} 은 「모른다」가 아니다`)
50
+ assert.equal(got!.size, 0, `${eventType} 은 물품을 건드리지 않는다`)
51
+ }
52
+ const energy = touchedItemKeys({ eventType: 'energy.measured', data: { id: 'm-1', kW: 12 } })
53
+ assert.equal(energy!.size, 0)
54
+ })
55
+
56
+ test('낯선 낱말은 「모른다」다 — 커널이 사건을 늘렸을 때 조용히 빠뜨리지 않는다', () => {
57
+ assert.equal(touchedItemKeys({ eventType: 'something.new', data: { epcList: ['x'] } }), null)
58
+ assert.equal(touchedItemKeys({ data: { epcList: ['x'] } }), null, '낱말이 없으면 판정할 수 없다')
59
+ assert.equal(touchedItemKeys(undefined), null)
60
+ assert.equal(touchedItemKeys({ eventType: 'epcis.object' }), null, '내용이 없으면 판정할 수 없다')
61
+ })
@@ -7,7 +7,15 @@
7
7
  import { test } from 'node:test'
8
8
  import assert from 'node:assert/strict'
9
9
 
10
- import { bizStepOf, epcOf, orderOf, locationOf, equipmentIdOf, twinEventKeys } from '../server/service/twin-event/twin-event-keys.ts'
10
+ /*
11
+ * **빌드 산출물을 부른다** — 이 모듈은 로그 문(`engine/log.js`)을 값으로 참조하고, 타입만 지우고 소스를
12
+ * 실행하는 방식은 그 `.js` 경로를 `.ts` 로 바꿔 주지 않는다. 소스를 부르면 「모듈을 찾을 수 없다」로 선다.
13
+ * **소스를 고친 뒤에는 `npm run build:server` 를 먼저 돌린다.**
14
+ */
15
+ import { createRequire } from 'node:module'
16
+
17
+ const req = createRequire(import.meta.url)
18
+ const { bizStepOf, epcOf, orderOf, locationOf, equipmentIdOf, twinEventKeys } = req('../dist-server/service/twin-event/twin-event-keys.js')
11
19
 
12
20
  /** EPCIS ObjectEvent 한 건(입고) — 실제 인제스트가 싣는 모양. */
13
21
  const EPCIS_OBJECT = {
@@ -102,7 +110,7 @@ test('상한을 넘는 값은 조용히 사라지지 않는다 — 잘리되 경
102
110
  })
103
111
 
104
112
  test('빈 봉투에도 터지지 않는다', () => {
105
- const empty = { bizStep: undefined, epc: undefined, orderId: undefined, locationId: undefined, moverId: undefined }
113
+ const empty = { action: undefined, bizStep: undefined, epc: undefined, orderId: undefined, locationId: undefined, moverId: undefined }
106
114
  assert.deepEqual(twinEventKeys(undefined), empty)
107
115
  assert.deepEqual(twinEventKeys({}), empty)
108
116
  })
@@ -85,7 +85,7 @@ before(async () => {
85
85
  q = new TwinModelItemQuery()
86
86
  ctx = { state: { domain: { id: domainId } } }
87
87
 
88
- await TwinEngine.ingestMaster(domainId, master())
88
+ await TwinEngine.ingestMaster(domainId, master(), 'reset')
89
89
  })
90
90
 
91
91
  after(async () => {
@@ -213,7 +213,7 @@ test('지도의 선이 진단이 된다 — 이어진 것뿐 아니라 **끊긴
213
213
  const bad = master()
214
214
  bad.equipment.push({ id: 'ghost-1', kind: 'welder', homeLocationId: 'no-such-station' }) // 끊어진 참조
215
215
  bad.equipment.push({ id: 'floater-1', kind: 'welder' }) // 기준 자리 없음
216
- await TwinEngine.ingestMaster(domainId, bad)
216
+ await TwinEngine.ingestMaster(domainId, bad, 'reset')
217
217
 
218
218
  const sum = await new TwinModelQuery().twinModelSummary(INSTANCE, ctx)
219
219
  const home = sum.edges.find((e: any) => e.from === 'equipment' && e.to === 'locations' && e.via === 'twin.rel.home')
@@ -238,6 +238,8 @@ test('멈춘 트윈도 저널을 접어 답하고, 그 답은 다시 계산한
238
238
  const model = {
239
239
  spaceId: 'journal-space',
240
240
  utcOffsetMinutes: 0,
241
+ /* 커널은 선언 없이 팔레트·문서를 만들지 않는다 — 픽스처 자기 이름공간을 준다. */
242
+ identity: { namespaces: ['https://twin.test/twin-model-item/journal'] },
241
243
  locations: [
242
244
  { id: 'dock-recv', type: 'dock', capacity: 100 },
243
245
  { id: 'bin-1', type: 'storage', capacity: 20 }
@@ -263,7 +265,7 @@ test('멈춘 트윈도 저널을 접어 답하고, 그 답은 다시 계산한
263
265
  for (let i = 0; i < 200; i++) k.tick(1000)
264
266
 
265
267
  const INST = 'journal-probe'
266
- await TwinEngine.provision(domainId, INST, 'wms', model)
268
+ await TwinEngine.provision(domainId, INST, 'wms', model, 'reset')
267
269
  await TwinEngine.persistBatch(domainId, INST, journal.all(), 0)
268
270
  await TwinEngine.stop(domainId, INST).catch(() => {})
269
271
  delete (TwinEngine as any).recovered[`${domainId}:${INST}`]
@@ -76,7 +76,7 @@ before(async () => {
76
76
  domainId = d.id
77
77
  q = new TwinModelTreeQuery()
78
78
  ctx = { state: { domain: { id: domainId } } }
79
- await TwinEngine.ingestMaster(domainId, master())
79
+ await TwinEngine.ingestMaster(domainId, master(), 'reset')
80
80
  })
81
81
 
82
82
  after(async () => {
@@ -108,13 +108,13 @@ test('수량을 곱해 내려간다 — 그리고 모르면 곱하지 않는다'
108
108
  { key: 'frame-line', label: 'f', inputs: [{ material: 'steel', qty: 4 }], outputs: [{ material: 'frame', qty: 1 }] }
109
109
  ]
110
110
  })
111
- await TwinEngine.ingestMaster(domainId, noQty)
111
+ await TwinEngine.ingestMaster(domainId, noQty, 'reset')
112
112
  const t2 = await explode('trailer')
113
113
  assert.equal(t2.items.find((n: any) => n.id === 'frame').qty, null, '모르는 수량을 1 로 메우지 않는다')
114
114
  assert.equal(t2.items.find((n: any) => n.id === 'steel').qty, null, '모르는 값 아래는 전부 모르는 값이다')
115
115
  assert.equal(t2.qtyIncomplete, true, '합계가 성립하지 않는다는 사실을 낸다')
116
116
 
117
- await TwinEngine.ingestMaster(domainId, master())
117
+ await TwinEngine.ingestMaster(domainId, master(), 'reset')
118
118
  })
119
119
 
120
120
  test('역전개도 같은 선언으로 — 방향만 뒤집는다', async () => {
@@ -146,11 +146,11 @@ test('사이클은 다시 탐색하지 않고 알린다', async () => {
146
146
  { key: 'frame-line', label: 'f', inputs: [{ material: 'trailer', qty: 1 }], outputs: [{ material: 'frame', qty: 1 }] }
147
147
  ]
148
148
  })
149
- await TwinEngine.ingestMaster(domainId, loop)
149
+ await TwinEngine.ingestMaster(domainId, loop, 'reset')
150
150
  const t = await explode('trailer')
151
151
  assert.ok(t.items.length < 10, '무한히 자라지 않는다')
152
152
  assert.ok(t.cycles.length > 0, '사이클을 만났다는 사실을 말한다')
153
- await TwinEngine.ingestMaster(domainId, master())
153
+ await TwinEngine.ingestMaster(domainId, master(), 'reset')
154
154
  })
155
155
 
156
156
  test('상한에서 끊기면 끊겼다고 알린다 — 조용히 자르지 않는다', async () => {
@@ -167,10 +167,10 @@ test('선언에 없는 자재를 참조하면 그 사실이 드러난다', async
167
167
  { key: 'trailer-line', label: 't', inputs: [{ material: 'no-such-part', qty: 1 }], outputs: [{ material: 'trailer', qty: 1 }] }
168
168
  ]
169
169
  })
170
- await TwinEngine.ingestMaster(domainId, ghost)
170
+ await TwinEngine.ingestMaster(domainId, ghost, 'reset')
171
171
  const t = await explode('trailer')
172
172
  const child = t.items.find((n: any) => n.id === 'no-such-part')
173
173
  assert.ok(child, '가리켜진 것은 그리되')
174
174
  assert.equal(child.known, false, '선언에 없다는 사실을 함께 낸다(있는 척하지 않는다)')
175
- await TwinEngine.ingestMaster(domainId, master())
175
+ await TwinEngine.ingestMaster(domainId, master(), 'reset')
176
176
  })
@@ -68,7 +68,7 @@ after(async () => {
68
68
  })
69
69
 
70
70
  test('인제스트가 원천을 각인한다 — 원천이 무엇이든 한 자리에', async () => {
71
- await TwinEngine.ingestMaster(domainId, master(BORN, 3, { kind: 'template', templateId: 'wh-unit-load', params: { bins: 3 } }))
71
+ await TwinEngine.ingestMaster(domainId, master(BORN, 3, { kind: 'template', templateId: 'wh-unit-load', params: { bins: 3 } }), 'reset')
72
72
  assert.deepEqual(await originOf(BORN), { kind: 'template', templateId: 'wh-unit-load', params: { bins: 3 } })
73
73
  })
74
74
 
@@ -78,7 +78,7 @@ test('원천을 모르는 재프로비전이 이미 아는 것을 지우지 않
78
78
  * 인제스트한다. 그때 `origin` 을 덮어 비우면 그 트윈은 **조용히 다시 읽을 수 없게 된다.**
79
79
  * 그런 종류의 손상은 다음번에 다시 읽으려 할 때까지 아무도 모른다.
80
80
  */
81
- await TwinEngine.ingestMaster(domainId, master(BORN, 5))
81
+ await TwinEngine.ingestMaster(domainId, master(BORN, 5), 'reset')
82
82
  assert.deepEqual(
83
83
  await originOf(BORN),
84
84
  { kind: 'template', templateId: 'wh-unit-load', params: { bins: 3 } },
@@ -90,12 +90,12 @@ test('원천을 모르는 재프로비전이 이미 아는 것을 지우지 않
90
90
  })
91
91
 
92
92
  test('새 원천을 말하면 갱신된다 — 보존은 침묵에만 적용된다', async () => {
93
- await TwinEngine.ingestMaster(domainId, master(BORN, 5, { kind: 'reference', source: 'oracle-wms' }))
93
+ await TwinEngine.ingestMaster(domainId, master(BORN, 5, { kind: 'reference', source: 'oracle-wms' }), 'reset')
94
94
  assert.deepEqual(await originOf(BORN), { kind: 'reference', source: 'oracle-wms' })
95
95
  })
96
96
 
97
97
  test('원천 없이 태어난 트윈은 원천이 없다 — 짐작해 채우지 않는다', async () => {
98
- await TwinEngine.ingestMaster(domainId, master(ORPHAN, 2))
98
+ await TwinEngine.ingestMaster(domainId, master(ORPHAN, 2), 'reset')
99
99
  assert.equal(await originOf(ORPHAN), null, '모르는 것을 아는 척하면 엉뚱한 원본으로 현장을 덮는다')
100
100
  })
101
101
 
@@ -30,6 +30,10 @@ const GTIN = 'urn:epc:idpat:sgtin:0614141.012345.*'
30
30
  * 이 테스트가 흐름 요건까지 시험하려는 것이 아니므로 커널 테스트와 같은 네 자리를 쓴다.
31
31
  */
32
32
  const model = () => ({
33
+ /* 이 픽스처가 식별자를 만드는 이름공간 — **픽스처마다 다르게** 둔다(같은 접두 아래에서 서로 다른
34
+ 시험의 번호가 부딪치면 표준의 유일성 요구를 깬다, CBV 2.0 §8.2.4). 커널은 선언 없이 팔레트·문서를
35
+ 만들지 않는다(0.7.45 에서 날조 프리픽스를 지웠다). */
36
+ identity: { namespaces: ['https://twin.test/warm-start-seam'] },
33
37
  locations: [
34
38
  { id: 'dock-recv', type: 'dock', capacity: 100 },
35
39
  { id: 'bin-1', type: 'storage', capacity: 200 },