@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
@@ -86,7 +86,7 @@ before(async () => {
86
86
  const d = await ds.getRepository(Domain).save({ name: 'guard-test', subdomain: 'guard-test' } as any)
87
87
  domainId = d.id
88
88
 
89
- await TwinEngine.ingestMaster(domainId, master(3, '서울 창고', '보관 구역'))
89
+ await TwinEngine.ingestMaster(domainId, master(3, '서울 창고', '보관 구역'), 'reset')
90
90
  })
91
91
 
92
92
  after(async () => {
@@ -95,7 +95,7 @@ after(async () => {
95
95
  })
96
96
 
97
97
  test('멈춰 있으면 재인제스트가 통한다 — 막는 것은 가동 중일 때뿐이다', async () => {
98
- await TwinEngine.ingestMaster(domainId, master(5, '서울 창고', '보관 구역'))
98
+ await TwinEngine.ingestMaster(domainId, master(5, '서울 창고', '보관 구역'), 'reset')
99
99
  const t = await trace()
100
100
  assert.equal(t.boardLocations, 6, '자리 6개(도크 1 + 빈 5)로 갱신된다')
101
101
  assert.deepEqual(t.revs, [1, 2], '구조가 달라졌으니 리비전이 하나 늘어난다')
@@ -103,7 +103,7 @@ test('멈춰 있으면 재인제스트가 통한다 — 막는 것은 가동 중
103
103
  })
104
104
 
105
105
  test('가동 중이면 거절하고 — **아무것도 바꾸지 않는다**', async () => {
106
- await TwinEngine.startFromRegistry(domainId, INSTANCE, 'sim-experiment')
106
+ await TwinEngine.startFromRegistry(domainId, INSTANCE, 'reset')
107
107
  const before = await trace()
108
108
 
109
109
  /*
@@ -111,7 +111,7 @@ test('가동 중이면 거절하고 — **아무것도 바꾸지 않는다**', a
111
111
  * 부분 쓰기는 앞쪽(공간·구역)에서 일어났으므로, 구조만 확인하면 예전 결함을 못 잡는다.
112
112
  */
113
113
  await assert.rejects(
114
- () => TwinEngine.ingestMaster(domainId, master(9, '부산 창고', '다른 구역')),
114
+ () => TwinEngine.ingestMaster(domainId, master(9, '부산 창고', '다른 구역'), 'reset'),
115
115
  /is running/,
116
116
  '가동 중에는 거절해야 한다'
117
117
  )
@@ -121,7 +121,7 @@ test('가동 중이면 거절하고 — **아무것도 바꾸지 않는다**', a
121
121
 
122
122
  test('멈추면 다시 통한다 — 거절은 잠금이 아니라 순서다', async () => {
123
123
  await TwinEngine.stop(domainId, INSTANCE)
124
- await TwinEngine.ingestMaster(domainId, master(9, '부산 창고', '다른 구역'))
124
+ await TwinEngine.ingestMaster(domainId, master(9, '부산 창고', '다른 구역'), 'reset')
125
125
 
126
126
  const t = await trace()
127
127
  assert.equal(t.boardLocations, 10, '아까 거절당한 그 구조가 이제 들어간다')
@@ -74,7 +74,7 @@ after(async () => {
74
74
  })
75
75
 
76
76
  test('지운 트윈의 캐시는 남지 않는다 — TTL 은 없어진 주인을 지우지 않는다', async () => {
77
- await TwinEngine.ingestMaster(domainId, master())
77
+ await TwinEngine.ingestMaster(domainId, master(), 'reset')
78
78
  /* 캐시를 실제로 채운다 — 손으로 넣으면 "채우는 경로" 가 바뀌어도 검사가 통과한다. */
79
79
  await TwinEngine.structureRevOf(domainId, INSTANCE)
80
80
  const { measured, structureRev } = caches()
@@ -0,0 +1,103 @@
1
+ /*
2
+ * ★ 연결 적합성 하네스 — **절반만 돌린 스위트가 초록을 주장하지 못하게.**
3
+ *
4
+ * 지키는 것:
5
+ * ① 돌리지 않은 프로브는 **「모른다」**로 접힌다 → 판정이 그 단계를 달성으로 만들지 않는다.
6
+ * ② **실패한 프로브의 수를 0 으로 읽지 않는다.** 「0건이었다」와 「세지 못했다」는 다른 사실이다.
7
+ * ③ **쓰기 프로브는 동의 없이 계획에 들어오지 않는다** — 남의 운영 시스템에 명령을 넣는 일이다.
8
+ * ④ **게이팅 실패 뒤는 돌리지 않는다** — 빨간 칸을 늘리는 것은 원인을 알려 주지 않는다.
9
+ */
10
+ import { test } from 'node:test'
11
+ import assert from 'node:assert/strict'
12
+
13
+ import { PROBE_SEQUENCE, foldProbeResults, plannedProbes, type ProbeResult } from '../server/engine/integration-probes.ts'
14
+ import { assessStagePath, forbiddenClaims } from '../server/engine/stage-path.ts'
15
+
16
+ const pass = (id: any, count?: number, coverage?: { declared: number; total: number }): ProbeResult =>
17
+ ({ id, ran: true, passed: true, ...(count !== undefined ? { count } : {}), ...(coverage ? { coverage } : {}) })
18
+
19
+ /** 읽기 프로브 전부 통과 — 반출까지 관측된 상태. */
20
+ const readOnlyAllPass = (): ProbeResult[] => [
21
+ pass('connect'), pass('discover', 1), pass('master', 12), pass('master-warnings', 3), pass('items', 340),
22
+ pass('live-ingest', 1200), pass('live-removal', 17),
23
+ pass('declarations', undefined, { declared: 6, total: 8 }), pass('forecast'),
24
+ pass('actuate-declared')
25
+ ]
26
+
27
+ test('시퀀스는 읽기부터 시작하고 쓰기 프로브는 하나뿐이다', () => {
28
+ assert.equal(PROBE_SEQUENCE[0].id, 'connect')
29
+ assert.deepEqual(PROBE_SEQUENCE.filter(p => p.writes).map(p => p.id), ['actuate-effect'])
30
+ })
31
+
32
+ test('아무것도 돌리지 않았으면 첫 칸부터 「모른다」다 — 실패로 접지 않는다', () => {
33
+ const p = assessStagePath(foldProbeResults([]))
34
+ assert.equal(p.reached, null)
35
+ assert.deepEqual(p.next?.stage, 'structure')
36
+ assert.ok(p.stages[0].blockedBy.includes('no-locations'))
37
+ assert.ok(p.stages[0].unmeasured.includes('master-warnings'), '세지 않은 것은 모르는 것이다')
38
+ })
39
+
40
+ test('읽기만 전부 통과하면 예측까지 도달하고 폐루프는 「검증되지 않음」으로 남는다', () => {
41
+ const p = assessStagePath(foldProbeResults(readOnlyAllPass()))
42
+ assert.equal(p.reached, 'forecast')
43
+ assert.deepEqual(p.stages[3].unmeasured, ['correlated-effect'])
44
+ assert.deepEqual(p.stages[3].blockedBy, [], '능력은 선언∩구현이므로 막힌 것이 아니라 안 돌린 것이다')
45
+ assert.deepEqual(forbiddenClaims(p), ['claim.automatic-control'])
46
+ })
47
+
48
+ test('쓰기 프로브를 돌려 효과가 관측되면 폐루프가 닫힌다', () => {
49
+ const p = assessStagePath(foldProbeResults([...readOnlyAllPass(), pass('actuate-effect', 2)]))
50
+ assert.equal(p.reached, 'closed-loop')
51
+ assert.deepEqual(forbiddenClaims(p), [])
52
+ })
53
+
54
+ test('측정하지 못한 프로브는 수를 들지 않는다 — 0 을 지어내지 않는다', () => {
55
+ /*
56
+ * 규칙은 「실패면 모른다」가 아니다 — **센 값은 판정과 무관한 사실**이다(0건도 사실이다).
57
+ * 측정 자체를 못 한 프로브(타임아웃 등)는 `count` 를 **비운다**. 0 을 채우면 「세어 보니 없었다」로
58
+ * 읽히고, 그건 스냅샷 피드를 delta 로 오인하게 만드는 정반대의 결론을 낳는다.
59
+ */
60
+ const rs: ProbeResult[] = [...readOnlyAllPass().filter(r => r.id !== 'live-removal'),
61
+ { id: 'live-removal', ran: true, passed: false, detail: 'timeout' }]
62
+ const e = foldProbeResults(rs)
63
+ assert.equal(e.removalsObserved, null, '측정하지 못했으면 모른다')
64
+ assert.deepEqual(assessStagePath(e).stages[1].unmeasured, ['removals'])
65
+ })
66
+
67
+ test('세어서 0 을 얻은 것은 사실이다 — 모른다로 접지 않는다', () => {
68
+ const rs: ProbeResult[] = [...readOnlyAllPass().filter(r => r.id !== 'live-removal'),
69
+ { id: 'live-removal', ran: true, passed: false, count: 0 }]
70
+ const e = foldProbeResults(rs)
71
+ assert.equal(e.removalsObserved, 0)
72
+ assert.deepEqual(assessStagePath(e).stages[1].blockedBy, ['no-removal-observed'])
73
+ })
74
+
75
+ test('반출을 세었는데 0 이면 라이브가 막힌다 — 스냅샷 되풀어 읽기', () => {
76
+ const rs: ProbeResult[] = [...readOnlyAllPass().filter(r => r.id !== 'live-removal'), pass('live-removal', 0)]
77
+ const p = assessStagePath(foldProbeResults(rs))
78
+ assert.deepEqual(p.stages[1].blockedBy, ['no-removal-observed'])
79
+ assert.equal(p.reached, 'structure')
80
+ assert.ok(forbiddenClaims(p).includes('claim.realtime'))
81
+ })
82
+
83
+ test('쓰기 프로브는 동의 없이 계획에 들어오지 않는다', () => {
84
+ assert.ok(!plannedProbes([]).includes('actuate-effect'))
85
+ assert.ok(plannedProbes([], { allowWrites: true }).includes('actuate-effect'))
86
+ })
87
+
88
+ test('게이팅 프로브가 실패하면 그 뒤는 계획하지 않는다', () => {
89
+ const planned = plannedProbes([{ id: 'connect', ran: true, passed: false, detail: '401' }])
90
+ assert.deepEqual(planned, [], '붙지 못했으면 뒤를 두드릴 이유가 없다')
91
+ })
92
+
93
+ test('통과한 프로브는 다시 계획하지 않고, 남은 것만 계획한다', () => {
94
+ const planned = plannedProbes([pass('connect'), pass('discover', 1)])
95
+ assert.equal(planned[0], 'master')
96
+ assert.ok(!planned.includes('connect'))
97
+ })
98
+
99
+ test('게이팅이 아닌 프로브의 실패는 뒤를 멈추지 않는다', () => {
100
+ const planned = plannedProbes([pass('connect'), pass('discover', 1), pass('master', 12),
101
+ { id: 'master-warnings', ran: true, passed: false, detail: 'not-counted' }])
102
+ assert.ok(planned.includes('items'), '못 옮긴 값을 세지 못한 것이 구조 확인을 멈추지는 않는다')
103
+ })
@@ -0,0 +1,95 @@
1
+ /*
2
+ * ★ 실행기 — **회색 칸이 빨간 칸으로 위장되지 않게.**
3
+ *
4
+ * ① 시뮬 트윈에 라이브를 요구하지 않는다(해당 없음). 그 빨강은 결함이 아니다.
5
+ * ② 템플릿으로 세운 트윈에는 옮겨 온 마스터가 없다 — 「못 옮긴 값」을 물을 대상이 아니다.
6
+ * ③ 반출 0 의 처방이 둘로 갈린다: 스냅샷이면 방식을 바꿔야 하고, 아니면 더 기다릴 수 있다.
7
+ * ④ 액추에이션은 계약에 없다 — 측정을 만들지 않고 미실행으로 남긴다.
8
+ */
9
+ import { test } from 'node:test'
10
+ import assert from 'node:assert/strict'
11
+
12
+ import { assessSuite, type SuiteFacts } from '../server/engine/integration-runner.ts'
13
+
14
+ const live = (): SuiteFacts => ({
15
+ subject: 'live',
16
+ fromReference: true,
17
+ adapterCapabilities: ['live'],
18
+ connectOk: true,
19
+ siteCount: 3,
20
+ locationCount: 12,
21
+ itemCount: 340,
22
+ lastWarningCount: 2,
23
+ journalTotal: 1200,
24
+ removalsJournaled: 17,
25
+ specCoverage: { declared: 6, total: 8 },
26
+ forecastRan: true,
27
+ feed: 'delta-poll'
28
+ })
29
+
30
+ test('라이브 전부 갖추면 예측까지 도달하고, 폐루프는 능력이 없어 막힌다', () => {
31
+ const a = assessSuite(live())
32
+ assert.equal(a.path.reached, 'forecast')
33
+ assert.deepEqual(a.path.stages[3].blockedBy, ['no-actuate-capability'])
34
+ assert.deepEqual(a.forbiddenClaims, ['claim.automatic-control'])
35
+ assert.equal(a.profile.multiSite, true)
36
+ })
37
+
38
+ test('시뮬 트윈은 라이브가 해당 없음이고, 그 사유를 말한다', () => {
39
+ const a = assessSuite({ ...live(), subject: 'sim', journalTotal: null, removalsJournaled: null })
40
+ assert.deepEqual(a.notApplicable.map(n => n.id), ['live-ingest', 'live-removal'])
41
+ assert.deepEqual(a.path.stages[1].blockedBy, ['subject-is-simulation'])
42
+ assert.deepEqual(a.path.stages[1].unmeasured, [], '해당 없음을 미측정으로 접지 않는다')
43
+ assert.equal(a.path.reached, 'structure')
44
+ assert.ok(!a.planned.includes('live-removal'), '해당 없는 프로브는 계획하지 않는다')
45
+ })
46
+
47
+ test('템플릿으로 세운 트윈은 못 옮긴 값을 물을 대상이 아니다', () => {
48
+ const a = assessSuite({ ...live(), fromReference: false, lastWarningCount: null })
49
+ assert.deepEqual(a.notApplicable.map(n => n.id), ['master-warnings'])
50
+ assert.equal(a.path.stages[0].achieved, true, '해당 없음이 구조 달성을 막지 않는다')
51
+ })
52
+
53
+ test('반출 0 — 스냅샷이면 방식을 바꿔야 한다고 말한다', () => {
54
+ const a = assessSuite({ ...live(), removalsJournaled: 0, feed: 'snapshot-poll' })
55
+ assert.deepEqual(a.path.stages[1].blockedBy, ['snapshot-only-feed'])
56
+ assert.ok(a.forbiddenClaims.includes('claim.realtime'))
57
+ })
58
+
59
+ test('반출 0 — 스냅샷이 아니면 아직 일어나지 않은 것일 수 있다', () => {
60
+ const a = assessSuite({ ...live(), removalsJournaled: 0, feed: 'delta-poll' })
61
+ assert.deepEqual(a.path.stages[1].blockedBy, ['no-removal-observed'])
62
+ })
63
+
64
+ test('승격 전 저널은 반출을 셀 수 없다 — 「없다」가 아니라 「모른다」', () => {
65
+ const a = assessSuite({ ...live(), removalsJournaled: null })
66
+ assert.deepEqual(a.path.stages[1].unmeasured, ['removals'])
67
+ assert.deepEqual(a.path.stages[1].blockedBy, [])
68
+ })
69
+
70
+ test('액추에이션 효과는 측정을 만들지 않고 미실행으로 남긴다', () => {
71
+ const a = assessSuite({ ...live(), adapterCapabilities: ['live', 'actuate'] })
72
+ const eff = a.results.find(r => r.id === 'actuate-effect')!
73
+ assert.equal(eff.ran, false)
74
+ assert.equal(eff.detail, 'actuation-not-wired')
75
+ assert.deepEqual(a.path.stages[3].unmeasured, ['correlated-effect'])
76
+ })
77
+
78
+ test('쓰기 프로브는 동의 없이 계획에 들어오지 않는다', () => {
79
+ const able = { ...live(), adapterCapabilities: ['live', 'actuate'] }
80
+ assert.ok(!assessSuite(able).planned.includes('actuate-effect'))
81
+ assert.ok(assessSuite(able, { allowWrites: true }).planned.includes('actuate-effect'))
82
+ })
83
+
84
+ test('액추에이션 능력이 없으면 쓰기 동의가 있어도 계획하지 않는다 — 보낼 문이 없다', () => {
85
+ assert.ok(!assessSuite(live(), { allowWrites: true }).planned.includes('actuate-effect'))
86
+ })
87
+
88
+ test('접속을 안 돌렸으면 구조부터 미측정이 아니라 미실행으로 남고, 계획의 첫 칸이 된다', () => {
89
+ const a = assessSuite({ ...live(), connectOk: null })
90
+ assert.equal(a.planned[0], 'connect')
91
+ })
92
+
93
+ test('못 옮긴 값이 0 이어도 세었으므로 통과다', () => {
94
+ assert.equal(assessSuite({ ...live(), lastWarningCount: 0 }).path.stages[0].achieved, true)
95
+ })
@@ -0,0 +1,78 @@
1
+ /*
2
+ * 개체 → 품목 참조 — **정체성 모양이 하나가 아니고, 구분자는 우리가 정하지 않는다** (2026-08-23).
3
+ *
4
+ * ── 무엇이 조용히 비어 있었나 ───────────────────────────────────────────────
5
+ * 화면이 「이 개체는 선언된 자재가 아닙니다」라고 말했다. 그런데 그 품목은 원본의 선언에 이름까지 있었고
6
+ * (「냉동그린빈스」), 대응 표도 멀쩡했다. 원인은 참조를 꺼내는 규칙이 GS1 SGTIN 하나만 알았던 것이다.
7
+ *
8
+ * ── 그리고 고치는 방향을 한 번 더 고쳤다 ────────────────────────────────────
9
+ * 처음에는 「`@` 뒤는 개체다」를 규칙으로 넣었다. 그것은 **한 원본(chef)의 관행**이고, 커널 계약이 그
10
+ * 구분자를 정한 적이 없다 — 사용자가 「이 화면이 특정 레거시를 기준으로 해서는 안 된다」고 짚었다.
11
+ * 그래서 자르는 판단을 걷고, **그 트윈의 선언과 견주어** 답한다.
12
+ *
13
+ * 이 시험이 지키는 것:
14
+ * ① SGTIN 은 표준이 정한 자리에서 품목 참조를 꺼낸다.
15
+ * ② 그 밖의 모양은 **손대지 않는다**(구분자를 모른다).
16
+ * ③ 선언과 견줄 때는 두 방향을 본다 — 관측이 선언을 확장한 경우, 화면이 짧게 줄인 경우.
17
+ * ④ 후보가 둘 이상이면 **답하지 않는다**(모르는 것이 틀린 답보다 낫다).
18
+ */
19
+ import { test } from 'node:test'
20
+ import assert from 'node:assert/strict'
21
+
22
+ import { itemRefOf, resolveByDeclaration } from '../server/service/twin-model/item-ref.ts'
23
+
24
+ test('GS1 SGTIN 은 품목 참조 숫자다 — 표준이 자리를 정해 두었다', () => {
25
+ assert.equal(itemRefOf('urn:epc:id:sgtin:0614141.077101.42'), '077101')
26
+ assert.equal(itemRefOf('urn:epc:idpat:sgtin:0614141.078101.*'), '078101')
27
+ })
28
+
29
+ test('그 밖의 정체성은 손대지 않는다 — 어느 부호가 개체인지 우리가 정하지 않는다', () => {
30
+ assert.equal(itemRefOf('https://x.net/class/product-M1@M2R0001'), 'https://x.net/class/product-M1@M2R0001')
31
+ assert.equal(itemRefOf('https://x.net/class/product-M1'), 'https://x.net/class/product-M1')
32
+ assert.equal(itemRefOf(' https://x.net/class/p-1 '), 'https://x.net/class/p-1', '앞뒤 공백만 다듬는다')
33
+ })
34
+
35
+ test('읽을 수 없는 값은 null 이다', () => {
36
+ assert.equal(itemRefOf(''), null)
37
+ assert.equal(itemRefOf(' '), null)
38
+ assert.equal(itemRefOf(null), null)
39
+ assert.equal(itemRefOf(undefined), null)
40
+ assert.equal(itemRefOf(42), null)
41
+ })
42
+
43
+ const DECLARED = new Map<string, string>([
44
+ ['https://x.net/class/product-M0000438', 'M0000438'],
45
+ ['https://x.net/class/product-B0000033', 'B0000033']
46
+ ])
47
+
48
+ test('그대로 같으면 그것이다', () => {
49
+ assert.equal(resolveByDeclaration('https://x.net/class/product-M0000438', DECLARED), 'M0000438')
50
+ })
51
+
52
+ test('관측이 선언을 확장한 경우 — 개체는 자기 클래스로 시작한다(부호를 모른다)', () => {
53
+ for (const sep of ['@', '#', '/', ':', '']) {
54
+ assert.equal(
55
+ resolveByDeclaration(`https://x.net/class/product-M0000438${sep}M2R0001`, DECLARED),
56
+ 'M0000438',
57
+ `구분자 「${sep}」 에서도 같은 답이어야 한다`
58
+ )
59
+ }
60
+ })
61
+
62
+ test('화면이 짧게 줄인 경우 — 선언이 그것으로 끝난다', () => {
63
+ assert.equal(resolveByDeclaration('net/class/product-B0000033', DECLARED), 'B0000033')
64
+ assert.equal(resolveByDeclaration('product-M0000438', DECLARED), 'M0000438')
65
+ })
66
+
67
+ test('후보가 둘 이상이면 답하지 않는다 — 남의 레시피와 견주지 않게', () => {
68
+ const ambiguous = new Map<string, string>([
69
+ ['https://a.net/class/product-1', 'A1'],
70
+ ['https://b.net/class/product-1', 'B1']
71
+ ])
72
+ assert.equal(resolveByDeclaration('class/product-1', ambiguous), undefined)
73
+ })
74
+
75
+ test('선언에 없으면 없는 것이다 — 비슷한 것을 이어 주지 않는다', () => {
76
+ assert.equal(resolveByDeclaration('https://x.net/class/product-ZZZ', DECLARED), undefined)
77
+ assert.equal(resolveByDeclaration(null, DECLARED), undefined)
78
+ })
@@ -0,0 +1,177 @@
1
+ /*
2
+ * ★ 저널을 통째로 읽는 자리는 **코드에 그 이유가 적혀 있어야 한다** — 소스 규율 검사.
3
+ *
4
+ * ── 왜 이 검사가 필요한가 (2026-08-20) ──────────────────────────────────────
5
+ * `TwinEngine.timeRange` 는 min/max **두 수**를 구하려고 공간의 인스턴스마다 저널을 `find()` 로 전부
6
+ * 실어 왔다. 개발 서버에서 한 트윈이 275,882행 · `payload` 131MB 였고, 화면 상단의 컨텍스트 띠가 그
7
+ * 함수를 **4초마다** 불렀다. 프로세스 CPU 81~152%, 아무 일도 하지 않는 질의가 8~14초였다.
8
+ *
9
+ * **사람 리뷰가 이것을 잡지 못한 이유는 코드가 맞았기 때문이다.** min/max 는 정확히 나왔다. 틀린 것은
10
+ * 답이 아니라 비용이고, 비용은 읽어서는 보이지 않는다. 그래서 사람에게 다시 맡기지 않고 기계에 맡긴다.
11
+ *
12
+ * ── 규율 ────────────────────────────────────────────────────────────────────
13
+ * 저널(`TwinEvent`)을 `find()` 로 읽는 자리는 둘 중 하나여야 한다.
14
+ * ① **상한이 있다**(`take:`) — 목록·표본처럼 화면이 쓰는 만큼만 읽는 경우.
15
+ * ② **폴드다** — 사실을 하나씩 접는 것이 목적이라 그 구간의 행이 필요한 경우. 이때는 바로 앞에
16
+ * `journal-fold:` 로 시작하는 주석을 두어 **왜 필요한지**, 그리고 상한이 없다면 **없어질 조건**을
17
+ * 함께 적는다.
18
+ *
19
+ * 둘 다 아니면 이 검사가 빨개진다. 「두 수만 필요한데 표를 다 읽는」 코드가 다시 들어오는 것을 막는
20
+ * 자리가 여기다. 집계(MIN/MAX/COUNT)는 DB 가 하고 행은 실어 오지 않는다.
21
+ */
22
+ import { test } from 'node:test'
23
+ import assert from 'node:assert/strict'
24
+ import { readFileSync, readdirSync, statSync } from 'node:fs'
25
+ import { join } from 'node:path'
26
+
27
+ /** 훑는 곳 — 저널을 읽을 수 있는 층 둘. */
28
+ const ROOTS = [
29
+ new URL('../server', import.meta.url).pathname,
30
+ new URL('../../operato-twin/server', import.meta.url).pathname
31
+ ]
32
+
33
+ const READ = 'getRepository(TwinEvent)'
34
+ /** 폴드라고 밝히는 표식. 이유를 함께 적는 것이 이 표식의 값이다. */
35
+ const FOLD_PRAGMA = 'journal-fold:'
36
+ /** 표식을 찾아 볼 범위 — 바로 앞 주석 블록. 멀리 두면 무엇에 붙은 표식인지 알 수 없다. */
37
+ const PRAGMA_LOOKBACK = 900
38
+
39
+ /** 주석을 지운 소스 — 규율을 **설명하는 글**이 규율 위반으로 잡히지 않게. */
40
+ function stripComments(src: string): string {
41
+ return src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^[ \t]*\/\/.*$/gm, '')
42
+ }
43
+
44
+ function sources(dir: string): string[] {
45
+ let entries: string[]
46
+ try {
47
+ entries = readdirSync(dir)
48
+ } catch {
49
+ return []
50
+ }
51
+ return entries.flatMap(f => {
52
+ const p = join(dir, f)
53
+ return statSync(p).isDirectory() ? sources(p) : p.endsWith('.ts') && !p.endsWith('.d.ts') ? [p] : []
54
+ })
55
+ }
56
+
57
+ interface Site {
58
+ file: string
59
+ line: number
60
+ bounded: boolean
61
+ fold: boolean
62
+ }
63
+
64
+ /** 저널을 `find()` 로 읽는 자리들. 쓰기(`insert`/`save`)와 집계(`createQueryBuilder`)는 대상이 아니다. */
65
+ function journalFindSites(): Site[] {
66
+ const out: Site[] = []
67
+ for (const root of ROOTS) {
68
+ for (const f of sources(root)) {
69
+ const src = readFileSync(f, 'utf-8')
70
+ let at = src.indexOf(READ)
71
+ while (at >= 0) {
72
+ const tail = src.slice(at + READ.length, at + READ.length + 600)
73
+ /* 이 자리가 어떤 호출인가 — 첫 메서드만 본다. */
74
+ const call = /^\s*\.?\s*(find|findOne|count|countBy|createQueryBuilder|save|insert|delete|create)\b/.exec(tail)
75
+ if (call && call[1] === 'find') {
76
+ /* 옵션 객체 안에 상한이 있나 — 괄호 균형까지 보지 않고 넉넉히 잘라 본다(실패 시 폴드 표식이 받는다). */
77
+ const opts = tail.slice(0, 500)
78
+ const before = src.slice(Math.max(0, at - PRAGMA_LOOKBACK), at)
79
+ out.push({
80
+ file: f.slice(root.length + 1),
81
+ line: src.slice(0, at).split('\n').length,
82
+ bounded: /\btake\s*:/.test(opts),
83
+ fold: before.includes(FOLD_PRAGMA)
84
+ })
85
+ }
86
+ at = src.indexOf(READ, at + 1)
87
+ }
88
+ }
89
+ }
90
+ return out
91
+ }
92
+
93
+ test('저널을 통째로 읽는 자리는 폴드라고 밝히고 이유를 적는다', () => {
94
+ const bare = journalFindSites()
95
+ .filter(s => !s.bounded && !s.fold)
96
+ .map(s => `${s.file}:${s.line}`)
97
+
98
+ assert.deepEqual(
99
+ bare,
100
+ [],
101
+ `이유 없이 저널 전체를 읽는 자리 ${bare.length}곳:\n ${bare.join('\n ')}\n\n` +
102
+ `둘 중 하나로 만든다.\n` +
103
+ ` ① 화면이 쓰는 만큼만 읽는다면 \`take:\` 를 준다.\n` +
104
+ ` ② 사실을 접는 것이 목적이라면 바로 앞에 \`journal-fold:\` 주석으로 왜 행이 필요한지,\n` +
105
+ ` 상한이 없다면 그 예외가 없어질 조건까지 적는다.\n\n` +
106
+ `수 몇 개만 필요하다면 셋째 길이 맞다: 집계는 DB 가 한다(MIN/MAX/COUNT + createQueryBuilder).\n` +
107
+ `\`timeRange\` 가 min/max 두 수를 구하려고 275,882행·payload 131MB 를 4초마다 파싱했던 자리다.`
108
+ )
109
+ })
110
+
111
+ test('검사가 실제로 저널 읽기를 보고 있다 — 대상이 사라지면 초록이 거짓이 된다', () => {
112
+ const sites = journalFindSites()
113
+
114
+ assert.ok(
115
+ sites.length >= 3,
116
+ `저널 \`find()\` 를 ${sites.length}곳 찾았다 — 셋보다 적으면 호출 형태가 바뀐 것이다(리포지토리를 ` +
117
+ `변수에 담아 부르면 이 검사가 놓친다). 그때는 이 검사의 패턴을 함께 옮긴다.`
118
+ )
119
+ })
120
+
121
+ test('시각축은 집계로 답한다 — 행을 실어 오는 방식으로 되돌아가지 않는다', () => {
122
+ /*
123
+ * `timeRange` 만은 이름으로 못 박는다. 이 함수가 이 결함의 출처이고, 되돌아가도 **답은 맞기 때문에**
124
+ * 결과를 보는 시험으로는 잡히지 않는다.
125
+ */
126
+ const src = readFileSync(new URL('../server/engine/twin-engine.ts', import.meta.url).pathname, 'utf-8')
127
+ const at = src.indexOf('static async timeRange(')
128
+ assert.ok(at > 0, 'timeRange 를 찾지 못했다 — 이름이 바뀌면 이 검사도 함께 옮긴다')
129
+
130
+ const body = src.slice(at, at + 2500)
131
+ assert.ok(body.includes('MIN(e.eventTime)'), '가장 이른 시각은 DB 가 고른다')
132
+ assert.ok(body.includes('MAX(e.eventTime)'), '가장 늦은 시각도 그렇다')
133
+ assert.ok(!body.includes('getRepository(TwinEvent).find('), '저널을 행 단위로 실어 오지 않는다')
134
+ })
135
+
136
+ /*
137
+ * ── 세는 것도 읽는 것이다 (2026-08-22 실측) ─────────────────────────────────
138
+ * 위 규율은 **행을 실어 오는 것**을 봤다. 그런데 `getManyAndCount()` 는 행을 싣지 않으면서도 조건에
139
+ * 맞는 행을 **전부 센다**. 실측: 저널 358만 행인 트윈에서 한 번에 5.32초, 상한 1만이면 0.04초.
140
+ * 이력 화면을 열 때마다 그것을 물었고 그 5초 동안 이벤트 루프가 다른 요청을 처리하지 못했다.
141
+ *
142
+ * 그래서 저널에는 **상한 없는 세기를 두지 않는다.** 정확한 수를 모르는 것은 사실이므로 그때는
143
+ * 「모른다」로 낸다(목록 규약의 `capped`) — 상한값을 전체로 적으면 화면이 거짓을 말한다.
144
+ */
145
+ test('저널을 상한 없이 세지 않는다 — 답은 맞지만 비용이 틀린 자리', () => {
146
+ /*
147
+ * **주석과 남의 표를 세지 않는다.** 처음 만든 검사는 「파일이 `TwinEvent` 를 언급하고 어딘가에
148
+ * `.count(` 가 있으면」 이었다. 그래서 공간·구조·대표표현을 세는 자리와 **이 규율을 설명하는 주석**
149
+ * 까지 잡았다 — 거짓 경보는 참 경보를 죽인다. 규칙은 하나다: **저널 저장소에서 세는 자리**만 본다.
150
+ */
151
+ const offenders: string[] = []
152
+ for (const file of ROOTS.flatMap(r => sources(r))) {
153
+ const src = stripComments(readFileSync(file, 'utf-8'))
154
+ for (const m of src.matchAll(/getRepository\(TwinEvent\)|repository: getRepository\(TwinEvent\)/g)) {
155
+ const after = src.slice(m.index ?? 0, (m.index ?? 0) + 900)
156
+ const counted = after.match(/getManyAndCount\(|getCount\(|\.count\(/)
157
+ if (!counted) continue
158
+ /* 상한이 함께 적혀 있으면 규율을 지킨 것이다(상한까지만 센다). */
159
+ if (/take\(|COUNT_CAP/.test(after.slice(0, (counted.index ?? 0) + 80))) continue
160
+ offenders.push(`${file.split('/packages/')[1]} ${counted[0]}`)
161
+ }
162
+ }
163
+ assert.deepEqual(
164
+ offenders,
165
+ [],
166
+ `저널을 상한 없이 세는 자리가 있다 — 상한까지만 세고 넘으면 「모른다」로 낸다:\n ${offenders.join('\n ')}`
167
+ )
168
+ })
169
+
170
+ test('건수 상한이 값으로 선언돼 있고, 넘으면 전체를 비운다', () => {
171
+ const src = readFileSync(new URL('../server/service/twin-journal/twin-journal-query.ts', import.meta.url).pathname, 'utf-8')
172
+ assert.match(src, /const COUNT_CAP = JOURNAL_COUNT_CAP/, '상한은 저널 세기의 문에서 온다 — 값이 두 곳에 흩어지지 않게')
173
+ assert.match(src, /take\(COUNT_CAP \+ 1\)/, '상한을 넘었는지 알려면 하나 더 읽어야 한다')
174
+ /* 상한을 넘으면 **전체를 비운다** — 상한값을 전체로 적으면 화면이 거짓을 말한다. */
175
+ assert.match(src, /capped \? undefined : counted\.length/)
176
+ assert.match(src, /capped/, '넘었다는 사실을 함께 낸다(조용히 자르지 않는다)')
177
+ })
@@ -0,0 +1,133 @@
1
+ /*
2
+ * ★ 저널 보존 — **지우는 것은 선언이 있을 때만, 체크포인트가 대신할 수 있는 만큼만.**
3
+ *
4
+ * ── 왜 필요한가 (2026-08-22 실측) ───────────────────────────────────────────
5
+ * 개발 저널이 시간당 **581,794행 · 0.85GB**(하루 20GB)로 자랐다. 데모 MES 트윈 셋이 전체의 **97%** 를
6
+ * 만들고 지우는 것이 없었다. 같은 날 고친 질의들은 「그 크기에서도 빠르게」였고 **크기 자체를 줄이는
7
+ * 것은 없었다** — 그러면 며칠마다 같은 자리로 돌아온다.
8
+ *
9
+ * ── 그런데 저널을 지우는 것은 사실을 잃는 일이다 ────────────────────────────
10
+ * 그래서 이 시험이 세 규율을 고정한다. 규율이 깨지면 잃는 것이 조용하다 — 지워진 저널은 「없었던 일」과
11
+ * 구별되지 않는다.
12
+ *
13
+ * ① **선언이 없으면 아무것도 지우지 않는다.** 기본값은 없음이다.
14
+ * ② **체크포인트가 없는 인스턴스는 건드리지 않는다.** 그때 저널은 **유일한** 복구 수단이다
15
+ * (엔진 주석: 「만료 시 저널 replay 폴백」). 둘을 함께 잃으면 그 트윈의 상태는 되돌릴 수 없다.
16
+ * ③ **체크포인트 리비전보다 뒤는 지우지 않는다.** 그 뒤는 스냅샷이 대신하지 못한다.
17
+ *
18
+ * 그리고 보존 기간은 **스냅샷 TTL 보다 짧을 수 없다** — 짧으면 스냅샷이 살아 있는데 그것이 가리키는
19
+ * 앞쪽 저널이 없는 구간이 생기고, 시간여행·계보가 그 구간에서 조용히 빈다.
20
+ *
21
+ * ── 이 시험이 무엇을 재는가 ─────────────────────────────────────────────────
22
+ * DB 를 띄우지 않는다. 이 규율은 **조건을 어떻게 세우나**에 있고, 그것은 순수하게 확인할 수 있다 —
23
+ * 소스에서 그 조건 넷이 함께 있는지, 그리고 계약의 기본값이 무엇인지 본다. DB 위의 거동은 실서버에서
24
+ * 로그로 확인한다(지운 건수를 말하므로 볼 수 있다).
25
+ */
26
+ import { test } from 'node:test'
27
+ import assert from 'node:assert/strict'
28
+ import { readFileSync } from 'node:fs'
29
+ import { join } from 'node:path'
30
+
31
+ const ENGINE = join(import.meta.dirname, '..', 'server', 'engine', 'twin-engine.ts')
32
+ const src = readFileSync(ENGINE, 'utf8')
33
+
34
+ /** `pruneJournal` 본문만 — 다른 자리의 조건이 섞여 판정이 흐려지지 않게. */
35
+ function pruneBody(): string {
36
+ const at = src.indexOf('static async pruneJournal')
37
+ assert.ok(at > 0, 'pruneJournal 이 있어야 한다')
38
+ const rest = src.slice(at)
39
+ const end = rest.indexOf('\n /** 보존 정리 주기 기동')
40
+ return rest.slice(0, end === -1 ? 4000 : end)
41
+ }
42
+
43
+ test('선언이 없으면 아무것도 지우지 않는다 — 기본값은 없음이다', () => {
44
+ /* 계약의 기본값 — 여기에 숫자가 박히면 아무도 정하지 않은 채로 지워진다. */
45
+ assert.match(
46
+ src,
47
+ /static JOURNAL_RETENTION_DAYS\?: number = undefined/,
48
+ '보존 기간의 기본값은 undefined 여야 한다(조용히 지우는 것보다 조용히 쌓는 편이 낫다)'
49
+ )
50
+ /* 그리고 그 값이 없으면 즉시 돌아간다. */
51
+ assert.match(pruneBody(), /if \(!days \|\| days <= 0\) return/, '선언이 없으면 지우지 않고 돌아간다')
52
+ })
53
+
54
+ test('체크포인트가 없는 인스턴스는 건드리지 않는다 — 저널이 유일한 복구 수단이다', () => {
55
+ const body = pruneBody()
56
+ assert.match(body, /loadSnapshot\(/, '스냅샷을 확인해야 한다')
57
+ assert.match(body, /if \(!upTo\)[\s\S]{0,200}?continue/, '스냅샷이 없으면 그 인스턴스를 건너뛴다')
58
+ /* 건너뛴 사실을 세고 말한다 — 조용히 넘기면 「보존이 도는데 왜 안 줄어드나」가 된다. */
59
+ assert.match(body, /skipped\.push/, '건너뛴 것을 센다')
60
+ })
61
+
62
+ test('지우는 조건이 넷 다 있다 — 하나라도 빠지면 지나친 삭제가 된다', () => {
63
+ const body = pruneBody()
64
+ for (const [what, re] of [
65
+ ['도메인', /domain_id = :domainId/],
66
+ ['인스턴스', /instance_id = :instanceId/],
67
+ ['체크포인트 리비전 이하', /revision <= :upTo/],
68
+ ['보존 기간 이전(실제 시계)', /created_at < :cutoff/]
69
+ ] as [string, RegExp][]) {
70
+ assert.match(body, re, `${what} 조건이 있어야 한다`)
71
+ }
72
+ /* 시각을 모르는 옛 행은 「오래됐다」로 읽지 않는다. */
73
+ assert.match(body, /created_at IS NOT NULL/, '시각이 빈 행은 지우지 않는다')
74
+ /*
75
+ * **`event_time` 을 기준으로 쓰지 않는다** — 그것은 트윈의 시계다(실측: order-check 의 가장 늦은
76
+ * eventTime 이 2027-06-08 로 실제보다 10개월 앞). 보존은 저장 나이의 문제이고 그것은 실제 시계다.
77
+ */
78
+ assert.doesNotMatch(body, /event_time\s*<\s*:cutoff/, '도메인 시각을 보존 기준으로 쓰지 않는다')
79
+ })
80
+
81
+ test('도메인의 정책이 프로세스 기본값보다 먼저다 — 그리고 엔진은 그것이 어디서 오는지 모른다', () => {
82
+ const body = pruneBody()
83
+ /*
84
+ * 보존 기간은 **테넌트의 정책**이다(한 고객은 30일, 다른 고객은 7일). 그래서 도메인에 먼저 묻고,
85
+ * 답이 없을 때만 프로세스 기본값으로 내려간다. 순서가 뒤집히면 설정 화면에서 고친 값이 먹지 않는다.
86
+ */
87
+ assert.match(body, /this\.retentionDaysOf\(domainId\)/, '도메인에 먼저 묻는다')
88
+ assert.match(body, /perDomain \?\? this\.JOURNAL_RETENTION_DAYS/, '도메인 답이 없을 때만 프로세스 기본값이다')
89
+ /*
90
+ * 그리고 그 답이 **어디서 오는지 엔진은 모른다** — 시임일 뿐이다. 엔진이 `Setting` 을 직접 읽으면
91
+ * 커널의 호스트가 화면 관심사를 알게 된다(방향이 거꾸로다).
92
+ */
93
+ assert.doesNotMatch(src, /from '@things-factory\/setting-base'/, '엔진은 Setting 을 알지 않는다')
94
+ })
95
+
96
+ test('보존 기간은 스냅샷 TTL 보다 짧아질 수 없다 — 짧으면 앞쪽이 비는 구간이 생긴다', () => {
97
+ const body = pruneBody()
98
+ assert.match(body, /SNAPSHOT_TTL_S \/ 86400/, '스냅샷 TTL 을 하한으로 삼아야 한다')
99
+ assert.match(body, /Math\.max\(days, minDays\)/, '선언이 그보다 짧으면 올린다')
100
+ assert.match(body, /twinWarn\(/, '올렸다는 사실을 말한다(조용히 바꾸지 않는다)')
101
+ })
102
+
103
+ test('지운 것을 말한다 — 조용히 줄어든 저널은 없었던 일과 구별되지 않는다', () => {
104
+ const body = pruneBody()
105
+ assert.match(body, /journal pruned/, '지운 건수를 로그에 남긴다')
106
+ assert.match(body, /revision ≤/, '어디까지 지웠는지 함께 남긴다')
107
+ })
108
+
109
+ test('raw SQL 을 쓰지 않는다 — 드라이버 다섯을 다 지나야 한다', () => {
110
+ const body = pruneBody()
111
+ assert.doesNotMatch(body, /\.query\(/, '드라이버 방언을 직접 적지 않는다')
112
+ assert.match(body, /createQueryBuilder\(\)[\s\S]{0,80}\.delete\(\)/, '조건 삭제는 쿼리빌더로 한다')
113
+ })
114
+
115
+ test('선언도 없고 물을 길도 없으면 주기 정리를 걸지 않는다', () => {
116
+ const at = src.indexOf('static startRetentionLoop')
117
+ assert.ok(at > 0, 'startRetentionLoop 이 있어야 한다')
118
+ const body = src.slice(at, at + 900)
119
+ /*
120
+ * 조건이 **둘**이다. 프로세스 기본값(`JOURNAL_RETENTION_DAYS`)만 보면 안 된다 — 도메인마다 다른 값을
121
+ * 앱이 답하는 시임(`retentionDaysOf`)이 있고, 그것이 심겨 있으면 기본값이 없어도 걸어야 한다
122
+ * (그 도메인이 자기 값을 가질 수 있다). 반대로 **둘 다 없으면 물을 곳이 없으므로** 걸지 않는다.
123
+ *
124
+ * 기본값만 보게 두면 시임만 심은 배포에서 정리가 조용히 돌지 않는다 — 「보존을 켰는데 왜 안 줄어드나」.
125
+ */
126
+ assert.match(
127
+ body,
128
+ /if \(!this\.JOURNAL_RETENTION_DAYS && !this\.retentionDaysOf\) return/,
129
+ '기본값도 없고 도메인 시임도 없을 때만 타이머를 걸지 않는다'
130
+ )
131
+ /* 타이머가 프로세스를 붙잡지 않게 — 지우는 일이 종료를 막을 이유가 없다. */
132
+ assert.match(body, /unref/, '타이머는 unref 한다')
133
+ })