@things-factory/headless-twin 10.0.14 → 10.0.17

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 (478) hide show
  1. package/dist-server/engine/attention-digest.d.ts +4 -4
  2. package/dist-server/engine/attention-digest.js +7 -7
  3. package/dist-server/engine/attention-digest.js.map +1 -1
  4. package/dist-server/engine/canonical-ingest.d.ts +11 -2
  5. package/dist-server/engine/canonical-ingest.js +137 -8
  6. package/dist-server/engine/canonical-ingest.js.map +1 -1
  7. package/dist-server/engine/command-routing.js +1 -1
  8. package/dist-server/engine/command-routing.js.map +1 -1
  9. package/dist-server/engine/declared-stimulus.js +1 -1
  10. package/dist-server/engine/declared-stimulus.js.map +1 -1
  11. package/dist-server/engine/energy-topology.js +1 -1
  12. package/dist-server/engine/energy-topology.js.map +1 -1
  13. package/dist-server/engine/ingest-dedupe.d.ts +38 -0
  14. package/dist-server/engine/ingest-dedupe.js +145 -0
  15. package/dist-server/engine/ingest-dedupe.js.map +1 -0
  16. package/dist-server/engine/ingest-health.d.ts +124 -9
  17. package/dist-server/engine/ingest-health.js +169 -15
  18. package/dist-server/engine/ingest-health.js.map +1 -1
  19. package/dist-server/engine/integration-probes.d.ts +3 -3
  20. package/dist-server/engine/integration-probes.js +6 -6
  21. package/dist-server/engine/integration-probes.js.map +1 -1
  22. package/dist-server/engine/kpi-baseline.js +4 -4
  23. package/dist-server/engine/kpi-baseline.js.map +1 -1
  24. package/dist-server/engine/kpi-fold.d.ts +122 -12
  25. package/dist-server/engine/kpi-fold.js +247 -18
  26. package/dist-server/engine/kpi-fold.js.map +1 -1
  27. package/dist-server/engine/kpi-query.d.ts +11 -11
  28. package/dist-server/engine/kpi-query.js +185 -28
  29. package/dist-server/engine/kpi-query.js.map +1 -1
  30. package/dist-server/engine/live-feed-registry.d.ts +1 -1
  31. package/dist-server/engine/live-feed-registry.js +1 -1
  32. package/dist-server/engine/live-feed-registry.js.map +1 -1
  33. package/dist-server/engine/load-meter.d.ts +1 -1
  34. package/dist-server/engine/load-meter.js.map +1 -1
  35. package/dist-server/engine/local-declarations.d.ts +7 -7
  36. package/dist-server/engine/local-declarations.js +9 -9
  37. package/dist-server/engine/local-declarations.js.map +1 -1
  38. package/dist-server/engine/log.d.ts +1 -1
  39. package/dist-server/engine/log.js +1 -1
  40. package/dist-server/engine/log.js.map +1 -1
  41. package/dist-server/engine/measured-estimator.js +1 -1
  42. package/dist-server/engine/measured-estimator.js.map +1 -1
  43. package/dist-server/engine/measured-yield.js +1 -1
  44. package/dist-server/engine/measured-yield.js.map +1 -1
  45. package/dist-server/engine/model-basis.d.ts +1 -1
  46. package/dist-server/engine/model-basis.js +1 -1
  47. package/dist-server/engine/model-basis.js.map +1 -1
  48. package/dist-server/engine/model-gap.d.ts +3 -3
  49. package/dist-server/engine/model-gap.js +1 -1
  50. package/dist-server/engine/model-gap.js.map +1 -1
  51. package/dist-server/engine/model-vocabulary.js +1 -1
  52. package/dist-server/engine/model-vocabulary.js.map +1 -1
  53. package/dist-server/engine/oee-accumulator.d.ts +4 -4
  54. package/dist-server/engine/oee-accumulator.js +3 -3
  55. package/dist-server/engine/oee-accumulator.js.map +1 -1
  56. package/dist-server/engine/property-effects.js +2 -2
  57. package/dist-server/engine/property-effects.js.map +1 -1
  58. package/dist-server/engine/read-time.d.ts +22 -0
  59. package/dist-server/engine/read-time.js +81 -0
  60. package/dist-server/engine/read-time.js.map +1 -0
  61. package/dist-server/engine/restart-policy.js +1 -1
  62. package/dist-server/engine/restart-policy.js.map +1 -1
  63. package/dist-server/engine/runtime-key.js +1 -1
  64. package/dist-server/engine/runtime-key.js.map +1 -1
  65. package/dist-server/engine/spec-coverage.d.ts +2 -2
  66. package/dist-server/engine/spec-coverage.js +1 -1
  67. package/dist-server/engine/spec-coverage.js.map +1 -1
  68. package/dist-server/engine/stage-path.d.ts +4 -4
  69. package/dist-server/engine/stage-path.js +1 -1
  70. package/dist-server/engine/stage-path.js.map +1 -1
  71. package/dist-server/engine/state-axes.d.ts +1 -1
  72. package/dist-server/engine/state-axes.js +2 -2
  73. package/dist-server/engine/state-axes.js.map +1 -1
  74. package/dist-server/engine/travel-estimator.d.ts +2 -2
  75. package/dist-server/engine/travel-estimator.js +1 -1
  76. package/dist-server/engine/travel-estimator.js.map +1 -1
  77. package/dist-server/engine/twin-engine.d.ts +243 -53
  78. package/dist-server/engine/twin-engine.js +690 -191
  79. package/dist-server/engine/twin-engine.js.map +1 -1
  80. package/dist-server/engine/warm-start.d.ts +2 -2
  81. package/dist-server/engine/warm-start.js +5 -5
  82. package/dist-server/engine/warm-start.js.map +1 -1
  83. package/dist-server/index.js +1 -1
  84. package/dist-server/index.js.map +1 -1
  85. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js +1 -1
  86. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js.map +1 -1
  87. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js +3 -3
  88. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js.map +1 -1
  89. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js +2 -2
  90. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js.map +1 -1
  91. package/dist-server/migrations/1786300000000-IndexEventTypeByTime.d.ts +5 -0
  92. package/dist-server/migrations/1786300000000-IndexEventTypeByTime.js +54 -0
  93. package/dist-server/migrations/1786300000000-IndexEventTypeByTime.js.map +1 -0
  94. package/dist-server/migrations/1786400000000-IndexEventCreatedAt.d.ts +5 -0
  95. package/dist-server/migrations/1786400000000-IndexEventCreatedAt.js +44 -0
  96. package/dist-server/migrations/1786400000000-IndexEventCreatedAt.js.map +1 -0
  97. package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.d.ts +6 -0
  98. package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.js +84 -0
  99. package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.js.map +1 -0
  100. package/dist-server/migrations/index.js +7 -1
  101. package/dist-server/migrations/index.js.map +1 -1
  102. package/dist-server/routes.d.ts +1 -0
  103. package/dist-server/routes.js +69 -5
  104. package/dist-server/routes.js.map +1 -1
  105. package/dist-server/service/index.d.ts +2 -2
  106. package/dist-server/service/index.js +27 -24
  107. package/dist-server/service/index.js.map +1 -1
  108. package/dist-server/service/reference/control-routing.d.ts +1 -1
  109. package/dist-server/service/reference/control-routing.js +1 -1
  110. package/dist-server/service/reference/control-routing.js.map +1 -1
  111. package/dist-server/service/reference/discovery-result.d.ts +2 -2
  112. package/dist-server/service/reference/discovery-result.js +3 -3
  113. package/dist-server/service/reference/discovery-result.js.map +1 -1
  114. package/dist-server/service/reference/hook-contract.d.ts +43 -0
  115. package/dist-server/service/reference/hook-contract.js +59 -0
  116. package/dist-server/service/reference/hook-contract.js.map +1 -0
  117. package/dist-server/service/reference/ingest-space.d.ts +1 -1
  118. package/dist-server/service/reference/ingest-space.js +4 -4
  119. package/dist-server/service/reference/ingest-space.js.map +1 -1
  120. package/dist-server/service/reference/knob-defaults.d.ts +1 -1
  121. package/dist-server/service/reference/knob-defaults.js +4 -4
  122. package/dist-server/service/reference/knob-defaults.js.map +1 -1
  123. package/dist-server/service/reference/reference-adapter.d.ts +140 -8
  124. package/dist-server/service/reference/reference-adapter.js +27 -0
  125. package/dist-server/service/reference/reference-adapter.js.map +1 -1
  126. package/dist-server/service/reference/reference-hook.d.ts +20 -0
  127. package/dist-server/service/reference/reference-hook.js +133 -0
  128. package/dist-server/service/reference/reference-hook.js.map +1 -0
  129. package/dist-server/service/reference/reference-live.d.ts +2 -2
  130. package/dist-server/service/reference/reference-live.js +88 -13
  131. package/dist-server/service/reference/reference-live.js.map +1 -1
  132. package/dist-server/service/reference/reference-master.d.ts +14 -14
  133. package/dist-server/service/reference/reference-master.js +34 -12
  134. package/dist-server/service/reference/reference-master.js.map +1 -1
  135. package/dist-server/service/reference/reference-resolver.d.ts +1 -1
  136. package/dist-server/service/reference/reference-resolver.js +10 -10
  137. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  138. package/dist-server/service/reference/twin-reference.d.ts +3 -3
  139. package/dist-server/service/reference/twin-reference.js.map +1 -1
  140. package/dist-server/service/twin-attention/twin-attention-query.d.ts +1 -1
  141. package/dist-server/service/twin-attention/twin-attention-query.js +1 -1
  142. package/dist-server/service/twin-attention/twin-attention-query.js.map +1 -1
  143. package/dist-server/service/twin-audit/command-audit.d.ts +1 -1
  144. package/dist-server/service/twin-audit/command-audit.js.map +1 -1
  145. package/dist-server/service/twin-audit/twin-audit-event.d.ts +1 -1
  146. package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -1
  147. package/dist-server/service/twin-audit/twin-audit-query.js +1 -1
  148. package/dist-server/service/twin-audit/twin-audit-query.js.map +1 -1
  149. package/dist-server/service/twin-control/twin-control-mutation.js +1 -1
  150. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  151. package/dist-server/service/twin-event/twin-event-keys.d.ts +22 -2
  152. package/dist-server/service/twin-event/twin-event-keys.js +37 -5
  153. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  154. package/dist-server/service/twin-event/twin-event-type.d.ts +1 -0
  155. package/dist-server/service/twin-event/twin-event-type.js +8 -1
  156. package/dist-server/service/twin-event/twin-event-type.js.map +1 -1
  157. package/dist-server/service/twin-event/twin-event.d.ts +2 -2
  158. package/dist-server/service/twin-event/twin-event.js +32 -4
  159. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  160. package/dist-server/service/twin-forecast/forecast-metrics.js +1 -1
  161. package/dist-server/service/twin-forecast/forecast-metrics.js.map +1 -1
  162. package/dist-server/service/twin-forecast/gap-analytics.d.ts +1 -1
  163. package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
  164. package/dist-server/service/twin-forecast/twin-forecast-query.js +29 -11
  165. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  166. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.d.ts +1 -1
  167. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js +1 -1
  168. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js.map +1 -1
  169. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js +1 -1
  170. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js.map +1 -1
  171. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  172. package/dist-server/service/twin-journal/event-type-split-shape.d.ts +50 -0
  173. package/dist-server/service/twin-journal/event-type-split-shape.js +111 -0
  174. package/dist-server/service/twin-journal/event-type-split-shape.js.map +1 -0
  175. package/dist-server/service/twin-journal/event-type-split.d.ts +15 -0
  176. package/dist-server/service/twin-journal/event-type-split.js +102 -0
  177. package/dist-server/service/twin-journal/event-type-split.js.map +1 -0
  178. package/dist-server/service/twin-journal/journal-order.d.ts +15 -0
  179. package/dist-server/service/twin-journal/journal-order.js +17 -0
  180. package/dist-server/service/twin-journal/journal-order.js.map +1 -0
  181. package/dist-server/service/twin-journal/twin-journal-query.d.ts +5 -5
  182. package/dist-server/service/twin-journal/twin-journal-query.js +232 -21
  183. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  184. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +27 -1
  185. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +59 -3
  186. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  187. package/dist-server/service/twin-metrics/twin-metrics-query.js +14 -1
  188. package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
  189. package/dist-server/service/twin-model/axis-journal-evidence.js +1 -1
  190. package/dist-server/service/twin-model/axis-journal-evidence.js.map +1 -1
  191. package/dist-server/service/twin-model/epcis-coverage.js +36 -1
  192. package/dist-server/service/twin-model/epcis-coverage.js.map +1 -1
  193. package/dist-server/service/twin-model/iec61850-coverage.js +1 -1
  194. package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -1
  195. package/dist-server/service/twin-model/isa95-coverage.d.ts +15 -2
  196. package/dist-server/service/twin-model/isa95-coverage.js +32 -8
  197. package/dist-server/service/twin-model/isa95-coverage.js.map +1 -1
  198. package/dist-server/service/twin-model/item-ref.js +2 -2
  199. package/dist-server/service/twin-model/item-ref.js.map +1 -1
  200. package/dist-server/service/twin-model/name-index.d.ts +1 -1
  201. package/dist-server/service/twin-model/name-index.js +1 -1
  202. package/dist-server/service/twin-model/name-index.js.map +1 -1
  203. package/dist-server/service/twin-model/project-structure.d.ts +1 -1
  204. package/dist-server/service/twin-model/project-structure.js +3 -3
  205. package/dist-server/service/twin-model/project-structure.js.map +1 -1
  206. package/dist-server/service/twin-model/twin-equipment.d.ts +1 -1
  207. package/dist-server/service/twin-model/twin-equipment.js +1 -1
  208. package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
  209. package/dist-server/service/twin-model/twin-lineage-query.js +16 -5
  210. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
  211. package/dist-server/service/twin-model/twin-location.d.ts +1 -1
  212. package/dist-server/service/twin-model/twin-location.js.map +1 -1
  213. package/dist-server/service/twin-model/twin-model-item-query.d.ts +3 -3
  214. package/dist-server/service/twin-model/twin-model-item-query.js +14 -14
  215. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
  216. package/dist-server/service/twin-model/twin-model-mutation.d.ts +2 -2
  217. package/dist-server/service/twin-model/twin-model-mutation.js +4 -4
  218. package/dist-server/service/twin-model/twin-model-mutation.js.map +1 -1
  219. package/dist-server/service/twin-model/twin-model-query.js +117 -28
  220. package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
  221. package/dist-server/service/twin-model/twin-model-tree-query.js +22 -0
  222. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
  223. package/dist-server/service/twin-model/twin-operation.d.ts +1 -1
  224. package/dist-server/service/twin-model/twin-operation.js +1 -1
  225. package/dist-server/service/twin-model/twin-operation.js.map +1 -1
  226. package/dist-server/service/twin-space/move-space.js +2 -2
  227. package/dist-server/service/twin-space/move-space.js.map +1 -1
  228. package/dist-server/service/twin-space/representation-role.d.ts +2 -0
  229. package/dist-server/service/twin-space/representation-role.js +18 -0
  230. package/dist-server/service/twin-space/representation-role.js.map +1 -0
  231. package/dist-server/service/twin-space/space-integrity.d.ts +1 -1
  232. package/dist-server/service/twin-space/space-integrity.js.map +1 -1
  233. package/dist-server/service/twin-space/twin-space-representation.d.ts +2 -0
  234. package/dist-server/service/twin-space/twin-space-representation.js +16 -0
  235. package/dist-server/service/twin-space/twin-space-representation.js.map +1 -1
  236. package/dist-server/service/twin-space/twin-space-resolver.d.ts +2 -2
  237. package/dist-server/service/twin-space/twin-space-resolver.js +36 -7
  238. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  239. package/dist-server/service/twin-structure/twin-structure.js +3 -3
  240. package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
  241. package/dist-server/service/twin-subject/event-subjects.d.ts +43 -0
  242. package/dist-server/service/twin-subject/event-subjects.js +137 -0
  243. package/dist-server/service/twin-subject/event-subjects.js.map +1 -0
  244. package/dist-server/service/twin-subject/index.d.ts +9 -0
  245. package/dist-server/service/twin-subject/index.js +16 -0
  246. package/dist-server/service/twin-subject/index.js.map +1 -0
  247. package/dist-server/service/twin-subject/item-resolver.d.ts +10 -0
  248. package/dist-server/service/twin-subject/item-resolver.js +93 -0
  249. package/dist-server/service/twin-subject/item-resolver.js.map +1 -0
  250. package/dist-server/service/twin-subject/subject-query-shape.d.ts +22 -0
  251. package/dist-server/service/twin-subject/subject-query-shape.js +88 -0
  252. package/dist-server/service/twin-subject/subject-query-shape.js.map +1 -0
  253. package/dist-server/service/twin-subject/twin-subject-backfill.d.ts +23 -0
  254. package/dist-server/service/twin-subject/twin-subject-backfill.js +159 -0
  255. package/dist-server/service/twin-subject/twin-subject-backfill.js.map +1 -0
  256. package/dist-server/service/twin-subject/twin-subject-event.d.ts +14 -0
  257. package/dist-server/service/twin-subject/twin-subject-event.js +111 -0
  258. package/dist-server/service/twin-subject/twin-subject-event.js.map +1 -0
  259. package/dist-server/service/twin-subject/twin-subject-fast-path.d.ts +39 -0
  260. package/dist-server/service/twin-subject/twin-subject-fast-path.js +295 -0
  261. package/dist-server/service/twin-subject/twin-subject-fast-path.js.map +1 -0
  262. package/dist-server/service/twin-subject/twin-subject-query.d.ts +27 -0
  263. package/dist-server/service/twin-subject/twin-subject-query.js +232 -0
  264. package/dist-server/service/twin-subject/twin-subject-query.js.map +1 -0
  265. package/dist-server/service/twin-subject/twin-subject-writer.d.ts +27 -0
  266. package/dist-server/service/twin-subject/twin-subject-writer.js +166 -0
  267. package/dist-server/service/twin-subject/twin-subject-writer.js.map +1 -0
  268. package/dist-server/service/twin-target/twin-target-resolver.js +1 -1
  269. package/dist-server/service/twin-target/twin-target-resolver.js.map +1 -1
  270. package/dist-shared/entity-delta.d.ts +1 -1
  271. package/dist-shared/entity-delta.js +3 -3
  272. package/dist-shared/entity-delta.js.map +1 -1
  273. package/dist-shared/touched-items.js +2 -2
  274. package/dist-shared/touched-items.js.map +1 -1
  275. package/dist-shared/twin-level.js +1 -1
  276. package/dist-shared/twin-level.js.map +1 -1
  277. package/package.json +3 -3
  278. package/server/engine/attention-digest.ts +9 -9
  279. package/server/engine/canonical-ingest.ts +170 -13
  280. package/server/engine/command-routing.ts +1 -1
  281. package/server/engine/declared-stimulus.ts +1 -1
  282. package/server/engine/energy-topology.ts +1 -1
  283. package/server/engine/ingest-dedupe.ts +149 -0
  284. package/server/engine/ingest-health.ts +234 -20
  285. package/server/engine/integration-probes.ts +7 -7
  286. package/server/engine/kpi-baseline.ts +4 -4
  287. package/server/engine/kpi-fold.ts +395 -27
  288. package/server/engine/kpi-query.ts +212 -46
  289. package/server/engine/live-feed-registry.ts +1 -1
  290. package/server/engine/load-meter.ts +1 -1
  291. package/server/engine/local-declarations.ts +13 -13
  292. package/server/engine/log.ts +1 -1
  293. package/server/engine/measured-estimator.ts +1 -1
  294. package/server/engine/measured-yield.ts +1 -1
  295. package/server/engine/model-basis.ts +2 -2
  296. package/server/engine/model-gap.ts +3 -3
  297. package/server/engine/model-vocabulary.ts +1 -1
  298. package/server/engine/oee-accumulator.ts +4 -4
  299. package/server/engine/property-effects.ts +2 -2
  300. package/server/engine/read-time.ts +77 -0
  301. package/server/engine/restart-policy.ts +1 -1
  302. package/server/engine/runtime-key.ts +1 -1
  303. package/server/engine/spec-coverage.ts +3 -3
  304. package/server/engine/stage-path.ts +5 -5
  305. package/server/engine/state-axes.ts +2 -2
  306. package/server/engine/travel-estimator.ts +2 -2
  307. package/server/engine/twin-engine.ts +764 -201
  308. package/server/engine/warm-start.ts +6 -6
  309. package/server/index.ts +1 -1
  310. package/server/migrations/1786000000000-RenameTwinInstanceBoardToModel.ts +1 -1
  311. package/server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.ts +3 -3
  312. package/server/migrations/1786200000000-CarryLiveFeedCursor.ts +2 -2
  313. package/server/migrations/1786300000000-IndexEventTypeByTime.ts +48 -0
  314. package/server/migrations/1786400000000-IndexEventCreatedAt.ts +38 -0
  315. package/server/migrations/1786500000000-CreateTwinSubjectEvents.ts +93 -0
  316. package/server/migrations/index.ts +7 -1
  317. package/server/routes.ts +68 -5
  318. package/server/service/index.ts +4 -1
  319. package/server/service/reference/control-routing.ts +1 -1
  320. package/server/service/reference/discovery-result.ts +3 -3
  321. package/server/service/reference/hook-contract.ts +103 -0
  322. package/server/service/reference/ingest-space.ts +4 -4
  323. package/server/service/reference/knob-defaults.ts +4 -4
  324. package/server/service/reference/reference-adapter.ts +166 -9
  325. package/server/service/reference/reference-hook.ts +145 -0
  326. package/server/service/reference/reference-live.ts +92 -14
  327. package/server/service/reference/reference-master.ts +39 -17
  328. package/server/service/reference/reference-resolver.ts +10 -10
  329. package/server/service/reference/twin-reference.ts +3 -3
  330. package/server/service/twin-attention/twin-attention-query.ts +1 -1
  331. package/server/service/twin-audit/command-audit.ts +1 -1
  332. package/server/service/twin-audit/twin-audit-event.ts +2 -2
  333. package/server/service/twin-audit/twin-audit-query.ts +1 -1
  334. package/server/service/twin-control/twin-control-mutation.ts +1 -1
  335. package/server/service/twin-event/twin-event-keys.ts +37 -5
  336. package/server/service/twin-event/twin-event-type.ts +18 -1
  337. package/server/service/twin-event/twin-event.ts +32 -6
  338. package/server/service/twin-forecast/forecast-metrics.ts +1 -1
  339. package/server/service/twin-forecast/gap-analytics.ts +1 -1
  340. package/server/service/twin-forecast/twin-forecast-query.ts +30 -11
  341. package/server/service/twin-ingest-window/twin-ingest-window-query.ts +1 -1
  342. package/server/service/twin-ingest-window/twin-ingest-window-writer.ts +1 -1
  343. package/server/service/twin-instance/twin-instance.ts +4 -4
  344. package/server/service/twin-journal/event-type-split-shape.ts +112 -0
  345. package/server/service/twin-journal/event-type-split.ts +144 -0
  346. package/server/service/twin-journal/journal-order.ts +20 -0
  347. package/server/service/twin-journal/twin-journal-query.ts +224 -20
  348. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +57 -3
  349. package/server/service/twin-metrics/twin-metrics-query.ts +14 -1
  350. package/server/service/twin-model/axis-journal-evidence.ts +1 -1
  351. package/server/service/twin-model/epcis-coverage.ts +36 -1
  352. package/server/service/twin-model/iec61850-coverage.ts +1 -1
  353. package/server/service/twin-model/isa95-coverage.ts +45 -8
  354. package/server/service/twin-model/item-ref.ts +2 -2
  355. package/server/service/twin-model/name-index.ts +1 -1
  356. package/server/service/twin-model/project-structure.ts +4 -4
  357. package/server/service/twin-model/twin-equipment.ts +2 -2
  358. package/server/service/twin-model/twin-lineage-query.ts +16 -5
  359. package/server/service/twin-model/twin-location.ts +1 -1
  360. package/server/service/twin-model/twin-model-item-query.ts +14 -14
  361. package/server/service/twin-model/twin-model-mutation.ts +4 -4
  362. package/server/service/twin-model/twin-model-query.ts +117 -28
  363. package/server/service/twin-model/twin-model-tree-query.ts +25 -1
  364. package/server/service/twin-model/twin-operation.ts +2 -2
  365. package/server/service/twin-space/move-space.ts +3 -3
  366. package/server/service/twin-space/representation-role.ts +15 -0
  367. package/server/service/twin-space/space-integrity.ts +1 -1
  368. package/server/service/twin-space/twin-space-representation.ts +37 -0
  369. package/server/service/twin-space/twin-space-resolver.ts +39 -7
  370. package/server/service/twin-structure/twin-structure.ts +3 -3
  371. package/server/service/twin-subject/event-subjects.ts +155 -0
  372. package/server/service/twin-subject/index.ts +10 -0
  373. package/server/service/twin-subject/item-resolver.ts +92 -0
  374. package/server/service/twin-subject/subject-query-shape.ts +84 -0
  375. package/server/service/twin-subject/twin-subject-backfill.ts +196 -0
  376. package/server/service/twin-subject/twin-subject-event.ts +101 -0
  377. package/server/service/twin-subject/twin-subject-fast-path.ts +346 -0
  378. package/server/service/twin-subject/twin-subject-query.ts +210 -0
  379. package/server/service/twin-subject/twin-subject-writer.ts +189 -0
  380. package/server/service/twin-target/twin-target-resolver.ts +1 -1
  381. package/shared/entity-delta.ts +3 -3
  382. package/shared/touched-items.ts +2 -2
  383. package/shared/twin-level.ts +1 -1
  384. package/test/adopt-structure-live.test.ts +3 -3
  385. package/test/aggregate-not-findone.test.ts +112 -0
  386. package/test/attention-digest.test.ts +6 -6
  387. package/test/axis-read.test.ts +2 -2
  388. package/test/boot-resume.test.ts +20 -7
  389. package/test/broadcast-period.test.ts +2 -2
  390. package/test/broadcast-queue-overflow.test.ts +244 -0
  391. package/test/canonical-ingest-vocabularies.test.ts +179 -7
  392. package/test/canonical-quantity-door.test.ts +2 -2
  393. package/test/checkpoint-refuses-empty.test.ts +2 -2
  394. package/test/connector-capability-declaration.test.ts +88 -0
  395. package/test/control-capability.test.ts +1 -1
  396. package/test/cursor-stall-not-read-failure.test.ts +2 -2
  397. package/test/declaration-reaches-model.test.ts +5 -5
  398. package/test/declared-stimulus.test.ts +4 -4
  399. package/test/discovery-result.test.ts +2 -2
  400. package/test/entity-delta.test.ts +4 -4
  401. package/test/event-subjects.test.ts +147 -0
  402. package/test/event-time-column.test.ts +3 -3
  403. package/test/event-type-split.test.ts +155 -0
  404. package/test/fold-gap-speaks.test.ts +53 -0
  405. package/test/forecast-metrics.test.ts +1 -1
  406. package/test/forecast-tuning.test.ts +1 -1
  407. package/test/ingest-health-engine.test.ts +2 -2
  408. package/test/ingest-health-wiring.test.ts +7 -5
  409. package/test/ingest-health.test.ts +6 -6
  410. package/test/ingest-history.test.ts +2 -2
  411. package/test/ingest-idempotent.test.ts +132 -0
  412. package/test/ingest-space.test.ts +3 -3
  413. package/test/integration-probes.test.ts +3 -3
  414. package/test/integration-runner.test.ts +1 -1
  415. package/test/item-ref.test.ts +1 -1
  416. package/test/journal-order-axis.test.ts +80 -0
  417. package/test/journal-read-discipline.test.ts +16 -6
  418. package/test/journal-retention.test.ts +171 -29
  419. package/test/journal-sort-axis.test.ts +1 -1
  420. package/test/journal-write-door.test.ts +3 -3
  421. package/test/journal-write-trend.test.ts +1 -1
  422. package/test/kernel-kind-guard.test.ts +3 -3
  423. package/test/knob-defaults.test.ts +1 -1
  424. package/test/kpi-baseline-db.test.ts +2 -2
  425. package/test/kpi-fold.test.ts +185 -14
  426. package/test/kpi-query-bench.test.ts +3 -3
  427. package/test/live-cursor-wiring.test.ts +6 -6
  428. package/test/live-kernel-facts.test.ts +11 -11
  429. package/test/live-mirror-parity.test.ts +3 -3
  430. package/test/load-meter.test.ts +1 -1
  431. package/test/local-declarations.test.ts +10 -10
  432. package/test/log-stamp.test.ts +1 -1
  433. package/test/measured-yield.test.ts +2 -2
  434. package/test/merge-streams.test.ts +70 -0
  435. package/test/mirror-resumes-from-checkpoint.test.ts +13 -13
  436. package/test/model-basis.test.ts +2 -2
  437. package/test/model-gap.test.ts +4 -4
  438. package/test/model-vocabulary.test.ts +2 -2
  439. package/test/move-space.test.ts +1 -1
  440. package/test/oee-accumulator.test.ts +2 -2
  441. package/test/operational-vocabulary.test.ts +2 -2
  442. package/test/project-structure-db.test.ts +2 -2
  443. package/test/projection-reaches-screen.test.ts +2 -2
  444. package/test/property-effects.test.ts +3 -3
  445. package/test/read-failure-ledger.test.ts +106 -0
  446. package/test/read-failure-visible.test.ts +7 -7
  447. package/test/read-time.test.ts +118 -0
  448. package/test/reference-grounding.test.ts +1 -1
  449. package/test/reference-hook.test.ts +86 -0
  450. package/test/registry-key-guard.test.ts +1 -1
  451. package/test/rename-twin.test.ts +81 -0
  452. package/test/restart-policy.test.ts +3 -3
  453. package/test/resync-origin-site.test.ts +3 -3
  454. package/test/revision-axis.test.ts +14 -8
  455. package/test/runtime-key.test.ts +3 -3
  456. package/test/snapshot-freshness.test.ts +4 -4
  457. package/test/source-outcome-audit.test.ts +1 -1
  458. package/test/space-integrity.test.ts +1 -1
  459. package/test/space-representation.test.ts +66 -0
  460. package/test/space-scope-includes-stopped.test.ts +55 -0
  461. package/test/spec-coverage.test.ts +1 -1
  462. package/test/standard-coverage.test.ts +43 -1
  463. package/test/status-tally.test.ts +1 -1
  464. package/test/structure-revision-db.test.ts +7 -7
  465. package/test/subject-fast-path.test.ts +148 -0
  466. package/test/subject-read-model.test.ts +175 -0
  467. package/test/touched-items.test.ts +2 -2
  468. package/test/twin-audit.test.ts +1 -1
  469. package/test/twin-event-keys.test.ts +49 -3
  470. package/test/twin-model-item-db.test.ts +5 -5
  471. package/test/twin-model-tree-db.test.ts +35 -2
  472. package/test/twin-origin-resync.test.ts +1 -1
  473. package/test/warm-start-seam.test.ts +1 -1
  474. package/test/warm-start.test.ts +6 -6
  475. package/test/withheld-door.test.ts +95 -0
  476. package/test/yield-loop.test.ts +4 -4
  477. package/tsconfig.shared.tsbuildinfo +1 -1
  478. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,70 @@
1
+ /*
2
+ * ★ 나눠 묻고 합치는 규율 — 갈래마다 상한까지 받아야 합친 결과가 정확하다.
3
+ *
4
+ * ── 왜 이 시험이 있나 (2026-08-28) ──────────────────────────────────────────
5
+ * 「종류 IN + 시각순」은 색인을 못 쓴다. 그래서 종류를 하나씩 정확일치로 묻고 합치는데, 그때 함정이
6
+ * 하나 있다 — UX 레인이 구현 전에 짚어 주었다.
7
+ *
8
+ * 「갈래마다 상한을 걸면 합친 결과가 시각순으로 온전하지 않다.
9
+ * 종류 A 의 마지막이 종류 B 의 첫째보다 이를 수 있다.」
10
+ *
11
+ * 그 지적은 **갈래마다 상한보다 적게 받을 때** 참이다. 상한까지 받으면 성질이 선다: 어떤 갈래에서 빠진
12
+ * 행은 그 갈래의 상한 번째보다 뒤이고, 우리는 전체에서 상한 개만 취하므로 그 행이 이 페이지에 들어올 수
13
+ * 없다. 이 시험이 그 성질을 못박는다.
14
+ */
15
+ import { test } from 'node:test'
16
+ import assert from 'node:assert/strict'
17
+
18
+ import { mergeStreams } from '../server/service/twin-journal/event-type-split-shape.js'
19
+
20
+ const byN = (a: { n: number }, b: { n: number }) => a.n - b.n
21
+ const seq = (from: number, count: number) => Array.from({ length: count }, (_, i) => ({ n: from + i }))
22
+
23
+ test('★ 한 갈래가 앞을 다 차지해도 전체에서 앞 limit 개가 나온다 — 짚어 준 그 경우다', () => {
24
+ /*
25
+ * A 가 1~5, B 가 100~104. limit 3 이면 답은 A 의 1,2,3 이다. B 는 한 건도 들어오지 않는 것이 맞다.
26
+ */
27
+ const r = mergeStreams([seq(1, 5), seq(100, 5)], 3, byN)
28
+ assert.deepEqual(r.items.map(x => x.n), [1, 2, 3])
29
+ assert.equal(r.capped, true, '더 있었다는 사실을 말해야 한다')
30
+ })
31
+
32
+ test('★ 갈래를 섞어도 시각순이 유지된다', () => {
33
+ const a = [{ n: 1 }, { n: 4 }, { n: 7 }]
34
+ const b = [{ n: 2 }, { n: 5 }, { n: 8 }]
35
+ const c = [{ n: 3 }, { n: 6 }, { n: 9 }]
36
+ const r = mergeStreams([a, b, c], 9, byN)
37
+ assert.deepEqual(r.items.map(x => x.n), [1, 2, 3, 4, 5, 6, 7, 8, 9])
38
+ assert.equal(r.capped, false, '상한에 닿지 않았으면 잘리지 않았다')
39
+ })
40
+
41
+ test('★ 상한에 닿은 갈래가 있으면 잘렸다고 말한다 — 숫자가 알리지 않고 작아지지 않게', () => {
42
+ /* 한 갈래가 정확히 상한만큼 받아 왔으면 그 갈래에 더 있었을 수 있다. */
43
+ const r = mergeStreams([seq(1, 3), seq(10, 1)], 3, byN)
44
+ assert.equal(r.items.length, 3)
45
+ assert.equal(r.capped, true)
46
+ })
47
+
48
+ test('상한에 못 닿았고 합쳐도 상한 미만이면 잘리지 않았다', () => {
49
+ const r = mergeStreams([seq(1, 2), seq(10, 2)], 10, byN)
50
+ assert.deepEqual(r.items.map(x => x.n), [1, 2, 10, 11])
51
+ assert.equal(r.capped, false)
52
+ })
53
+
54
+ test('빈 갈래가 섞여도 답이 같다 — 그 종류가 그 구간에 없는 것은 정상이다', () => {
55
+ const r = mergeStreams([[], seq(5, 2), []], 10, byN)
56
+ assert.deepEqual(r.items.map(x => x.n), [5, 6])
57
+ assert.equal(r.capped, false)
58
+ })
59
+
60
+ test('★ 갈래가 상한보다 많이 받아 오면 그 사실을 말한다 — 있을 수 없는 일이다', () => {
61
+ /* 부르는 쪽이 상한을 잘못 걸었다는 뜻이고, 그 상태로 답하면 합친 결과를 믿을 수 없다. */
62
+ const r = mergeStreams([seq(1, 5)], 3, byN)
63
+ assert.equal(r.underfilled, true)
64
+ })
65
+
66
+ test('갈래가 하나면 그냥 앞에서 자른 것과 같다', () => {
67
+ const r = mergeStreams([seq(1, 5)], 5, byN)
68
+ assert.deepEqual(r.items.map(x => x.n), [1, 2, 3, 4, 5])
69
+ assert.equal(r.underfilled, false)
70
+ })
@@ -1,24 +1,24 @@
1
1
  /*
2
- * ★ **재기동이 저널을 처음부터 다시 접지 않는다** — 미러가 저장된 재개점에서 이어 선다.
2
+ * ★ **재기동이 저널을 처음부터 다시 계산하지 않는다** — 미러가 저장된 재개점에서 이어 선다.
3
3
  *
4
4
  * ── 무엇이 오래 잘못돼 있었나 (2026-08-24 실측) ─────────────────────────────
5
- * 재개점을 **읽는** 쪽은 다 있었다: `recover` 는 `fold` 가 있으면 꼬리만 접고, 없으면 저널을 0부터
6
- * 접는다. 그런데 **쓰는 쪽이 없었다** — `persistSnapshot` 은 `{revision, state, structureRev}` 만
5
+ * 재개점을 **읽는** 쪽은 다 있었다: `recover` 는 `fold` 가 있으면 꼬리만 계산하고, 없으면 저널을 0부터
6
+ * 계산한다. 그런데 **쓰는 쪽이 없었다** — `persistSnapshot` 은 `{revision, state, structureRev}` 만
7
7
  * 적었다. 실측으로 저장된 스냅샷 34건 전부 `fold` 가 비어 있었고, 저널은 2,960만 줄이었다.
8
8
  *
9
9
  * 그래서 두 가지가 함께 일어났다.
10
10
  *
11
- * ① 켤 때마다 처음부터 접었다 — 규모 기준(엔티티 10만·품목 100만)에서 느린 것이 아니라 못 하는 것이다
11
+ * ① 켤 때마다 처음부터 계산했다 — 규모 기준(엔티티 10만·품목 100만)에서 느린 것이 아니라 못 하는 것이다
12
12
  * ② 미러는 되찾은 상태를 **버렸다**(`startLive`). 「다음 계측이 정정한다」는 전제였는데, 원본이 커서
13
- * 증분으로 말하는 현장에서는 그 정정이 오지 않는다 — 미러가 영구히 빈 채로 남고 화면은 조용했다
13
+ * 증분으로 말하는 현장에서는 그 정정이 오지 않는다 — 미러가 영구히 빈 채로 남고 화면은 들어온 것이 없었다
14
14
  *
15
15
  * ── 왜 커널 시험만으로 충분하지 않나 ────────────────────────────────────────
16
- * 커널은 「재개점 + 꼬리 == 0부터 접기」를 증명한다(`observed-checkpoint.test.ts`). 그것이 참이어도
16
+ * 커널은 「재개점 + 꼬리 == 0부터 계산하기」를 증명한다(`observed-checkpoint.test.ts`). 그것이 참이어도
17
17
  * 호스트가 재개점을 **적지 않거나 되세우지 않으면** 아무 일도 일어나지 않는다 — 이 저장소가 실제로
18
18
  * 넉 달 동안 그 상태였다. 그래서 여기서 DB 를 물고 실제로 걷는다.
19
19
  *
20
20
  * ── 결정적인 단정 ───────────────────────────────────────────────────────────
21
- * 재기동 전에 **저널을 지운다.** 그래도 상태가 돌아오면 그것은 저널을 접어 온 것이 **아니다** —
21
+ * 재기동 전에 **저널을 지운다.** 그래도 상태가 돌아오면 그것은 저널을 계산해 온 것이 **아니다** —
22
22
  * 저장된 재개점에서 온 것이다. 그리고 그 상황은 가정이 아니라 설계된 거동이다: 보존 정책이 체크포인트
23
23
  * 리비전 이하의 행을 지운다(「그 앞은 스냅샷이 대신한다」).
24
24
  */
@@ -31,7 +31,7 @@ const req = createRequire(import.meta.url)
31
31
  const { NamingStrategy, addDataSource, entities: shellEntities, Domain } = req('@things-factory/shell')
32
32
  const { entities: authEntities } = req('@things-factory/auth-base')
33
33
  const { entities: twinEntities } = req('../dist-server/service/index.js')
34
- /* 체크포인트는 `cache-service` 표에 앉는다 — 그 엔티티를 빼면 저장이 조용히 실패한다(스냅샷도 씨앗도). */
34
+ /* 체크포인트는 `cache-service` 표에 앉는다 — 그 엔티티를 빼면 저장이 알리지 않고 실패한다(스냅샷도 씨앗도). */
35
35
  const { entities: cacheEntities } = req('@things-factory/cache-service/dist-server/service/index.js')
36
36
  const { TwinEngine } = req('../dist-server/engine/index.js')
37
37
  const { TwinEvent } = req('../dist-server/service/twin-event/twin-event.js')
@@ -56,7 +56,7 @@ const master = (): any => ({
56
56
  equipment: [{ id: 'fk-1', kind: 'forklift', homeLocationId: 'dock' }]
57
57
  })
58
58
 
59
- /** 원본이 말한 사실 — 오더와 작업. 미러는 이것을 접어 상태를 만든다. */
59
+ /** 원본이 말한 사실 — 오더와 작업. 미러는 이것을 계산해 상태를 만든다. */
60
60
  function facts(n: number): any[] {
61
61
  const out: any[] = []
62
62
  for (let i = 1; i <= n; i++) {
@@ -112,7 +112,7 @@ after(async () => {
112
112
  await ds?.destroy()
113
113
  })
114
114
 
115
- test('체크포인트가 **이어 접을 씨앗을 함께 적는다** — 이 줄이 없어서 넉 달 동안 처음부터 접었다', async () => {
115
+ test('체크포인트가 **이어 계산할 씨앗을 함께 적는다** — 이 줄이 없어서 넉 달 동안 처음부터 계산했다', async () => {
116
116
  const rt = await TwinEngine.bootDeclared(domainId, INSTANCE, 'resync')
117
117
  assert.equal(rt.mode, 'live', '미러로 떠야 이 검사가 뜻이 있다')
118
118
 
@@ -124,14 +124,14 @@ test('체크포인트가 **이어 접을 씨앗을 함께 적는다** — 이
124
124
  await TwinEngine.persistSnapshot(domainId, INSTANCE)
125
125
  const stored = await TwinEngine.loadSnapshot(domainId, INSTANCE)
126
126
  assert.ok(stored, '스냅샷이 저장됐다')
127
- assert.ok(stored.fold?.reducer, '★ 재개점이 함께 적혀야 한다 — 없으면 다음 기동이 저널을 0부터 접는다')
127
+ assert.ok(stored.fold?.reducer, '★ 재개점이 함께 적혀야 한다 — 없으면 다음 기동이 저널을 0부터 계산한다')
128
128
  assert.ok(stored.fold?.oee, '가동 누적기도 함께 — 상태만 되돌리면 가동률이 부팅 이후로만 잡힌다')
129
129
  /* 씨앗이 실제로 사실을 들었는지 — 빈 껍데기를 적어 두고 초록이 되지 않게. */
130
130
  assert.ok((stored.fold.reducer as any).orders?.length, '씨앗에 오더가 담겨 있다')
131
131
  assert.ok((stored.fold.reducer as any).tasks?.length, '씨앗에 작업이 담겨 있다')
132
132
  })
133
133
 
134
- test('★ 저널을 지워도 미러가 상태를 들고 다시 선다 — 그러므로 저널을 접은 것이 아니다', async () => {
134
+ test('★ 저널을 지워도 미러가 상태를 들고 다시 선다 — 그러므로 저널을 계산한 것이 아니다', async () => {
135
135
  /*
136
136
  * 이 단정이 이 축의 전부다. 저널이 없는데 상태가 돌아오면 그 상태는 **저장된 재개점**에서 온 것이다.
137
137
  * 그리고 이 상황은 가정이 아니다 — 보존 정책이 체크포인트 리비전 이하의 행을 실제로 지운다.
@@ -145,7 +145,7 @@ test('★ 저널을 지워도 미러가 상태를 들고 다시 선다 — 그
145
145
  assert.equal(
146
146
  await ds.getRepository(TwinEvent).countBy({ domain: { id: domainId } as any, instanceId: INSTANCE } as any),
147
147
  0,
148
- '저널이 비었다 — 이제 접을 것이 없다'
148
+ '저널이 비었다 — 이제 계산할 것이 없다'
149
149
  )
150
150
 
151
151
  await TwinEngine.bootDeclared(domainId, INSTANCE, 'resync')
@@ -131,11 +131,11 @@ test('선언이 침묵하면 기본값으로 떨어진다 — 없는 것을 지
131
131
  assert.equal(modelBasisChanged(quiet(base), quiet(base * 1.3)), true, '기본 10% 밖')
132
132
  })
133
133
 
134
- test('변동폭이 바뀌면 다른 모델이다 — 판정의 잣대가 조용히 바뀌지 않게', () => {
134
+ test('변동폭이 바뀌면 다른 모델이다 — 판정의 잣대가 알리지 않고 바뀌지 않게', () => {
135
135
  const a = weld(70 * 60_000)
136
136
  const b = {
137
137
  measured: { weld: 70 * 60_000 },
138
138
  declared: [{ key: 'weld', duration: 'PT1H10M', variability: { distribution: 'triangular', min: 'PT30M', max: 'PT3H', mode: 'PT1H5M' } }]
139
139
  }
140
- assert.equal(modelBasisChanged(a, b), true, '변동폭 선언이 바뀌었는데 같은 모델이라고 하면 잣대가 조용히 바뀐다')
140
+ assert.equal(modelBasisChanged(a, b), true, '변동폭 선언이 바뀌었는데 같은 모델이라고 하면 잣대가 오류 없이 바뀐다')
141
141
  })
@@ -20,7 +20,7 @@
20
20
  * ── 지키는 것 ───────────────────────────────────────────────────────────────
21
21
  * ① 마스터가 말한 것과 관측으로 알게 된 것을 섞지 않는다
22
22
  * ② 트윈이 안 돌면 `null` — 「없다」와 「세어 보지 못했다」는 다르다
23
- * ③ 조용히 자르지 않는다 — 상한에서 밀려난 수를 함께 낸다
23
+ * ③ 알리지 않고 자르지 않는다 — 상한에서 밀려난 수를 함께 낸다
24
24
  * ④ 덮지 못하는 축을 밝힌다 — 계량기는 커널이 표시를 달지 않아 이 화면이 못 찾아 준다
25
25
  */
26
26
  import { test } from 'node:test'
@@ -68,7 +68,7 @@ test('돌지만 관측 갭이 없으면 0 이다 — 이건 「없음」과 다
68
68
  assert.equal(g!.total, 0, '세어 보니 없었다')
69
69
  })
70
70
 
71
- test('id 를 조용히 자르지 않는다 — 밀려난 수를 함께 낸다', () => {
71
+ test('id 를 알리지 않고 자르지 않는다 — 밀려난 수를 함께 낸다', () => {
72
72
  const many = Array.from({ length: GAP_ID_LIMIT + 7 }, (_, i) => loc(`ghost-${i}`, 'observed'))
73
73
  const g = modelGapOf({ locations: many })
74
74
 
@@ -95,7 +95,7 @@ test('모르는 설비의 에너지 보고는 「없음」과 0 을 가른다
95
95
  /*
96
96
  * 이 값을 **루트에서 읽고 있었다**(2026-08-20에 잡음). 커널은 `EnergyState` 안에 담으므로 루트에서
97
97
  * 읽으면 영원히 `null` 이고, 오류 없이 「에너지 트윈이 아니다」로 읽힌다. 나만 읽는 필드를 잘못된
98
- * 경로에서 읽으면 이렇게 조용히 사라진다.
98
+ * 경로에서 읽으면 이렇게 오류 없이 사라진다.
99
99
  *
100
100
  * 그리고 커널은 0 이면 그 칸을 만들지 않는다. 그래서 `energy` 자체가 있는지로 「세었고 0」과
101
101
  * 「에너지 트윈이 아님」을 가른다.
@@ -112,7 +112,7 @@ test('모르는 설비의 에너지 보고는 「없음」과 0 을 가른다
112
112
 
113
113
  test('계량기도 덮는다 — 이 축이 가장 무겁다', () => {
114
114
  /*
115
- * 모르는 `meterId` 는 조용히 새 계량기가 되고 그 kW 가 그 현장의 수요·피크·요금 판정에 더해진다.
115
+ * 모르는 `meterId` 는 알리지 않고 새 계량기가 되고 그 kW 가 그 현장의 수요·피크·요금 판정에 더해진다.
116
116
  * 로케이션 오타는 스키매틱이 비는 것으로 드러나지만, 이건 **그럴듯한 숫자**로 나온다.
117
117
  * 커널 0.7.39 가 `MeterPointState.origin` 을 내면서 덮을 수 있게 됐다.
118
118
  */
@@ -4,7 +4,7 @@
4
4
  * ── 계기 (2026-08-06, 사용자 신고) ───────────────────────────────────────────
5
5
  * 모델러의 엔티티 목록이 통째로 비어 있었다. **오류는 없었다.** 저장된 모델가 옛 어휘(`nodes`/`movers`)
6
6
  * 인데 화면과 호스트가 새 어휘를 읽었기 때문이다. 같은 원인이 구조 서명(`N[]M[]` — 서로 다른 두 공장이
7
- * 같아 보인다)·주행 추정기·설비 동기까지 조용히 망가뜨리고 있었다.
7
+ * 같아 보인다)·주행 추정기·설비 동기까지 알리지 않고 망가뜨리고 있었다.
8
8
  *
9
9
  * 그래서 **DB에서 읽는 순간** 한 번 정규화한다. 이 테스트가 지키는 것은 그 정규화가 ① 세 세대를 다
10
10
  * 흡수하고 ② 나머지 속성을 하나도 잃지 않고 ③ 새 모델에는 아무 일도 하지 않는 것이다.
@@ -101,7 +101,7 @@ test('두 옛 모델이 구조적으로 구별된다 — 서명이 빈 문자열
101
101
  })
102
102
 
103
103
  test('보드를 저장하는 엔티티가 **변환기를 달고 있다** — 결선이 빠지면 순수 모듈은 그린인데 화면은 빈다', () => {
104
- /* 이 결함이 정확히 그랬다: 리더는 커널에 있었고 목록만 그것을 거쳤다. 결선은 조용히 빠질 수 있으므로
104
+ /* 이 결함이 정확히 그랬다: 리더는 커널에 있었고 목록만 그것을 거쳤다. 결선은 알리지 않고 빠질 수 있으므로
105
105
  기계가 지킨다. 새 엔티티가 보드를 담게 되면 여기 함께 올린다. */
106
106
  const owners = ['server/service/twin-instance/twin-instance.ts', 'server/service/twin-structure/twin-structure.ts']
107
107
 
@@ -25,7 +25,7 @@ test('잃을 것이 없으면 경고도 없다 — 정상 경로를 시끄럽게
25
25
  assert.deepEqual(j.warnings, [])
26
26
  })
27
27
 
28
- test('없는 현장으로는 옮기지 않는다 — 조용히 새 공간을 만들지 않는다', () => {
28
+ test('없는 현장으로는 옮기지 않는다 — 알리지 않고 새 공간을 만들지 않는다', () => {
29
29
  const j = judgeMoveTwinSpace({ ...CLEAN, toSpaceExists: false })
30
30
  assert.equal(j.refusal?.code, 'space-not-found')
31
31
  assert.equal(j.refusal?.params?.spaceId, 'rosarito-site')
@@ -108,7 +108,7 @@ test('★ withLiveOee: projector 미러(oee 없음) + 누적기 → live 설비
108
108
  /*
109
109
  * ── 되짚은 화면에도 가동 이력이 있어야 한다 (2026-08-05) ────────────────────
110
110
  * OEE 는 원 시스템이 누적을 보내 주지 않아 호스트가 상태 전이를 적분해 만든다. 그런데 그 누적기는
111
- * **라이브에서만** 돌았고, 저널을 접는 경로는 그 계산을 하지 않았다 — 과거를 되짚으면 모든 설비가
111
+ * **라이브에서만** 돌았고, 저널을 계산하는 경로는 그 계산을 하지 않았다 — 과거를 되짚으면 모든 설비가
112
112
  * "가동 이력이 전혀 없음" 으로 보였다(실 저널: `rosarito-mes` 18대, `hatiolab-wms` 22대가 그랬다).
113
113
  *
114
114
  * 없는 것은 데이터가 아니라 계산이다: 입력은 저널에 다 있다. 그래서 같은 누적기에 같은 이벤트를
@@ -174,7 +174,7 @@ test('★ 계측 시점을 벽시계로 재지 않는다 — 과거 화면의
174
174
  *
175
175
  * 예전에는 `metricsSinceMs: 0` 을 돌려줬다. 시각이 epoch 밀리초라서 그 창은 곧 "1970년부터" 가 되고,
176
176
  * 공식의 분모가 지금 시각 전체가 된다 — 화면에 `idleMs 1767226603000`(약 56,000년) 유휴가 섰다.
177
- * 그 값은 availability·overall 의 분모이기도 해서 **성과 숫자 전체가 조용히 거짓**이 됐다.
177
+ * 그 값은 availability·overall 의 분모이기도 해서 **성과 숫자 전체가 알리지 않고 거짓**이 됐다.
178
178
  */
179
179
  test('이벤트를 못 본 설비는 계측 창을 만들지 않는다 — epoch 를 창으로 삼지 않는다', () => {
180
180
  const acc = new OeeAccumulator()
@@ -56,7 +56,7 @@ test('거부는 이유와 함께 남는다 — 유효한 것은 함께 통과한
56
56
  const { accepted, rejected } = ingestCanonicalRecords(
57
57
  [
58
58
  { taskId: 'tk-1', kind: 'pick', status: 'completed' },
59
- { taskId: 'tk-2', kind: 'pick', status: 'FINISHED' } // 커널이 접을 수 없는 낱말
59
+ { taskId: 'tk-2', kind: 'pick', status: 'FINISHED' } // 커널이 다룰 수 없는 낱말
60
60
  ] as any,
61
61
  'domain-1',
62
62
  AT
@@ -66,7 +66,7 @@ test('거부는 이유와 함께 남는다 — 유효한 것은 함께 통과한
66
66
  assert.ok(rejected[0].errors.join(' ').length > 0)
67
67
  })
68
68
 
69
- test('피드 결선이 거부를 조용히 버리지 않는다 — 이유를 사람에게 말한다', () => {
69
+ test('피드 결선이 거부를 알리지 않고 버리지 않는다 — 이유를 사람에게 말한다', () => {
70
70
  const src = readFileSync(fileURLToPath(new URL('../server/service/reference/reference-live.ts', import.meta.url)), 'utf-8')
71
71
  assert.match(src, /record\(s\) rejected/, '거부 수와 이유를 남긴다')
72
72
  })
@@ -12,7 +12,7 @@
12
12
  * · 끊어진 참조는 **이름을 대고 보고**되고, 행 자체는 남는다(막지 않되 침묵하지 않는다)
13
13
  * · **전량 교체** — 두 번 돌려도 늘지 않고, 원본에서 사라진 것은 행에서도 사라진다
14
14
  * · 원본 표식(`sourceRef`·`ingestedAt`) 없는 행은 만들 수 없다(캐시라는 사실이 구조로 지켜진다)
15
- * · 소요시간은 ISO 8601 **문자열 그대로** 남는다(분으로 접으면 초가 조용히 사라진다)
15
+ * · 소요시간은 ISO 8601 **문자열 그대로** 남는다(분으로 줄이면 초가 오류 없이 사라진다)
16
16
  */
17
17
  import { test, before, after, beforeEach } from 'node:test'
18
18
  import assert from 'node:assert/strict'
@@ -161,7 +161,7 @@ test('원본 표식 없는 행은 만들 수 없다 — 캐시라는 사실이
161
161
  )
162
162
  })
163
163
 
164
- test('소요시간은 ISO 8601 문자열 그대로 — 분으로 접으면 초가 조용히 사라진다', async () => {
164
+ test('소요시간은 ISO 8601 문자열 그대로 — 분으로 줄이면 초가 오류 없이 사라진다', async () => {
165
165
  const op = await ds.getRepository(TwinOperation).findOne({ where: { operationKey: 'cut' } })
166
166
  assert.equal(op?.duration, 'PT1H10M30S')
167
167
  /* 변동성·파라미터는 도메인마다 형이 달라 원형 그대로 싣는다. */
@@ -16,7 +16,7 @@
16
16
  * 것은 "이 사실이 여기 앉는다" 는 선언이고, 앉을 자리를 만들어 두고 안 채우면 그 컬럼은 영원히 빈다.
17
17
  *
18
18
  * 그래서 컬럼마다 셋을 요구한다:
19
- * ① 픽스처가 모델에 그 값을 싣는다(값을 줄 방법을 모르면 **검사가 실패한다** — 조용히 건너뛰면
19
+ * ① 픽스처가 모델에 그 값을 싣는다(값을 줄 방법을 모르면 **검사가 실패한다** — 알리지 않고 건너뛰면
20
20
  * 그 컬럼은 아무도 안 보는 컬럼이 된다)
21
21
  * ② 투영을 지나 행에 남는다
22
22
  * ③ 조회 응답에 나온다(화면이 읽는 자리)
@@ -131,7 +131,7 @@ function master(): any {
131
131
  assert.ok(
132
132
  col in VALUE,
133
133
  `픽스처가 "${c.axis}.${col}" 에 실을 값을 모른다 — VALUE 에 하나 적어라(모델의 사실이 아니면 NOT_FROM_MODEL 에 이유와 함께).\n` +
134
- '조용히 건너뛰면 그 컬럼은 아무도 안 보는 컬럼이 되고, 그것이 이 하네스가 막으려는 실패다.'
134
+ '알리지 않고 건너뛰면 그 컬럼은 아무도 안 보는 컬럼이 되고, 그것이 이 하네스가 막으려는 실패다.'
135
135
  )
136
136
  o[col] = VALUE[col]
137
137
  }
@@ -4,7 +4,7 @@
4
4
  * ── 왜 이 시험이 있나 (2026-08-18) ──────────────────────────────────────────
5
5
  * 화면은 속성마다 「무엇을 바꾸나」를 말한다. 등록되지 않은 속성에는 「표시만 됩니다」라고 한다 — 그것이
6
6
  * 정직한 기본값이다. 그런데 에너지 값들(계약전력·정격·요금)은 **오래전부터 읽히고 있었는데** 등록부에
7
- * 없었다: 사용자는 계약전력을 넣고도 「이 값은 아무것도 바꾸지 않는다」는 문장을 읽었다. 조용한 거짓말이다.
7
+ * 없었다: 사용자는 계약전력을 넣고도 「이 값은 아무것도 바꾸지 않는다」는 문장을 읽었다. 들어온 것이 없는 거짓말이다.
8
8
  *
9
9
  * 반대 방향의 위험도 있다: 효과를 등록해 두고 **읽는 코드가 없으면** 화면이 「쓰입니다」라고 말하는데
10
10
  * 아무 일도 일어나지 않는다. 그래서 등록부의 각 줄이 가리키는 자리를 이 시험이 확인한다.
@@ -65,7 +65,7 @@ test('축으로 갈라 낸다 — 그 축의 항목에만 효과를 말한다',
65
65
  test('요금은 커널이 아니라 호스트가 읽는다 — 그 사실을 readBy 가 말한다', () => {
66
66
  const tariff = PROPERTY_EFFECTS.filter(e => e.id.startsWith('tariff.'))
67
67
  assert.equal(tariff.length, 3)
68
- for (const e of tariff) assert.match(e.readBy, /kpi-query/, '금액은 저널을 접는 쪽이 만든다')
68
+ for (const e of tariff) assert.match(e.readBy, /kpi-query/, '금액은 저널을 계산하는 쪽이 만든다')
69
69
  })
70
70
 
71
71
  /*
@@ -110,7 +110,7 @@ test('수율·셋업이 커널 창구까지 배선돼 있다 — 등록만 하
110
110
  const divisions = layer.match(/pct \/ 100/g) ?? []
111
111
  assert.equal(divisions.length, 1, `퍼센트 환산은 한 자리에서만 (발견 ${divisions.length})`)
112
112
  assert.match(hostSrc('engine/twin-engine.ts'), /declareParameters\(params\)/, '기동·구조전환 때 커널에 싣는다')
113
- /* 커널이 그 창구를 갖지 않은 버전이면 **말한다** — 조용히 상수로 돌지 않게. */
113
+ /* 커널이 그 창구를 갖지 않은 버전이면 **말한다** — 알리지 않고 상수로 돌지 않게. */
114
114
  assert.match(hostSrc('engine/twin-engine.ts'), /declareParameters missing/, '버전 어긋남을 경고로 드러낸다')
115
115
  })
116
116
 
@@ -0,0 +1,106 @@
1
+ /*
2
+ * ★ 연결된 시스템에 닿지 못한 것과 다시 닿은 것을 인제스트 레저가 기록한다.
3
+ *
4
+ * ── 무엇이 없었나 (2026-08-27) ──────────────────────────────────────────────
5
+ * 커넥터는 폴링이 실패할 때 세 가지를 안다: 연달아 몇 번인지 · 언제부터인지 · 다음에 언제 다시 묻는지.
6
+ * 그런데 어댑터 계약에는 이유 한 줄만 받는 자리가 있었다. 그래서 커넥터가 세 가지를 **한국어 문장으로
7
+ * 합쳐** 이유 칸에 넣고 있었다(`ppms-sunsage.ts`).
8
+ *
9
+ * 두 가지가 틀어진다.
10
+ * ① 연달아 횟수와 시작 시각은 이 레저가 **이미 센다**. 두 곳에서 세면 갈라진다.
11
+ * ② 그 칸은 화면에 그려지고 화면은 다섯 언어로 그린다. 한 언어의 문장은 번역되지 않는다.
12
+ *
13
+ * 그리고 회복을 받는 자리가 아예 없었다. 읽기가 다시 성공하면 실패 기록만 지워졌고, 그러면 밤새 두 시간
14
+ * 끊겼던 연결과 한 번도 끊기지 않은 연결이 아침에 똑같이 보인다.
15
+ */
16
+ import { test } from 'node:test'
17
+ import assert from 'node:assert/strict'
18
+ import {
19
+ newIngestLedger,
20
+ recordReadFailure,
21
+ recordRecovered,
22
+ clearReadFailure,
23
+ ingestHealth
24
+ } from '../server/engine/ingest-health.js'
25
+
26
+ const t0 = Date.UTC(2026, 7, 27, 3, 0, 0)
27
+
28
+ test('★ 연달아 실패한 횟수와 시작 시각은 레저가 센다 — 커넥터가 보내지 않는다', () => {
29
+ const l = newIngestLedger()
30
+ recordReadFailure(l, 'connect timeout', t0, 'inverters')
31
+ recordReadFailure(l, 'connect timeout', t0 + 60_000, 'inverters')
32
+ recordReadFailure(l, 'connect timeout', t0 + 120_000, 'inverters')
33
+ assert.equal(l.readFailure?.consecutive, 3)
34
+ assert.equal(l.readFailure?.sinceMs, t0, '처음 실패한 시각을 붙잡는다')
35
+ assert.equal(l.readFailure?.lastAtMs, t0 + 120_000)
36
+ })
37
+
38
+ test('★ 이유는 이유만 담는다 — 세는 값을 문장으로 합쳐 넣지 않는다', () => {
39
+ /*
40
+ * 이 테스트는 사람이 읽는 문장이 이유 칸에 들어가는 것을 막지 못한다(문자열은 무엇이든 담긴다).
41
+ * 막을 수 있는 것은 **그 값들을 담을 다른 자리가 있다**는 사실뿐이다. 그것을 테스트로 고정한다.
42
+ */
43
+ const l = newIngestLedger()
44
+ recordReadFailure(l, 'connect timeout', t0, 'inverters', 480_000)
45
+ assert.equal(l.readFailure?.reason, 'connect timeout')
46
+ assert.equal(l.readFailure?.nextRetryMs, 480_000)
47
+ })
48
+
49
+ test('다음 시도까지의 시간은 있을 때만 적는다 — 0 이나 없는 값을 「곧 다시 묻는다」로 만들지 않는다', () => {
50
+ const l = newIngestLedger()
51
+ recordReadFailure(l, 'connect timeout', t0)
52
+ assert.equal('nextRetryMs' in (l.readFailure as any), false)
53
+ recordReadFailure(l, 'connect timeout', t0 + 1000, undefined, 0)
54
+ assert.equal('nextRetryMs' in (l.readFailure as any), false)
55
+ })
56
+
57
+ test('★ 돌아오면 실패 기록이 사라지고 돌아온 사실이 남는다', () => {
58
+ const l = newIngestLedger()
59
+ recordReadFailure(l, 'connect timeout', t0)
60
+ recordReadFailure(l, 'connect timeout', t0 + 60_000)
61
+ recordRecovered(l, 2, 120_000, t0 + 120_000)
62
+ assert.equal(l.readFailure, undefined, '지금은 닿는다')
63
+ assert.deepEqual(l.lastRecovery, { atMs: t0 + 120_000, afterFailures: 2, downMs: 120_000 })
64
+ })
65
+
66
+ test('★ 그냥 지우면 끊긴 적이 있었다는 사실이 사라진다 — 그래서 두 함수를 나눈다', () => {
67
+ const l = newIngestLedger()
68
+ recordReadFailure(l, 'connect timeout', t0)
69
+ clearReadFailure(l)
70
+ assert.equal(l.readFailure, undefined)
71
+ assert.equal(l.lastRecovery, undefined, '지우기만 하면 남지 않는다')
72
+ })
73
+
74
+ test('★ 다시 끊기면 새 단절이다 — 지난번 회복 기록이 그것을 가리지 않는다', () => {
75
+ const l = newIngestLedger()
76
+ recordReadFailure(l, 'connect timeout', t0)
77
+ recordRecovered(l, 1, 60_000, t0 + 60_000)
78
+ recordReadFailure(l, '401 unauthorized', t0 + 600_000)
79
+ assert.equal(l.readFailure?.consecutive, 1, '연달아 횟수가 1 부터 다시 센다')
80
+ assert.equal(l.readFailure?.sinceMs, t0 + 600_000)
81
+ assert.equal(l.lastRecovery?.atMs, t0 + 60_000, '지난 회복도 함께 보인다')
82
+ })
83
+
84
+ test('★ 밖으로 내는 모양에 두 사실이 다 있다 — 화면이 옮기지 못하면 아무 도움이 되지 않는다', () => {
85
+ const l = newIngestLedger()
86
+ recordReadFailure(l, 'connect timeout', t0, 'inverters', 480_000)
87
+ const v: any = ingestHealth(l, 'attached')
88
+ assert.equal(v.readFailure.consecutive, 1)
89
+ assert.equal(v.readFailure.reason, 'connect timeout')
90
+ assert.equal(v.readFailure.stream, 'inverters')
91
+ assert.equal(v.readFailure.nextRetryMs, 480_000)
92
+ assert.equal(typeof v.readFailure.since, 'string')
93
+
94
+ recordRecovered(l, 1, 480_000, t0 + 480_000)
95
+ const w: any = ingestHealth(l, 'attached')
96
+ assert.equal(w.readFailure, undefined)
97
+ assert.equal(w.lastRecovery.at, new Date(t0 + 480_000).toISOString())
98
+ assert.equal(w.lastRecovery.afterFailures, 1)
99
+ assert.equal(w.lastRecovery.downMs, 480_000)
100
+ })
101
+
102
+ test('닿은 적도 끊긴 적도 없으면 두 칸이 다 없다 — 없는 칸을 「끊겼다」로 읽지 않게', () => {
103
+ const v: any = ingestHealth(newIngestLedger(), 'attached')
104
+ assert.equal('readFailure' in v, false)
105
+ assert.equal('lastRecovery' in v, false)
106
+ })
@@ -1,7 +1,7 @@
1
1
  /*
2
- * ★ 「원본에 닿지 못한다」와 「원본이 조용하다」는 **다른 사실**이다 — 상태가 그것을 가려야 한다.
2
+ * ★ 「원본에 닿지 못한다」와 「연결된 시스템에서 들어온 것이 없다」는 **다른 사실**이다 — 상태가 그것을 가려야 한다.
3
3
  *
4
- * ── 무엇이 조용했나 (2026-08-23 실측) ───────────────────────────────────────
4
+ * ── 무엇이 아무 신호도 내지 않았나 (2026-08-23 실측) ───────────────────────────────────────
5
5
  * 실 원본이 끊겼다(`jejoup.hatiolab.com:4000` 접속 시간 초과, HTTP 000 / 8초). 그런데 트윈의
6
6
  * **조회 가능한 상태 어디에도 그 사실이 없었다.**
7
7
  *
@@ -14,7 +14,7 @@
14
14
  *
15
15
  * ── 이 시험이 지키는 것 ─────────────────────────────────────────────────────
16
16
  * ① 실패가 상태에 남는다 ② 「몇 번」과 「언제부터」를 함께 든다(한 번의 흔들림과 20분째 단절은 다르다)
17
- * ③ **빈 읽기는 성공이다** — 조용한 원본을 끊긴 원본으로 만들지 않는다
17
+ * ③ **빈 읽기는 성공이다** — 들어온 것이 없는 연결을 끊긴 원본으로 만들지 않는다
18
18
  * ④ 조회 뷰로 나간다 — 상태에만 있고 안 나가면 화면이 못 본다
19
19
  */
20
20
  import { test } from 'node:test'
@@ -53,7 +53,7 @@ test('한 번의 흔들림과 20분째 단절을 가른다 — 시작 시각을
53
53
  assert.equal(l.readFailure?.lastAtMs, T0 + 1_200_000, '마지막 실패는 갱신된다')
54
54
  })
55
55
 
56
- test('빈 읽기는 성공이다 — 조용한 원본을 끊긴 원본으로 만들지 않는다', () => {
56
+ test('빈 읽기는 성공이다 — 들어온 것이 없는 연결을 끊긴 원본으로 만들지 않는다', () => {
57
57
  /*
58
58
  * ★ 이 단정이 고치려던 것의 **반대 방향으로 틀리는 것**을 막는다. 원본이 「새 것이 없다」고 답한 것은
59
59
  * 닿았다는 뜻이다.
@@ -90,7 +90,7 @@ test('닿고 있으면 그 필드를 만들지 않는다 — 빈 값이 「닿
90
90
 
91
91
  test('「유입 없음」과 「닿지 못함」은 다른 판정이다', () => {
92
92
  /*
93
- * 유입 없음은 원본이 조용한 것일 수 있고, 닿지 못함은 우리가 묻지도 못한 것이다. 두 상태가 같은
93
+ * 유입 없음은 연결된 시스템에서 들어온 것이 없는 것일 수 있고, 닿지 못함은 우리가 묻지도 못한 것이다. 두 상태가 같은
94
94
  * 값으로 뭉개지면 화면이 두 문장을 다르게 말할 수 없다.
95
95
  */
96
96
  const quiet = newIngestLedger()
@@ -99,7 +99,7 @@ test('「유입 없음」과 「닿지 못함」은 다른 판정이다', () =>
99
99
 
100
100
  const vq = ingestHealth(quiet, 'attached', undefined, T0)
101
101
  const vd = ingestHealth(dark, 'attached', undefined, T0)
102
- assert.equal(vq.lastAt, null, '조용한 쪽도 받은 것이 없다')
102
+ assert.equal(vq.lastAt, null, '들어온 것이 없는 쪽도 받은 것이 없다')
103
103
  assert.equal(vd.lastAt, null, '끊긴 쪽도 받은 것이 없다 — 그 값만으로는 갈리지 않는다')
104
104
  assert.notEqual(Boolean(vq.readFailure), Boolean(vd.readFailure), '이 축이 그 둘을 가른다')
105
105
  })
@@ -117,7 +117,7 @@ test('「유입 없음」과 「닿지 못함」은 다른 판정이다', () =>
117
117
  * status **연결 설정이 성립하나** — 접속 시험·마스터 동기가 정한다. 재부착의 **자격**이다
118
118
  * 읽기 실패 **지금 닿나** — 오갈 수 있는 사실이고 유입 장부가 그 축을 갖는다
119
119
  *
120
- * 「사실을 말하게」 하려던 것이 「사실을 못 듣게」 만들었다. 그 방향의 실패는 조용하다 — 화면은
120
+ * 「사실을 말하게」 하려던 것이 「사실을 못 듣게」 만들었다. 그 방향의 실패는 들어온 것이 없다 — 화면은
121
121
  * 「원본에 닿지 못한다」를 정직하게 말하면서, 정작 다시 닿을 기회를 잃는다.
122
122
  */
123
123
  test('읽기 실패가 레퍼런스의 status 를 건드리지 않는다 — 그것이 재부착의 자격이다', () => {
@@ -0,0 +1,118 @@
1
+ /*
2
+ * ★ 저장된 값을 시각으로 읽는 규칙 — 드라이버의 문을 지난다.
3
+ *
4
+ * ── 무엇이 어긋났나 (2026-08-28 실측) ───────────────────────────────────────
5
+ * `getRawOne` 은 드라이버가 준 값을 그대로 준다. sqlite 가 주는 그 값에는 시간대가 없다
6
+ * (`2026-08-27 14:37:20.049`). 그것을 `new Date()` 로 읽으면 서버의 지역 시간으로 해석해 한국에서
7
+ * 9시간이 밀린다. 네 자리에서 났고, 둘은 **지우는 경계**였다(경계가 이르게 밀리면 정리가 0건이 되어
8
+ * 멈춘다).
9
+ *
10
+ * ── 처음에 편법을 썼다 ──────────────────────────────────────────────────────
11
+ * 「시간대가 없으면 UTC 로 읽는다」를 응용 코드에 직접 적었다. 그것은 드라이버가 어떤 문자열로
12
+ * 저장하는지를 우리가 아는 것이고, 다섯 드라이버 규칙에 어긋난다.
13
+ *
14
+ * 표준 문은 `Driver.prepareHydratedValue` 다 — 엔티티 변환이 지나는 그 함수. 이 시험은 컬럼 값을
15
+ * 읽을 때 **우리가 해석하지 않고 그 문에 물어보는지**를 지킨다.
16
+ */
17
+ import { test } from 'node:test'
18
+ import assert from 'node:assert/strict'
19
+ import { readFileSync, readdirSync, statSync } from 'node:fs'
20
+ import { join } from 'node:path'
21
+
22
+ import { absoluteTime, hydratedDate } from '../server/engine/read-time.js'
23
+
24
+ /** 드라이버를 흉내낸 저장소 — 이 시험이 확인하는 것은 「누가 해석하나」다. */
25
+ const fakeRepository = (opts: { hasColumn?: boolean; hydrate?: (v: unknown) => unknown } = {}) => {
26
+ const calls: unknown[] = []
27
+ const repo = {
28
+ metadata: {
29
+ name: 'TwinEvent',
30
+ findColumnWithPropertyName: (name: string) => (opts.hasColumn === false ? undefined : { propertyName: name, type: Date })
31
+ },
32
+ manager: {
33
+ connection: {
34
+ driver: {
35
+ prepareHydratedValue: (value: unknown) => {
36
+ calls.push(value)
37
+ return opts.hydrate ? opts.hydrate(value) : new Date('2026-08-27T14:37:20.049Z')
38
+ }
39
+ }
40
+ }
41
+ }
42
+ }
43
+ return { repo: repo as any, calls }
44
+ }
45
+
46
+ test('★ 컬럼 값은 우리가 해석하지 않고 드라이버에게 물어본다', () => {
47
+ const { repo, calls } = fakeRepository()
48
+ const at = hydratedDate(repo, 'createdAt', '2026-08-27 14:37:20.049')
49
+ assert.equal(at?.toISOString(), '2026-08-27T14:37:20.049Z')
50
+ assert.deepEqual(calls, ['2026-08-27 14:37:20.049'], '드라이버의 문을 그 값으로 한 번 지나야 한다')
51
+ })
52
+
53
+ test('빈 값은 드라이버를 부르지 않고 null — 없는 것은 없는 것이다', () => {
54
+ const { repo, calls } = fakeRepository()
55
+ assert.equal(hydratedDate(repo, 'createdAt', null), null)
56
+ assert.equal(hydratedDate(repo, 'createdAt', undefined), null)
57
+ assert.deepEqual(calls, [])
58
+ })
59
+
60
+ test('드라이버가 날짜를 내주지 못하면 null — 「지금」으로 메우지 않는다', () => {
61
+ const { repo } = fakeRepository({ hydrate: () => 'still a string' })
62
+ assert.equal(hydratedDate(repo, 'createdAt', 'x'), null)
63
+ const bad = fakeRepository({ hydrate: () => new Date('nope') })
64
+ assert.equal(hydratedDate(bad.repo, 'createdAt', 'x'), null)
65
+ })
66
+
67
+ test('★ 컬럼 이름을 못 찾으면 명시 실패한다 — 이름이 바뀐 것을 값으로 덮지 않는다', () => {
68
+ const { repo } = fakeRepository({ hasColumn: false })
69
+ assert.throws(() => hydratedDate(repo, 'createdAt', '2026-08-27 14:37:20.049'), /no column for property/)
70
+ })
71
+
72
+ test('★ 계약이 절대시각인 값 — 시간대가 없으면 UTC 다(지역 시간으로 읽을 근거가 없다)', () => {
73
+ assert.equal(absoluteTime('2026-08-27 14:37:20.049')?.toISOString(), '2026-08-27T14:37:20.049Z')
74
+ /* 지역 시간으로 읽었다면 한국에서 9시간이 밀린다. */
75
+ assert.notEqual(absoluteTime('2026-08-27 14:37:20.049')?.toISOString(), '2026-08-27T05:37:20.049Z')
76
+ })
77
+
78
+ test('시간대가 적혀 있으면 그 말을 믿는다', () => {
79
+ assert.equal(absoluteTime('2026-08-27T14:37:20.049Z')?.toISOString(), '2026-08-27T14:37:20.049Z')
80
+ assert.equal(absoluteTime('2026-08-27T23:37:20.049+09:00')?.toISOString(), '2026-08-27T14:37:20.049Z')
81
+ assert.equal(absoluteTime('2026-08-27T09:37:20.049-05:00')?.toISOString(), '2026-08-27T14:37:20.049Z')
82
+ })
83
+
84
+ test('날짜 객체와 밀리초도 받는다', () => {
85
+ const d = new Date('2026-08-27T14:37:20.049Z')
86
+ assert.equal(absoluteTime(d), d)
87
+ assert.equal(absoluteTime(1_756_305_440_049)?.getTime(), 1_756_305_440_049)
88
+ })
89
+
90
+ test('★ 읽을 수 없는 값은 null — 경계를 정하는 자리가 이 함수를 쓴다', () => {
91
+ for (const bad of [null, undefined, '', ' ', '어제', 'not-a-date', {}, [], NaN, new Date('x')]) {
92
+ assert.equal(absoluteTime(bad), null, `${String(bad)} 는 null 이어야 한다`)
93
+ }
94
+ })
95
+
96
+ test('★ 컬럼 값을 직접 날짜로 만드는 자리가 남아 있지 않다', () => {
97
+ /*
98
+ * 규칙이 있어도 새로 쓰는 코드가 그것을 지나지 않으면 같은 결함이 다시 난다. 그래서 자리를 세어
99
+ * 둔다 — 원시 결과의 컬럼을 `new Date(...)` 로 감싸는 모양을 찾는다.
100
+ *
101
+ * 밀리초 숫자(`new Date(someMs)`)는 시간대가 없으므로 이 그물에 걸리지 않는다.
102
+ */
103
+ const root = join(import.meta.dirname, '..', 'server')
104
+ const offenders: string[] = []
105
+ const walk = (dir: string) => {
106
+ for (const name of readdirSync(dir)) {
107
+ const p = join(dir, name)
108
+ if (statSync(p).isDirectory()) { walk(p); continue }
109
+ if (!name.endsWith('.ts')) continue
110
+ const text = readFileSync(p, 'utf8')
111
+ for (const m of text.matchAll(/new Date\(\s*[A-Za-z_$][\w$?.]*\.(eventTime|createdAt|updatedAt|at)\b[^)]*\)/g)) {
112
+ offenders.push(`${name}: ${m[0]}`)
113
+ }
114
+ }
115
+ }
116
+ walk(root)
117
+ assert.deepEqual(offenders, [], '컬럼 값을 직접 날짜로 만드는 자리가 있다 — hydratedDate() 를 지나야 한다')
118
+ })
@@ -59,7 +59,7 @@ test('목과 가상은 대상 없이도 선다 — 그 자체가 사실이다',
59
59
  test('instance 와 product 를 가른다 — 그 구분이 이 어휘의 요점이다', () => {
60
60
  /*
61
61
  * 첫 실 연동에서 그 배포만의 성질이 넷 드러났는데(관계 리솔버가 다른 테넌트의 행을 답한다 · 모르는
62
- * 필터를 조용히 무시한다 · 지워진 품목의 BOM 이 남아 있다 · 사실상 무한을 뜻하는 표식) **그것이
62
+ * 필터를 알리지 않고 무시한다 · 지워진 품목의 BOM 이 남아 있다 · 사실상 무한을 뜻하는 표식) **그것이
63
63
  * 제품의 성질인지 그 인스턴스의 성질인지 아무도 모른다.** 두 번째 배포가 붙는 날 갈린다.
64
64
  *
65
65
  * 그래서 하나로 확인한 것을 `product` 라 부르면 안 되고, 어휘가 그 구분을 담는다.