@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,88 @@
1
+ /*
2
+ * ★ 구현했는데 선언하지 않은 능력을 아무도 말해 주지 않았다.
3
+ *
4
+ * ── 무엇이 났나 (2026-08-25) ────────────────────────────────────────────────
5
+ * 능력 판정은 **한 방향만** 본다. 선언했는데 구현이 없는 것은 걸러 내고, 구현했는데 선언이 없는 것은
6
+ * 들어온 것이 없다. 그래서 태양광 발전소를 붙이던 사람이 이렇게 막혔다.
7
+ *
8
+ * 커넥터가 openLiveFeed 를 구현했다 · capabilities: ['live'] 를 잊었다
9
+ * → 연결 화면이 「이 원본은 라이브를 못 낸다」로 읽는다
10
+ * → 재기동 정책을 정할 수 없어 트윈 생성이 거절된다
11
+ * → 화면에 보이는 것은 「재기동 정책이 없습니다」뿐이다
12
+ *
13
+ * 원인을 찾는 데 코드를 뒤져야 했다. 지금 저장소에 같은 상태인 커넥터가 일곱이다.
14
+ *
15
+ * ── 막지 않고 알린다 ────────────────────────────────────────────────────────
16
+ * 등록에서 던지면 커넥터 하나 때문에 호스트가 못 뜬다. 만들다 만 커넥터를 들고 있는 사람이 아무것도
17
+ * 못 하게 된다. 이 어긋남은 고칠 수 있는 것이지 위험한 것이 아니다.
18
+ */
19
+ import { test } from 'node:test'
20
+ import assert from 'node:assert/strict'
21
+
22
+ /* `twinWarn` 은 `console.warn` 으로 나간다 — 모듈을 바꿔치지 않고 그 자리에서 받는다. */
23
+ const warnings: string[] = []
24
+ const originalWarn = console.warn
25
+ console.warn = (...args: unknown[]) => {
26
+ warnings.push(args.join(' '))
27
+ }
28
+ process.on('exit', () => (console.warn = originalWarn))
29
+
30
+ const { registerAdapterType, capabilitiesOf } = await import('../server/service/reference/reference-adapter.js')
31
+
32
+ const adapter = (over: Record<string, unknown> = {}) =>
33
+ ({
34
+ type: `probe-${Math.round(performance.now() * 1000)}-${warnings.length}`,
35
+ async fetchMaster() {
36
+ return {} as any
37
+ },
38
+ ...over
39
+ }) as any
40
+
41
+ test('★ 라이브를 구현하고 선언하지 않으면 말해 준다 — 이것이 없어서 화면이 다른 말을 했다', () => {
42
+ const before = warnings.length
43
+ const a = adapter({ openLiveFeed: () => () => {} })
44
+ registerAdapterType(a)
45
+ const said = warnings.slice(before).join('\n')
46
+ assert.ok(said.includes(a.type), '어느 커넥터인지 말한다')
47
+ assert.ok(said.includes('live'), '무엇이 빠졌는지 말한다')
48
+ assert.ok(said.includes('capabilities'), '무엇을 하면 되는지 말한다')
49
+ /* 그리고 지금 화면이 어떻게 읽는지도 말한다 — 사람이 겪은 증상과 이어져야 원인으로 읽힌다. */
50
+ assert.ok(/live twin cannot be created/.test(said))
51
+ })
52
+
53
+ test('★ 조치를 구현하고 선언하지 않아도 말해 준다 — 같은 규율이다', () => {
54
+ const before = warnings.length
55
+ const a = adapter({ control: async () => ({ ok: true }) })
56
+ registerAdapterType(a)
57
+ assert.ok(warnings.slice(before).join('\n').includes('control'))
58
+ })
59
+
60
+ test('둘 다 어긋나면 둘 다 말한다 — 하나만 말하면 고친 뒤 또 막힌다', () => {
61
+ const before = warnings.length
62
+ const a = adapter({ openLiveFeed: () => () => {}, control: async () => ({ ok: true }) })
63
+ registerAdapterType(a)
64
+ const said = warnings.slice(before).join('\n')
65
+ assert.ok(said.includes('live') && said.includes('control'))
66
+ })
67
+
68
+ test('선언과 구현이 맞으면 아무 말도 하지 않는다 — 소음이 되면 아무도 읽지 않는다', () => {
69
+ const before = warnings.length
70
+ registerAdapterType(adapter({ openLiveFeed: () => () => {}, capabilities: ['live'] }))
71
+ registerAdapterType(adapter({}))
72
+ assert.equal(warnings.length, before)
73
+ })
74
+
75
+ test('선언했는데 구현이 없는 쪽은 판정이 이미 걸러 낸다 — 여기서 두 번 말하지 않는다', () => {
76
+ const before = warnings.length
77
+ const a = adapter({ capabilities: ['live'] })
78
+ registerAdapterType(a)
79
+ assert.equal(warnings.length, before, '등록은 들어온 것이 없다')
80
+ assert.deepEqual(capabilitiesOf(a), [], '능력 판정이 구현 없는 선언을 걸러 낸다')
81
+ })
82
+
83
+ test('★ 등록을 막지 않는다 — 커넥터 하나 때문에 호스트가 못 뜨면 안 된다', () => {
84
+ const a = adapter({ openLiveFeed: () => () => {} })
85
+ assert.doesNotThrow(() => registerAdapterType(a))
86
+ /* 등록은 되었고, 다만 그 능력은 선언되지 않았으므로 판정에서 빠진다 — 그 사실이 경고와 짝이다. */
87
+ assert.deepEqual(capabilitiesOf(a), [])
88
+ })
@@ -12,7 +12,7 @@
12
12
  * 이 시험이 지키는 것:
13
13
  * ① 선언과 구현이 **함께** 있어야 능력이다 — 선언만 있으면 화면이 거짓말을 하고, 구현만 있으면 닫아 둔
14
14
  * 문을 여는 것이다(선언이 권위다).
15
- * ② 능력 없는 원본은 **사유 코드로** 거절한다(조용한 실패 금지).
15
+ * ② 능력 없는 원본은 **사유 코드로** 거절한다(들어온 것이 없는 실패 금지).
16
16
  * ③ 없는 기능을 있는 것처럼 답하지 않는다(내장 시뮬은 아직 재씨앗을 못 한다).
17
17
  */
18
18
  import { test } from 'node:test'
@@ -76,9 +76,9 @@ test('「몇 번·언제부터」를 정체 쪽도 든다 — 한 번 밀린 것
76
76
 
77
77
  test('뷰로 나간다 — **유입이 없는 갈래에서도**', () => {
78
78
  /*
79
- * ★ 여기가 가장 조용해지기 쉬운 자리다. 커서가 막히면 유입이 0이므로 장부가 「빈 장부」 갈래를
79
+ * ★ 여기가 아무 신호도 나지 않기 가장 쉬운 자리다. 커서가 막히면 유입이 0이므로 장부가 「빈 장부」 갈래를
80
80
  * 지나고, 그 갈래에서 이 값을 떨어뜨리면 **정체가 실제로 일어나는 바로 그 상황에서** 침묵한다.
81
- * `unhandled` 를 스냅샷이 떨어뜨려 정합성 검사가 영원히 조용했던 것과 같은 부류다.
81
+ * `unhandled` 를 스냅샷이 떨어뜨려 정합성 검사가 영원히 아무 신호도 내지 않았던 것과 같은 부류다.
82
82
  */
83
83
  const l = newIngestLedger()
84
84
  recordCursorStall(l, 'one timestamp holds more rows than a page', T0, 'trace')
@@ -5,7 +5,7 @@
5
5
  * 커널 쪽에는 같은 이름의 하네스가 있다(kernel/test/declaration-in-state: 선언 ⊆ 상태). 그런데 사실이
6
6
  * 새던 자리는 그 앞에도 있었다: **레퍼런스 마스터 → 커널 모델** 이라는 문 하나(`masterToTwin`).
7
7
  *
8
- * 여기서 새면 아무도 모른다. 원본은 말했고, 화면은 조용히 id 를 보인다. 실제로 그렇게 잃었다 —
8
+ * 여기서 새면 아무도 모른다. 원본은 말했고, 화면은 알리지 않고 id 를 보인다. 실제로 그렇게 잃었다 —
9
9
  * 자리의 이름(화면이 `cut-station` 을 찍었다), 사람의 이름, GS1 참조, 자원 속성, 시험 명세 참조,
10
10
  * 사람·설비의 시험 결과. 하루 사이에 여섯 번이고, 전부 **한 줄에서 필드 하나가 빠진** 것이었다.
11
11
  *
@@ -21,7 +21,7 @@
21
21
  * 셋 중 어느 것도 아니면 실패한다 — 새 필드를 추가한 사람이 그때 분류해야 한다. 분류를 강제하는 것이
22
22
  * 이 검사의 전부다: 잊는 것이 기본값이면 잊는다.
23
23
  *
24
- * 필드 목록은 **소스에서 읽는다**(손으로 적지 않는다). 손으로 적으면 새 필드가 조용히 빠지고, 그것이
24
+ * 필드 목록은 **소스에서 읽는다**(손으로 적지 않는다). 손으로 적으면 새 필드가 알리지 않고 빠지고, 그것이
25
25
  * 바로 이 검사가 막으려는 실패다.
26
26
  */
27
27
  import { test } from 'node:test'
@@ -70,7 +70,7 @@ const ELSEWHERE: Record<string, string> = {
70
70
  footprint: '좌표는 `model.layout`(중심점)과 구역 기하로 간다 — 자리 줄에 실리는 값이 아니다.'
71
71
  }
72
72
 
73
- /** 모든 필드에 값을 실은 원본 — 값을 모르는 필드가 나오면 이 하네스는 실패한다(조용히 건너뛰지 않는다). */
73
+ /** 모든 필드에 값을 실은 원본 — 값을 모르는 필드가 나오면 이 하네스는 실패한다(알리지 않고 건너뛰지 않는다). */
74
74
  const VALUE: Record<string, unknown> = {
75
75
  id: '__ID__',
76
76
  type: 'cut-station',
@@ -122,7 +122,7 @@ function build(c: Case): any {
122
122
  assert.ok(
123
123
  f in VALUE,
124
124
  `픽스처가 "${c.iface}.${f}" 에 실을 값을 모른다 — VALUE 에 하나 적어라.\n` +
125
- '조용히 건너뛰면 그 필드는 아무도 안 보는 필드가 되고, 그것이 이 하네스가 막으려는 실패다.'
125
+ '알리지 않고 건너뛰면 그 필드는 아무도 안 보는 필드가 되고, 그것이 이 하네스가 막으려는 실패다.'
126
126
  )
127
127
  o[f] = f === 'id' ? c.id : VALUE[f]
128
128
  }
@@ -171,7 +171,7 @@ for (const c of CASES) {
171
171
  { lost, mangled },
172
172
  { lost: [], mangled: [] },
173
173
  `${c.iface}: 잃은 사실=[${lost}] · 옮기다 값이 바뀐 사실=[${mangled}]\n` +
174
- '원본이 말한 것을 문에서 버리면 아무도 모른다 — 화면은 조용히 id 를 보인다.\n' +
174
+ '원본이 말한 것을 문에서 버리면 아무도 모른다 — 화면은 알리지 않고 id 를 보인다.\n' +
175
175
  '일부러 다른 길로 보내는 것이면 ELSEWHERE 에 그 길을 적어라(이름만 바뀌는 것은 RENAMED).'
176
176
  )
177
177
  })
@@ -12,7 +12,7 @@
12
12
  * 첫 조각(`openLiveFeed`)이 같은 선언을 그대로 읽는다.
13
13
  *
14
14
  * 이 시험이 지키는 것:
15
- * ① 선언이 없으면 조용히 지난다(정적 구조만 반영하는 트윈이 정상이다).
15
+ * ① 선언이 없으면 알리지 않고 지난다(정적 구조만 반영하는 트윈이 정상이다).
16
16
  * ② **미러에는 싣지 않는다** — 외부 실물이 진실인 트윈에 도착을 지어 주면 그것은 현실이 아니다.
17
17
  * ③ 검사를 통과하지 못한 선언은 **태우지 않고** 이유를 남긴다(잘못된 선언 하나가 서버를 내린 이력이 있다).
18
18
  * ④ 검사기가 없으면 싣지 않는다 — 검사할 수 없는 선언을 태우는 것이 가장 비싼 선택이다.
@@ -30,7 +30,7 @@ const ok = () => ({ ok: true as const })
30
30
  const bad = () => ({ ok: false as const, errorCode: 'scenario-sku-mix-required' })
31
31
  const sim = { hasScenarioEngine: true, mode: 'sim' as const }
32
32
 
33
- test('선언이 없으면 조용히 지난다 — 구조만 반영하는 트윈이 정상이다', () => {
33
+ test('선언이 없으면 알리지 않고 지난다 — 구조만 반영하는 트윈이 정상이다', () => {
34
34
  assert.deepEqual(planStimulus(undefined, sim, ok), { action: 'skip', reason: 'none' })
35
35
  assert.deepEqual(planStimulus({ endpoint: 'x' }, sim, ok), { action: 'skip', reason: 'none' })
36
36
  /* 시나리오 자리에 객체가 아닌 것이 오면 선언이 아니다(문자열을 시나리오로 읽지 않는다). */
@@ -42,7 +42,7 @@ test('관측으로 도는 트윈에는 싣지 않는다 — 현실을 지어내
42
42
  assert.deepEqual(plan, { action: 'skip', reason: 'observed' })
43
43
  })
44
44
 
45
- test('시나리오 엔진이 없으면 싣지 않는다 — 조용히 아무 일도 없게 두지 않는다', () => {
45
+ test('시나리오 엔진이 없으면 싣지 않는다 — 알리지 않고 아무 일도 없게 두지 않는다', () => {
46
46
  const plan = planStimulus({ scenario }, { hasScenarioEngine: false, mode: 'sim' }, ok)
47
47
  assert.deepEqual(plan, { action: 'skip', reason: 'no-engine' })
48
48
  })
@@ -73,7 +73,7 @@ test('선언을 얹으면서 접속 정보를 지우지 않는다', () => {
73
73
  /*
74
74
  * ── 배선 (소스로 지킨다) ────────────────────────────────────────────────────
75
75
  * 판정은 순수 함수가 하지만, 그것을 **기동 때 부르지 않으면** 아무 일도 일어나지 않는다. 그리고 못 실은
76
- * 이유는 말해야 한다 — 「선언했는데 안 실렸다」가 조용하면 사용자는 트윈이 고장 난 줄로 읽는다.
76
+ * 이유는 말해야 한다 — 「선언했는데 안 실렸다」를 말하지 않으면 사용자는 트윈이 고장 난 줄로 읽는다.
77
77
  */
78
78
  test('두 기동 경로에서 부른다 — 시뮬은 싣고, 미러는 그 사실을 말한다', () => {
79
79
  const src = readFileSync(fileURLToPath(new URL('../server/engine/twin-engine.ts', import.meta.url)), 'utf-8')
@@ -2,7 +2,7 @@
2
2
  * 사이트 발견 결과 — **"못 찾았다" 를 "없다" 로 말하지 않게.**
3
3
  *
4
4
  * 실 시스템을 연결해 트윈을 만드는 경로에서, 발견이 실패하면 화면이 "사이트를 찾지 못했습니다" 라고만
5
- * 말했다. 레퍼런스 미등록·어댑터 미상·조회 실패·정말 0건이 그 한 문장으로 접혀 있었다 — 자격이
5
+ * 말했다. 레퍼런스 미등록·어댑터 미상·조회 실패·정말 0건이 그 한 문장에 섞여 있었다 — 자격이
6
6
  * 틀렸는지 주소가 틀렸는지 알 수 없었다.
7
7
  */
8
8
  import { test } from 'node:test'
@@ -50,7 +50,7 @@ test('네 가지 사유가 서로 구별된다 — 이것이 이 작업의 요
50
50
  discoveryTimedOut('s', 15).code
51
51
  ]
52
52
  assert.deepEqual(codes, ['reference-not-found', 'adapter-unknown', 'discovery-failed', 'discovery-timeout'])
53
- assert.equal(new Set(codes).size, 4, '넷이 한 코드로 접히지 않는다')
53
+ assert.equal(new Set(codes).size, 4, '넷이 한 코드로 합쳐지지 않는다')
54
54
  })
55
55
 
56
56
  test('원 시스템이 준 사유를 감추지 않는다 — 사용자가 관리자에게 물어볼 수 있게', () => {
@@ -3,10 +3,10 @@
3
3
  *
4
4
  * 지키는 것(2026-08-01 재편):
5
5
  * ① **레퍼런스가 아는 것은 전부 나간다** — 필드를 손으로 골라 담지 않는다. 화이트리스트였을 때
6
- * 노드 parentId·작업 전량·물품 전량이 조용히 누락됐다. 계약에 속성이 생기면 자동으로 도달해야 한다.
6
+ * 노드 parentId·작업 전량·물품 전량이 오류 없이 빠졌다. 계약에 속성이 생기면 자동으로 도달해야 한다.
7
7
  * ② **표식은 도메인 낱말을 훔치지 않는다** — 엔티티 종류 표식은 `entity`, `kind` 는 도메인 것
8
8
  * (무버 cutter · 작업 putaway · 오더 workorder).
9
- * ③ **조용히 자르지 않는다** — 물품 집약이 상한에 걸리면 total·truncated 로 밝힌다.
9
+ * ③ **알리지 않고 자르지 않는다** — 물품 집약이 상한에 걸리면 total·truncated 로 밝힌다.
10
10
  */
11
11
  import { test } from 'node:test'
12
12
  import assert from 'node:assert/strict'
@@ -111,7 +111,7 @@ test('물품: 개별(태그=epc) + 집약으로 나간다 — 수량·소속 팔
111
111
  assert.equal(agg.truncated, undefined, '상한 안이면 truncated 없음')
112
112
  })
113
113
 
114
- test('물품 집약: 상한을 넘으면 잘린 사실을 밝힌다 — 조용한 절단 금지', () => {
114
+ test('물품 집약: 상한을 넘으면 잘린 사실을 밝힌다 — 들어온 것이 없는 절단 금지', () => {
115
115
  const items = Array.from({ length: ITEMS_AGGREGATE_LIMIT + 7 }, (_, i) => ({ epc: `e${i}`, location: 'rack-1' }))
116
116
  const d = buildEntityDeltas({ items }, 't1')
117
117
  const agg = byTag(d, itemsTag('t1')).data
@@ -236,7 +236,7 @@ test('수요 구간·피크·계약은 집약 태그로 나간다 — 개체의
236
236
  assert.equal(agg.data.peakSince.kW, 4200)
237
237
  /* 하루치를 매 브로드캐스팅에 실으면 채널이 그것으로 찬다 — 최근 것만 내고 전체 수를 함께 낸다. */
238
238
  assert.equal(agg.data.recent.length, 8)
239
- assert.equal(agg.data.closedTotal, 137, '자른 사실을 조용히 두지 않는다')
239
+ assert.equal(agg.data.closedTotal, 137, '자른 사실을 알리지 않고 두지 않는다')
240
240
  })
241
241
 
242
242
  test('에너지가 없는 트윈은 에너지 태그를 내지 않는다 — 없는 것을 만들지 않는다', () => {
@@ -0,0 +1,147 @@
1
+ /*
2
+ * ★ 사건 하나가 무엇을 가리키는지 뽑는다 — 360 이력이 64초인 원인을 없애는 첫 조각.
3
+ *
4
+ * ── 왜 이 모양인가 (2026-08-27 실측) ────────────────────────────────────────
5
+ * 「이 품목의 이력」 조회가 64초다. 저장된 식별자에 품목과 로트가 한 문자열로 붙어 있어서, 품목만으로는
6
+ * 찾을 방법이 없다.
7
+ *
8
+ * https://seunghwa.net/class/product-M0000618-lot-L2604100071
9
+ *
10
+ * 문자열을 자르는 규칙을 여기에 넣지 않는다 — 어느 부호가 로트를 가리키는지 연결된 시스템마다 다르고,
11
+ * chef 의 `-lot-` 을 굳히면 이 파일이 그 시스템 위에 서게 된다. 대신 대상마다 한 줄을 적고, 품목이
12
+ * 무엇인지는 그 트윈의 선언과 견주어 부르는 쪽이 답한다.
13
+ */
14
+ import { test } from 'node:test'
15
+ import assert from 'node:assert/strict'
16
+ import { subjectsOf, SUBJECT_KINDS } from '../server/service/twin-subject/event-subjects.js'
17
+
18
+ const objectEvent = (over: any = {}) => ({
19
+ eventType: 'epcis.ObjectEvent',
20
+ eventTime: '2026-08-27T05:00:00.000Z',
21
+ data: {
22
+ epcList: ['https://seunghwa.net/class/product-M0000438-lot-L2403090386'],
23
+ bizLocation: { id: 'M2R0001' },
24
+ action: 'OBSERVE',
25
+ ...over
26
+ }
27
+ })
28
+
29
+ const find = (subjects: any[], kind: string) => subjects.filter(s => s.kind === kind).map(s => s.id)
30
+
31
+ test('★ 품목과 로트를 두 줄로 적는다 — 문자열을 자르지 않고도 품목으로 찾을 수 있다', () => {
32
+ const subjects = subjectsOf(objectEvent(), {
33
+ /* 선언과 견주어 품목을 찾는 일은 부르는 쪽이 한다 — 그 트윈의 선언을 아는 것이 그쪽이다. */
34
+ itemOf: epc => (epc.includes('product-M0000438') ? 'M0000438' : null)
35
+ })
36
+ assert.deepEqual(find(subjects, 'lot'), ['https://seunghwa.net/class/product-M0000438-lot-L2403090386'])
37
+ assert.deepEqual(find(subjects, 'item'), ['M0000438'])
38
+ })
39
+
40
+ test('★ 품목을 찾지 못하면 품목 줄을 만들지 않는다 — 짐작으로 이으면 남의 이력에 섞인다', () => {
41
+ const subjects = subjectsOf(objectEvent(), { itemOf: () => null })
42
+ assert.equal(find(subjects, 'lot').length, 1, '개체는 그대로 적는다')
43
+ assert.deepEqual(find(subjects, 'item'), [], '품목은 비운다')
44
+ })
45
+
46
+ test('★ 찾은 값이 개체 식별자와 같으면 품목이 아니다 — itemRefOf 는 URI 를 그대로 돌려준다', () => {
47
+ /*
48
+ * `itemRefOf` 는 SGTIN 이 아닌 모양에서 문자열을 그대로 낸다(자르는 판단을 하지 않으므로). 그 값을
49
+ * 품목으로 적으면 로트와 같은 줄이 두 종류로 생기고, 품목 이력이 로트 이력과 같아진다.
50
+ */
51
+ const epc = 'https://seunghwa.net/class/product-M0000438-lot-L2403090386'
52
+ const subjects = subjectsOf(objectEvent(), { itemOf: () => epc })
53
+ assert.deepEqual(find(subjects, 'item'), [])
54
+ })
55
+
56
+ test('★ 한 사건이 여러 대상을 가리킨다 — 자리·설비·오더·작업', () => {
57
+ const subjects = subjectsOf({
58
+ eventType: 'task.status',
59
+ eventTime: '2026-08-27T05:00:00.000Z',
60
+ data: { taskId: 'tk-1', moverId: 'mixer-1', toNode: 'M2R0001', orderId: 'ORD-9' }
61
+ })
62
+ assert.deepEqual(find(subjects, 'task'), ['tk-1'])
63
+ assert.deepEqual(find(subjects, 'equipment'), ['mixer-1'])
64
+ assert.deepEqual(find(subjects, 'order'), ['ORD-9'])
65
+ assert.ok(find(subjects, 'location').length <= 1, '자리는 한 줄이다')
66
+ })
67
+
68
+ test('★ 가리키는 것이 없으면 빈 배열이다 — 없는 대상을 만들지 않는다', () => {
69
+ const subjects = subjectsOf({ eventType: 'energy.demand.window', data: { maxKW: 3 } })
70
+ assert.deepEqual(subjects, [])
71
+ })
72
+
73
+ test('★ 작업이 오간 두 자리를 다 적는다 — 승격 컬럼은 둘 중 하나만 담을 수 있어 비워 둔다', () => {
74
+ /*
75
+ * `locationOf` 는 EPCIS 자리만 읽는다. 작업이 A→B 로 갈 때 컬럼 하나에 둘을 넣을 수 없으므로
76
+ * 아무것도 넣지 않는다. 이 표는 자리마다 줄이 하나라서 둘을 함께 담는다.
77
+ */
78
+ const subjects = subjectsOf({
79
+ eventType: 'task.status',
80
+ data: { taskId: 'tk-1', fromNode: 'M2R0001', toNode: 'M2R0002' }
81
+ })
82
+ assert.deepEqual(find(subjects, 'location'), ['M2R0001', 'M2R0002'])
83
+ })
84
+
85
+ test('★ 같은 대상을 두 번 적지 않는다 — 출발지와 도착지가 같을 수 있다', () => {
86
+ const subjects = subjectsOf({
87
+ eventType: 'task.status',
88
+ data: { taskId: 'tk-1', fromNode: 'M2R0001', toNode: 'M2R0001' }
89
+ })
90
+ assert.deepEqual(find(subjects, 'location'), ['M2R0001'])
91
+ })
92
+
93
+ test('종류 목록이 닫혀 있다 — 열어 두면 넣는 쪽마다 다른 이름을 쓴다', () => {
94
+ assert.deepEqual([...SUBJECT_KINDS], ['item', 'lot', 'order', 'location', 'equipment', 'task'])
95
+ })
96
+
97
+ test('너무 긴 식별자는 적지 않는다 — 자르면 그 줄이 다른 대상을 가리킨다', () => {
98
+ const long = 'x'.repeat(300)
99
+ const subjects = subjectsOf({ eventType: 'task.status', data: { taskId: long } })
100
+ assert.deepEqual(find(subjects, 'task'), [], '알리지 않고 자르지 않고 아예 만들지 않는다')
101
+ })
102
+
103
+ test('빈 값과 공백은 대상이 아니다 — 결측을 대상으로 만들지 않는다', () => {
104
+ const subjects = subjectsOf({ eventType: 'task.status', data: { taskId: ' ', moverId: '' } })
105
+ assert.deepEqual(subjects, [])
106
+ })
107
+
108
+ test('★ 클래스로 온 사건은 품목 줄만 만든다 — 개체가 없는 사건이다', () => {
109
+ /*
110
+ * 실측한 chef 의 사건이다. 무게로 담는 현장이라 개체 목록이 비어 있고 수량 목록에 클래스가 있다.
111
+ * 그것을 로트로 적으면 뜻이 틀린다 — 개체가 아니라 품목이다.
112
+ */
113
+ const subjects = subjectsOf({
114
+ eventType: 'epcis.ObjectEvent',
115
+ data: {
116
+ epcList: [],
117
+ quantityList: [{ epcClass: 'https://seunghwa.net/class/product-M0000879', quantity: 11797.6, uom: 'KGM' }],
118
+ bizLocation: { id: 'P2F0001' }
119
+ }
120
+ })
121
+ assert.deepEqual(find(subjects, 'lot'), [], '개체 줄을 만들지 않는다')
122
+ assert.deepEqual(find(subjects, 'item'), ['https://seunghwa.net/class/product-M0000879'])
123
+ assert.deepEqual(find(subjects, 'location'), ['P2F0001'])
124
+ })
125
+
126
+ test('★ 클래스도 선언과 견주어 품목 코드로 줄인다 — 선언이 답하면 그것을 쓴다', () => {
127
+ const subjects = subjectsOf(
128
+ {
129
+ eventType: 'epcis.ObjectEvent',
130
+ data: { epcList: [], quantityList: [{ epcClass: 'https://seunghwa.net/class/product-M0000879' }] }
131
+ },
132
+ { itemOf: epc => (epc.includes('M0000879') ? 'M0000879' : null) }
133
+ )
134
+ assert.deepEqual(find(subjects, 'item'), ['M0000879'])
135
+ })
136
+
137
+ test('개체와 클래스가 함께 오면 둘 다 적는다 — 한 사건이 둘을 가리킬 수 있다', () => {
138
+ const subjects = subjectsOf({
139
+ eventType: 'epcis.ObjectEvent',
140
+ data: {
141
+ epcList: ['https://seunghwa.net/class/product-M0000438-lot-L1'],
142
+ quantityList: [{ epcClass: 'https://seunghwa.net/class/product-M0000879' }]
143
+ }
144
+ })
145
+ assert.deepEqual(find(subjects, 'lot'), ['https://seunghwa.net/class/product-M0000438-lot-L1'])
146
+ assert.deepEqual(find(subjects, 'item'), ['https://seunghwa.net/class/product-M0000879'])
147
+ })
@@ -5,9 +5,9 @@
5
5
  * 예전에는 문자열 컬럼이었고, "UTC ISO 만 들어오니 사전식 비교가 맞는다" 는 전제 위에서 정렬·범위
6
6
  * 질의가 돌았다. 그 전제는 컬럼 어디에도 적혀 있지 않았다.
7
7
  *
8
- * 날짜 컬럼으로 바꾸면서 **조용한 함정**이 하나 드러났다: sqlite 는 datetime 을 공백 구분 형식
8
+ * 날짜 컬럼으로 바꾸면서 **들어온 것이 없는 함정**이 하나 드러났다: sqlite 는 datetime 을 공백 구분 형식
9
9
  * (`2026-08-07 11:00:00.000`)으로 저장하는데, 화면은 ISO 문자열(`…T11:00:00.000Z`)을 보낸다.
10
- * 문자열 그대로 비교하면 사전식으로 어긋나 **오류 없이 0건**이 나온다 — 원장 시간창이 조용히
10
+ * 문자열 그대로 비교하면 사전식으로 어긋나 **오류 없이 0건**이 나온다 — 원장 시간창이 알리지 않고
11
11
  * 비어 버리고, 사용자는 "그 시간에 아무 일도 없었다" 로 읽는다.
12
12
  *
13
13
  * 그래서 두 가지를 못 박는다: 저장 형식이 실제로 그렇다는 것과, 경계에서 날짜로 옮기면 걸린다는 것.
@@ -48,7 +48,7 @@ test('sqlite 는 datetime 을 공백 구분 형식으로 저장한다 — ISO
48
48
  await ds.destroy()
49
49
  })
50
50
 
51
- test('ISO 문자열을 그대로 넘기면 **조용히 0건** — 오류가 아니라 빈 결과라 더 위험하다', async () => {
51
+ test('ISO 문자열을 그대로 넘기면 **알리지 않고 0건** — 오류가 아니라 빈 결과라 더 위험하다', async () => {
52
52
  const { ds, repo } = await seeded()
53
53
  const rows = await repo.find({
54
54
  where: { eventTime: Between('2026-08-07T10:00:00.000Z', '2026-08-07T12:00:00.000Z') }
@@ -0,0 +1,155 @@
1
+ /*
2
+ * ★ 여러 종류를 시각순으로 — 거르는 축과 정렬 축이 어긋나면 색인이 버려진다.
3
+ *
4
+ * ── 실측 (2026-08-28 · 승화푸드 저널 3,374,894행) ───────────────────────────
5
+ * 종류 8개 IN + 시각순 50건 SEARCH USING INDEX ix_twin_event_1 (domain, instance) 37.181초 · 0건
6
+ * 같은 조건 · 정렬 없음 SEARCH USING INDEX ix_twin_event_7 (…, event_type=?) 0.003초
7
+ * 종류 하나 + 시각순 SEARCH USING INDEX ix_twin_event_7 (…, event_type=?) 0.003초
8
+ *
9
+ * 정렬을 붙이는 순간 여러 종류의 순서를 합칠 방법이 없어 시각 색인으로 337만 행을 걸으며 행마다 종류를
10
+ * 확인한다. 그 트윈에 에너지 사건은 **0건**이다 — 없는 것을 찾느라 37초를 쓴다.
11
+ *
12
+ * 360 이력이 64초였던 것과 같은 부류다. 고침도 같은 성질이다: **있는 색인을 쓰게 한다.**
13
+ */
14
+ import { test } from 'node:test'
15
+ import assert from 'node:assert/strict'
16
+ import { readFileSync } from 'node:fs'
17
+ import { join } from 'node:path'
18
+
19
+ import { splitShape } from '../server/service/twin-journal/event-type-split-shape.js'
20
+
21
+ const dir = (...p: string[]) => join(import.meta.dirname, '..', 'server', ...p)
22
+ const split = readFileSync(dir('service', 'twin-journal', 'event-type-split.ts'), 'utf8')
23
+ const shape = readFileSync(dir('service', 'twin-journal', 'event-type-split-shape.ts'), 'utf8')
24
+ const journal = readFileSync(dir('service', 'twin-journal', 'twin-journal-query.ts'), 'utf8')
25
+
26
+ const DESC = [{ name: 'eventTime', desc: true }]
27
+ const ENERGY = ['energy.measured', 'energy.equipment', 'energy.demand.window', 'energy.peak']
28
+
29
+ test('★ 에너지 원장이 보내는 모양이 이 갈래로 간다', () => {
30
+ const shape = splitShape({ filters: [{ name: 'eventType', operator: 'in', value: ENERGY }] }, DESC)
31
+ assert.ok(shape)
32
+ assert.deepEqual(shape!.eventTypes, ENERGY)
33
+ })
34
+
35
+ test('시간창이 함께 와도 간다 — 화면이 구간을 지정한다', () => {
36
+ const shape = splitShape(
37
+ {
38
+ filters: [
39
+ { name: 'eventType', operator: 'in', value: ENERGY },
40
+ { name: 'eventTime', operator: 'between', value: ['2026-08-01T00:00:00.000Z', '2026-08-02T00:00:00.000Z'] }
41
+ ]
42
+ },
43
+ DESC
44
+ )
45
+ assert.ok(shape)
46
+ assert.equal(shape!.fromTime?.toISOString(), '2026-08-01T00:00:00.000Z')
47
+ assert.equal(shape!.untilTime?.toISOString(), '2026-08-02T00:00:00.000Z')
48
+ })
49
+
50
+ test('★ 맞지 않는 물음은 일반 경로로 보낸다 — 억지로 답하면 목록이 알리지 않고 달라진다', () => {
51
+ const cases: [string, any, any[]][] = [
52
+ ['종류가 하나', { filters: [{ name: 'eventType', operator: 'in', value: ['a'] }] }, DESC],
53
+ ['종류가 너무 많다', { filters: [{ name: 'eventType', operator: 'in', value: Array.from({ length: 13 }, (_, i) => `t${i}`) }] }, DESC],
54
+ ['종류 필터가 없다', { filters: [] }, DESC],
55
+ ['정확일치', { filters: [{ name: 'eventType', operator: 'eq', value: 'a' }] }, DESC],
56
+ ['제외 목록', { filters: [{ name: 'eventType', operator: 'notin_with_null', value: ENERGY }] }, DESC],
57
+ ['자유 검색어', { search: '전력', filters: [{ name: 'eventType', operator: 'in', value: ENERGY }] }, DESC],
58
+ ['다른 필터가 섞였다', { filters: [{ name: 'eventType', operator: 'in', value: ENERGY }, { name: 'epc', operator: 'search', value: 'x' }] }, DESC],
59
+ ['리비전 정렬', { filters: [{ name: 'eventType', operator: 'in', value: ENERGY }] }, [{ name: 'revision', desc: true }]],
60
+ ['오름차순', { filters: [{ name: 'eventType', operator: 'in', value: ENERGY }] }, [{ name: 'eventTime', desc: false }]],
61
+ ['못 읽는 시각', { filters: [{ name: 'eventType', operator: 'in', value: ENERGY }, { name: 'eventTime', operator: 'lte', value: '어제' }] }, DESC]
62
+ ]
63
+ for (const [why, params, sortings] of cases) {
64
+ assert.equal(splitShape(params, sortings), null, `${why} 는 일반 경로로 가야 한다`)
65
+ }
66
+ })
67
+
68
+ test('★ 종류를 정확일치로 묻는다 — 이 한 줄이 색인을 쓰게 한다', () => {
69
+ assert.match(split, /andWhere\('e\.eventType = :eventType', \{ eventType \}\)/)
70
+ assert.doesNotMatch(split, /eventType IN/, '나눈 뒤에 다시 여러 값으로 묻지 않는다')
71
+ /* 정렬 축이 색인의 마지막 칸과 같아야 한다. */
72
+ assert.match(split, /orderBy\('e\.eventTime', 'DESC'\)/)
73
+ })
74
+
75
+ test('★ 한 갈래라도 못 읽으면 반쪽을 전부인 것처럼 내지 않는다', () => {
76
+ assert.match(split, /if \(!page\) return null/)
77
+ })
78
+
79
+ test('★ 이어 읽기 지점을 모든 갈래에 같이 건다', () => {
80
+ /* 갈래마다 다르게 걸면 어떤 종류는 이미 지나간 구간을 다시 낸다. */
81
+ assert.match(split, /e\.eventTime < :curAt OR \(e\.eventTime = :curAt AND e\.\$\{tiebreak\} < :curTie\)/)
82
+ assert.match(split, /if \(!cursor\) return null/, '못 읽는 커서로는 답하지 않는다')
83
+ })
84
+
85
+ test('★ 나눌 수를 제한한다 — 나누는 것이 손해가 되는 지점이 있다', () => {
86
+ assert.match(shape, /export const MAX_SPLIT = 24/)
87
+ assert.match(split, /if \(!pairs \|\| pairs > MAX_SPLIT\) return null/)
88
+ })
89
+
90
+ test('★ 화면이 부르는 자리에 붙어 있다', () => {
91
+ assert.match(journal, /const split = splitShape\(effective, effective\.sortings \?\? \[\]\)/)
92
+ assert.match(journal, /await eventTypeSplitPage\(domain\.id, instanceIds, split, limit, tiebreak, effective\.pagination\?\.after\)/)
93
+ /* 파생 테이블 갈래보다 뒤, 일반 저널 질의보다 앞이어야 한다. */
94
+ assert.ok(journal.indexOf('readModelPage(domain.id') < journal.indexOf('splitShape(effective'))
95
+ assert.ok(journal.indexOf('splitShape(effective') < journal.indexOf('const qb = getQueryBuilderFromListParams'))
96
+ })
97
+
98
+ test('★ 걷을 조건이 적혀 있다 — 부르는 데가 없어지면 지운다', () => {
99
+ /*
100
+ * 앱에서 종류를 여러 값으로 보내는 곳은 에너지 원장 한 곳뿐이다. 그 화면이 종류를 하나씩 고르는
101
+ * 모양으로 바뀌면 이 갈래는 아무도 부르지 않는다 — 그때 지워야 한다. 그 조건을 코드에 적어 두지
102
+ * 않으면 아무것도 하지 않는 갈래가 무언가를 받쳐 주는 것처럼 남는다.
103
+ */
104
+ assert.match(split, /이 갈래를 걷을 조건/)
105
+ assert.match(split, /그때 \*\*지운다\.\*\*/)
106
+ })
107
+
108
+ /* ── 공정 단계 칩 (2026-08-29) ─────────────────────────────────────────────
109
+ *
110
+ * ★ 칩을 고른 순간 이 갈래가 꺼져서, **고르지 않은 쪽이 더 빨랐다.**
111
+ *
112
+ * 원장은 종류 넷을 담아 묻고 그 위에 단계 칩을 겹친다. 앞 판은 `bizStep` 필터를 만나면 모양을
113
+ * 알아보지 못해 예전 경로로 떨어졌다 — 좁혀 물으면 느려지는 화면이 된다.
114
+ */
115
+
116
+ test('공정 단계 하나로 좁혀도 갈래 경로가 답한다', () => {
117
+ const shape = splitShape(
118
+ {
119
+ filters: [
120
+ { name: 'eventType', operator: 'in', value: ['epcis.ObjectEvent', 'epcis.AggregationEvent'] },
121
+ { name: 'bizStep', operator: 'eq', value: 'shipping' }
122
+ ]
123
+ } as any,
124
+ [{ name: 'eventTime', desc: true }] as any
125
+ )
126
+ assert.ok(shape, '이 모양을 알아봐야 한다')
127
+ assert.equal(shape?.bizStep, 'shipping')
128
+ assert.deepEqual(shape?.eventTypes, ['epcis.ObjectEvent', 'epcis.AggregationEvent'])
129
+ })
130
+
131
+ test('단계를 여러 값으로 주면 답하지 않는다 — 갈래가 곱해진다', () => {
132
+ const shape = splitShape(
133
+ {
134
+ filters: [
135
+ { name: 'eventType', operator: 'in', value: ['epcis.ObjectEvent', 'epcis.AggregationEvent'] },
136
+ { name: 'bizStep', operator: 'in', value: ['shipping', 'receiving'] }
137
+ ]
138
+ } as any,
139
+ [{ name: 'eventTime', desc: true }] as any
140
+ )
141
+ assert.equal(shape, null)
142
+ })
143
+
144
+ test('빈 단계 값은 받지 않는다 — 모든 행을 뜻하는지 없는 단계를 뜻하는지 알 수 없다', () => {
145
+ const shape = splitShape(
146
+ {
147
+ filters: [
148
+ { name: 'eventType', operator: 'in', value: ['epcis.ObjectEvent', 'epcis.AggregationEvent'] },
149
+ { name: 'bizStep', operator: 'eq', value: ' ' }
150
+ ]
151
+ } as any,
152
+ [{ name: 'eventTime', desc: true }] as any
153
+ )
154
+ assert.equal(shape, null)
155
+ })
@@ -0,0 +1,53 @@
1
+ /*
2
+ * ★ 저장본 뒤의 사건 — 계산했든 못 했든 **말한다**.
3
+ *
4
+ * ── 무엇이 침묵했나 (2026-08-28 실측) ───────────────────────────────────────
5
+ * 미러는 저장본에서 상태를 되세우고 그 뒤의 사건만 따라 계산한다. 그 자리가 이랬다.
6
+ *
7
+ * 10:34:37 found cached state → revision 3372228
8
+ * 10:34:37 resumed from the stored fold seed at revision 3372228 — items 2665 · orders 4794
9
+ *
10
+ * 그 시각 저널의 머리는 3,374,894였다. 2,666건이 저장본보다 앞서 있었는데 **계산했다는 줄도, 못
11
+ * 했다는 줄도 없었다.** 갈래가 셋인데 말하는 갈래가 하나뿐이었기 때문이다.
12
+ *
13
+ * ① 계산해서 나아갔다 → 말한다
14
+ * ② 계산이 오류로 실패했다 → 말한다
15
+ * ③ 오류 없이 끝났는데 그대로 → **아무 말도 없었다** ← 여기
16
+ * ④ 저널 머리를 못 읽었다 → **아무 말도 없었다** ← 여기
17
+ *
18
+ * ③④에서 트윈은 그 사건들의 결과 없이 뜨고, 화면은 그것을 「없는 사실」로 그린다.
19
+ */
20
+ import { test } from 'node:test'
21
+ import assert from 'node:assert/strict'
22
+ import { readFileSync } from 'node:fs'
23
+ import { join } from 'node:path'
24
+
25
+ const engine = readFileSync(join(import.meta.dirname, '..', 'server', 'engine', 'twin-engine.ts'), 'utf8')
26
+
27
+ test('★ 저널 머리를 못 읽었으면 그 사실을 말하고 그대로 뜬다', () => {
28
+ assert.match(engine, /if \(!tip\) \{/, '못 읽은 갈래가 있어야 한다')
29
+ assert.match(engine, /could not read the journal tip/, '무엇을 못 했는지 말해야 한다')
30
+ assert.match(engine, /without folding what came after it/, '무엇을 잃는지 말해야 한다')
31
+ })
32
+
33
+ test('★ 계산이 아무 일도 하지 않았으면 그것도 말한다', () => {
34
+ assert.match(engine, /\} else if \(!failed\) \{/, '오류가 아닌데 나아가지 않은 갈래가 있어야 한다')
35
+ assert.match(engine, /left the seed at revision/)
36
+ /* 오류로 실패한 것을 두 번 말하지 않는다 — 이미 위에서 말했다. */
37
+ assert.match(engine, /let failed: string \| undefined/)
38
+ })
39
+
40
+ test('★ 부팅마다 부르는 질의를 관계로 묻지 않는다 — 이 파일이 스스로 금지한 모양이다', () => {
41
+ /*
42
+ * `findOne({ where: { domain: { id } } })` 는 안쪽에 상한이 없는 두 단계 질의가 된다(실측 12초,
43
+ * 저널 375만 행). `tipOf` 는 부팅마다 트윈마다 불리므로 그 한 번이 부팅을 붙잡는다.
44
+ */
45
+ const from = engine.indexOf('private static async tipOf')
46
+ assert.ok(from > 0, 'tipOf 가 있어야 한다')
47
+ const tipOf = engine.slice(from, from + 2600)
48
+ /* 주석에 인용된 나쁜 모양과 헷갈리지 않게, 실제 코드 모양만 본다. */
49
+ assert.doesNotMatch(tipOf, /findOne\(\{ where: \{ domain: \{ id: domainId \}/, '관계로 묻는 모양이 남아 있다')
50
+ assert.match(tipOf, /createQueryBuilder\('e'\)/)
51
+ assert.match(tipOf, /orderBy\('e\.revision', 'DESC'\)/)
52
+ assert.match(tipOf, /\.limit\(1\)/)
53
+ })
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * 예측 지표 — **스냅샷에서 무엇을 세는가.**
3
3
  *
4
- * 여기서 틀리면 예측 그래프가 조용히 다른 것을 그린다(오류 없이 곡선이 나온다). 특히 에너지 지표는
4
+ * 여기서 틀리면 예측 그래프가 알리지 않고 다른 것을 그린다(오류 없이 곡선이 나온다). 특히 에너지 지표는
5
5
  * 「없음」과 「0」이 같은 그림이 되기 쉬워서, 그 경계를 명시로 잡아 둔다.
6
6
  */
7
7
  import { test } from 'node:test'
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * 예측 틱 크기 — **지평선이 정하고, 양끝은 묶인다.**
3
3
  *
4
- * 이 규칙이 틀리면 예측이 조용히 비싸지거나(1초 고정 시절 9,000틱·28.5초) 조용히 성글어진다
4
+ * 이 규칙이 틀리면 예측이 알리지 않고 비싸지거나(1초 고정 시절 9,000틱·28.5초) 알리지 않고 성글어진다
5
5
  * (짧은 작업이 한 틱에 삼켜진다). 둘 다 화면에는 그럴듯한 숫자로 나오므로 시험이 지킨다.
6
6
  */
7
7
  import { test } from 'node:test'