@things-factory/headless-twin 10.0.15 → 10.0.18

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 (513) 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 +18 -82
  5. package/dist-server/engine/canonical-ingest.js +183 -18
  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 +4 -4
  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 +90 -10
  17. package/dist-server/engine/ingest-health.js +125 -16
  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 +212 -14
  25. package/dist-server/engine/kpi-fold.js +374 -21
  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 +281 -36
  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 +15 -14
  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 +3 -3
  52. package/dist-server/engine/model-vocabulary.js.map +1 -1
  53. package/dist-server/engine/oee-accumulator.d.ts +6 -5
  54. package/dist-server/engine/oee-accumulator.js +6 -5
  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 +307 -54
  78. package/dist-server/engine/twin-engine.js +822 -196
  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 +30 -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 +116 -9
  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 +76 -13
  131. package/dist-server/service/reference/reference-live.js.map +1 -1
  132. package/dist-server/service/reference/reference-master.d.ts +25 -16
  133. package/dist-server/service/reference/reference-master.js +35 -13
  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 +12 -12
  137. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  138. package/dist-server/service/reference/template-registry.d.ts +1 -1
  139. package/dist-server/service/reference/template-registry.js.map +1 -1
  140. package/dist-server/service/reference/twin-reference.d.ts +3 -3
  141. package/dist-server/service/reference/twin-reference.js.map +1 -1
  142. package/dist-server/service/twin-attention/twin-attention-query.d.ts +1 -1
  143. package/dist-server/service/twin-attention/twin-attention-query.js +1 -1
  144. package/dist-server/service/twin-attention/twin-attention-query.js.map +1 -1
  145. package/dist-server/service/twin-audit/command-audit.d.ts +1 -1
  146. package/dist-server/service/twin-audit/command-audit.js.map +1 -1
  147. package/dist-server/service/twin-audit/twin-audit-event.d.ts +1 -1
  148. package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -1
  149. package/dist-server/service/twin-audit/twin-audit-query.js +1 -1
  150. package/dist-server/service/twin-audit/twin-audit-query.js.map +1 -1
  151. package/dist-server/service/twin-backfill/backfill-period-facts.d.ts +24 -0
  152. package/dist-server/service/twin-backfill/backfill-period-facts.js +96 -0
  153. package/dist-server/service/twin-backfill/backfill-period-facts.js.map +1 -0
  154. package/dist-server/service/twin-backfill/backfill-shape.d.ts +34 -0
  155. package/dist-server/service/twin-backfill/backfill-shape.js +75 -0
  156. package/dist-server/service/twin-backfill/backfill-shape.js.map +1 -0
  157. package/dist-server/service/twin-backfill/index.d.ts +2 -0
  158. package/dist-server/service/twin-backfill/index.js +6 -0
  159. package/dist-server/service/twin-backfill/index.js.map +1 -0
  160. package/dist-server/service/twin-backfill/twin-backfill-resolver.d.ts +10 -0
  161. package/dist-server/service/twin-backfill/twin-backfill-resolver.js +44 -0
  162. package/dist-server/service/twin-backfill/twin-backfill-resolver.js.map +1 -0
  163. package/dist-server/service/twin-control/twin-control-mutation.js +3 -3
  164. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  165. package/dist-server/service/twin-event/twin-event-keys.d.ts +1 -1
  166. package/dist-server/service/twin-event/twin-event-keys.js +2 -2
  167. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  168. package/dist-server/service/twin-event/twin-event-type.d.ts +1 -0
  169. package/dist-server/service/twin-event/twin-event-type.js +8 -1
  170. package/dist-server/service/twin-event/twin-event-type.js.map +1 -1
  171. package/dist-server/service/twin-event/twin-event.d.ts +2 -2
  172. package/dist-server/service/twin-event/twin-event.js +32 -4
  173. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  174. package/dist-server/service/twin-forecast/forecast-metrics.js +1 -1
  175. package/dist-server/service/twin-forecast/forecast-metrics.js.map +1 -1
  176. package/dist-server/service/twin-forecast/gap-analytics.d.ts +1 -1
  177. package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
  178. package/dist-server/service/twin-forecast/twin-forecast-query.js +29 -11
  179. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  180. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.d.ts +1 -1
  181. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js +1 -1
  182. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js.map +1 -1
  183. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js +1 -1
  184. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js.map +1 -1
  185. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  186. package/dist-server/service/twin-journal/event-type-split-shape.d.ts +50 -0
  187. package/dist-server/service/twin-journal/event-type-split-shape.js +111 -0
  188. package/dist-server/service/twin-journal/event-type-split-shape.js.map +1 -0
  189. package/dist-server/service/twin-journal/event-type-split.d.ts +15 -0
  190. package/dist-server/service/twin-journal/event-type-split.js +102 -0
  191. package/dist-server/service/twin-journal/event-type-split.js.map +1 -0
  192. package/dist-server/service/twin-journal/journal-order.d.ts +15 -0
  193. package/dist-server/service/twin-journal/journal-order.js +17 -0
  194. package/dist-server/service/twin-journal/journal-order.js.map +1 -0
  195. package/dist-server/service/twin-journal/twin-journal-query.d.ts +5 -5
  196. package/dist-server/service/twin-journal/twin-journal-query.js +232 -21
  197. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  198. package/dist-server/service/twin-lifecycle/domain-catalog.js +2 -1
  199. package/dist-server/service/twin-lifecycle/domain-catalog.js.map +1 -1
  200. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +27 -1
  201. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +59 -3
  202. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  203. package/dist-server/service/twin-metrics/twin-metrics-query.js +14 -1
  204. package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
  205. package/dist-server/service/twin-model/axis-journal-evidence.js +1 -1
  206. package/dist-server/service/twin-model/axis-journal-evidence.js.map +1 -1
  207. package/dist-server/service/twin-model/epcis-coverage.js +34 -3
  208. package/dist-server/service/twin-model/epcis-coverage.js.map +1 -1
  209. package/dist-server/service/twin-model/iec61850-coverage.js +1 -1
  210. package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -1
  211. package/dist-server/service/twin-model/isa95-coverage.d.ts +12 -9
  212. package/dist-server/service/twin-model/isa95-coverage.js +33 -9
  213. package/dist-server/service/twin-model/isa95-coverage.js.map +1 -1
  214. package/dist-server/service/twin-model/item-ref.js +2 -2
  215. package/dist-server/service/twin-model/item-ref.js.map +1 -1
  216. package/dist-server/service/twin-model/name-index.d.ts +1 -1
  217. package/dist-server/service/twin-model/name-index.js +2 -2
  218. package/dist-server/service/twin-model/name-index.js.map +1 -1
  219. package/dist-server/service/twin-model/project-structure.d.ts +1 -1
  220. package/dist-server/service/twin-model/project-structure.js +7 -6
  221. package/dist-server/service/twin-model/project-structure.js.map +1 -1
  222. package/dist-server/service/twin-model/twin-equipment.d.ts +2 -2
  223. package/dist-server/service/twin-model/twin-equipment.js +3 -3
  224. package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
  225. package/dist-server/service/twin-model/twin-lineage-query.js +16 -5
  226. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
  227. package/dist-server/service/twin-model/twin-location.d.ts +1 -1
  228. package/dist-server/service/twin-model/twin-location.js.map +1 -1
  229. package/dist-server/service/twin-model/twin-model-item-query.d.ts +3 -3
  230. package/dist-server/service/twin-model/twin-model-item-query.js +15 -15
  231. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
  232. package/dist-server/service/twin-model/twin-model-mutation.d.ts +2 -2
  233. package/dist-server/service/twin-model/twin-model-mutation.js +4 -4
  234. package/dist-server/service/twin-model/twin-model-mutation.js.map +1 -1
  235. package/dist-server/service/twin-model/twin-model-query.js +51 -30
  236. package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
  237. package/dist-server/service/twin-model/twin-model-tree-query.js +23 -1
  238. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
  239. package/dist-server/service/twin-model/twin-operation.d.ts +1 -1
  240. package/dist-server/service/twin-model/twin-operation.js +1 -1
  241. package/dist-server/service/twin-model/twin-operation.js.map +1 -1
  242. package/dist-server/service/twin-space/move-space.js +2 -2
  243. package/dist-server/service/twin-space/move-space.js.map +1 -1
  244. package/dist-server/service/twin-space/representation-role.d.ts +2 -0
  245. package/dist-server/service/twin-space/representation-role.js +18 -0
  246. package/dist-server/service/twin-space/representation-role.js.map +1 -0
  247. package/dist-server/service/twin-space/space-integrity.d.ts +1 -1
  248. package/dist-server/service/twin-space/space-integrity.js.map +1 -1
  249. package/dist-server/service/twin-space/twin-space-representation.d.ts +2 -0
  250. package/dist-server/service/twin-space/twin-space-representation.js +16 -0
  251. package/dist-server/service/twin-space/twin-space-representation.js.map +1 -1
  252. package/dist-server/service/twin-space/twin-space-resolver.d.ts +2 -2
  253. package/dist-server/service/twin-space/twin-space-resolver.js +37 -8
  254. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  255. package/dist-server/service/twin-structure/twin-structure.js +3 -3
  256. package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
  257. package/dist-server/service/twin-subject/event-subjects.d.ts +43 -0
  258. package/dist-server/service/twin-subject/event-subjects.js +137 -0
  259. package/dist-server/service/twin-subject/event-subjects.js.map +1 -0
  260. package/dist-server/service/twin-subject/index.d.ts +9 -0
  261. package/dist-server/service/twin-subject/index.js +16 -0
  262. package/dist-server/service/twin-subject/index.js.map +1 -0
  263. package/dist-server/service/twin-subject/item-resolver.d.ts +10 -0
  264. package/dist-server/service/twin-subject/item-resolver.js +93 -0
  265. package/dist-server/service/twin-subject/item-resolver.js.map +1 -0
  266. package/dist-server/service/twin-subject/subject-query-shape.d.ts +22 -0
  267. package/dist-server/service/twin-subject/subject-query-shape.js +88 -0
  268. package/dist-server/service/twin-subject/subject-query-shape.js.map +1 -0
  269. package/dist-server/service/twin-subject/twin-subject-backfill.d.ts +23 -0
  270. package/dist-server/service/twin-subject/twin-subject-backfill.js +159 -0
  271. package/dist-server/service/twin-subject/twin-subject-backfill.js.map +1 -0
  272. package/dist-server/service/twin-subject/twin-subject-event.d.ts +14 -0
  273. package/dist-server/service/twin-subject/twin-subject-event.js +111 -0
  274. package/dist-server/service/twin-subject/twin-subject-event.js.map +1 -0
  275. package/dist-server/service/twin-subject/twin-subject-fast-path.d.ts +39 -0
  276. package/dist-server/service/twin-subject/twin-subject-fast-path.js +295 -0
  277. package/dist-server/service/twin-subject/twin-subject-fast-path.js.map +1 -0
  278. package/dist-server/service/twin-subject/twin-subject-query.d.ts +27 -0
  279. package/dist-server/service/twin-subject/twin-subject-query.js +232 -0
  280. package/dist-server/service/twin-subject/twin-subject-query.js.map +1 -0
  281. package/dist-server/service/twin-subject/twin-subject-writer.d.ts +27 -0
  282. package/dist-server/service/twin-subject/twin-subject-writer.js +166 -0
  283. package/dist-server/service/twin-subject/twin-subject-writer.js.map +1 -0
  284. package/dist-server/service/twin-target/twin-target-resolver.js +1 -1
  285. package/dist-server/service/twin-target/twin-target-resolver.js.map +1 -1
  286. package/dist-shared/entity-delta.d.ts +1 -1
  287. package/dist-shared/entity-delta.js +4 -4
  288. package/dist-shared/entity-delta.js.map +1 -1
  289. package/dist-shared/touched-items.js +2 -2
  290. package/dist-shared/touched-items.js.map +1 -1
  291. package/dist-shared/twin-level.js +1 -1
  292. package/dist-shared/twin-level.js.map +1 -1
  293. package/package.json +7 -6
  294. package/server/engine/attention-digest.ts +9 -9
  295. package/server/engine/canonical-ingest.ts +202 -87
  296. package/server/engine/command-routing.ts +1 -1
  297. package/server/engine/declared-stimulus.ts +1 -1
  298. package/server/engine/energy-topology.ts +2 -2
  299. package/server/engine/ingest-dedupe.ts +149 -0
  300. package/server/engine/ingest-health.ts +171 -22
  301. package/server/engine/integration-probes.ts +7 -7
  302. package/server/engine/kpi-baseline.ts +4 -4
  303. package/server/engine/kpi-fold.ts +592 -41
  304. package/server/engine/kpi-query.ts +307 -48
  305. package/server/engine/live-feed-registry.ts +1 -1
  306. package/server/engine/load-meter.ts +1 -1
  307. package/server/engine/local-declarations.ts +15 -14
  308. package/server/engine/log.ts +1 -1
  309. package/server/engine/measured-estimator.ts +1 -1
  310. package/server/engine/measured-yield.ts +1 -1
  311. package/server/engine/model-basis.ts +2 -2
  312. package/server/engine/model-gap.ts +3 -3
  313. package/server/engine/model-vocabulary.ts +2 -2
  314. package/server/engine/oee-accumulator.ts +8 -6
  315. package/server/engine/property-effects.ts +2 -2
  316. package/server/engine/read-time.ts +77 -0
  317. package/server/engine/restart-policy.ts +1 -1
  318. package/server/engine/runtime-key.ts +1 -1
  319. package/server/engine/spec-coverage.ts +3 -3
  320. package/server/engine/stage-path.ts +5 -5
  321. package/server/engine/state-axes.ts +2 -2
  322. package/server/engine/travel-estimator.ts +2 -2
  323. package/server/engine/twin-engine.ts +906 -205
  324. package/server/engine/warm-start.ts +6 -6
  325. package/server/index.ts +1 -1
  326. package/server/migrations/1786000000000-RenameTwinInstanceBoardToModel.ts +1 -1
  327. package/server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.ts +3 -3
  328. package/server/migrations/1786200000000-CarryLiveFeedCursor.ts +2 -2
  329. package/server/migrations/1786300000000-IndexEventTypeByTime.ts +48 -0
  330. package/server/migrations/1786400000000-IndexEventCreatedAt.ts +38 -0
  331. package/server/migrations/1786500000000-CreateTwinSubjectEvents.ts +93 -0
  332. package/server/migrations/index.ts +7 -1
  333. package/server/routes.ts +73 -5
  334. package/server/service/index.ts +7 -1
  335. package/server/service/reference/control-routing.ts +1 -1
  336. package/server/service/reference/discovery-result.ts +3 -3
  337. package/server/service/reference/hook-contract.ts +103 -0
  338. package/server/service/reference/ingest-space.ts +4 -4
  339. package/server/service/reference/knob-defaults.ts +4 -4
  340. package/server/service/reference/reference-adapter.ts +145 -10
  341. package/server/service/reference/reference-hook.ts +145 -0
  342. package/server/service/reference/reference-live.ts +87 -15
  343. package/server/service/reference/reference-master.ts +51 -20
  344. package/server/service/reference/reference-resolver.ts +11 -11
  345. package/server/service/reference/template-registry.ts +1 -1
  346. package/server/service/reference/twin-reference.ts +3 -3
  347. package/server/service/twin-attention/twin-attention-query.ts +1 -1
  348. package/server/service/twin-audit/command-audit.ts +1 -1
  349. package/server/service/twin-audit/twin-audit-event.ts +2 -2
  350. package/server/service/twin-audit/twin-audit-query.ts +1 -1
  351. package/server/service/twin-backfill/backfill-period-facts.ts +168 -0
  352. package/server/service/twin-backfill/backfill-shape.ts +86 -0
  353. package/server/service/twin-backfill/index.ts +3 -0
  354. package/server/service/twin-backfill/twin-backfill-resolver.ts +35 -0
  355. package/server/service/twin-control/twin-control-mutation.ts +2 -2
  356. package/server/service/twin-event/twin-event-keys.ts +2 -2
  357. package/server/service/twin-event/twin-event-type.ts +18 -1
  358. package/server/service/twin-event/twin-event.ts +32 -6
  359. package/server/service/twin-forecast/forecast-metrics.ts +1 -1
  360. package/server/service/twin-forecast/gap-analytics.ts +1 -1
  361. package/server/service/twin-forecast/twin-forecast-query.ts +30 -11
  362. package/server/service/twin-ingest-window/twin-ingest-window-query.ts +1 -1
  363. package/server/service/twin-ingest-window/twin-ingest-window-writer.ts +1 -1
  364. package/server/service/twin-instance/twin-instance.ts +4 -4
  365. package/server/service/twin-journal/event-type-split-shape.ts +112 -0
  366. package/server/service/twin-journal/event-type-split.ts +144 -0
  367. package/server/service/twin-journal/journal-order.ts +20 -0
  368. package/server/service/twin-journal/twin-journal-query.ts +224 -20
  369. package/server/service/twin-lifecycle/domain-catalog.ts +2 -1
  370. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +58 -4
  371. package/server/service/twin-metrics/twin-metrics-query.ts +14 -1
  372. package/server/service/twin-model/axis-journal-evidence.ts +1 -1
  373. package/server/service/twin-model/epcis-coverage.ts +34 -3
  374. package/server/service/twin-model/iec61850-coverage.ts +1 -1
  375. package/server/service/twin-model/isa95-coverage.ts +43 -16
  376. package/server/service/twin-model/item-ref.ts +2 -2
  377. package/server/service/twin-model/name-index.ts +2 -2
  378. package/server/service/twin-model/project-structure.ts +7 -6
  379. package/server/service/twin-model/twin-equipment.ts +16 -4
  380. package/server/service/twin-model/twin-lineage-query.ts +16 -5
  381. package/server/service/twin-model/twin-location.ts +1 -1
  382. package/server/service/twin-model/twin-model-item-query.ts +15 -15
  383. package/server/service/twin-model/twin-model-mutation.ts +4 -4
  384. package/server/service/twin-model/twin-model-query.ts +49 -28
  385. package/server/service/twin-model/twin-model-tree-query.ts +26 -2
  386. package/server/service/twin-model/twin-operation.ts +2 -2
  387. package/server/service/twin-space/move-space.ts +3 -3
  388. package/server/service/twin-space/representation-role.ts +15 -0
  389. package/server/service/twin-space/space-integrity.ts +1 -1
  390. package/server/service/twin-space/twin-space-representation.ts +37 -0
  391. package/server/service/twin-space/twin-space-resolver.ts +40 -8
  392. package/server/service/twin-structure/twin-structure.ts +3 -3
  393. package/server/service/twin-subject/event-subjects.ts +155 -0
  394. package/server/service/twin-subject/index.ts +10 -0
  395. package/server/service/twin-subject/item-resolver.ts +92 -0
  396. package/server/service/twin-subject/subject-query-shape.ts +84 -0
  397. package/server/service/twin-subject/twin-subject-backfill.ts +196 -0
  398. package/server/service/twin-subject/twin-subject-event.ts +101 -0
  399. package/server/service/twin-subject/twin-subject-fast-path.ts +346 -0
  400. package/server/service/twin-subject/twin-subject-query.ts +210 -0
  401. package/server/service/twin-subject/twin-subject-writer.ts +189 -0
  402. package/server/service/twin-target/twin-target-resolver.ts +1 -1
  403. package/shared/entity-delta.ts +4 -4
  404. package/shared/touched-items.ts +2 -2
  405. package/shared/twin-level.ts +1 -1
  406. package/test/adopt-structure-live.test.ts +3 -3
  407. package/test/aggregate-not-findone.test.ts +112 -0
  408. package/test/attention-digest.test.ts +6 -6
  409. package/test/axis-read.test.ts +6 -5
  410. package/test/backfill-shape.test.ts +89 -0
  411. package/test/boot-resume.test.ts +20 -7
  412. package/test/broadcast-cost-baseline.test.ts +2 -1
  413. package/test/broadcast-period.test.ts +2 -2
  414. package/test/broadcast-queue-overflow.test.ts +244 -0
  415. package/test/canonical-ingest-vocabularies.test.ts +259 -8
  416. package/test/canonical-quantity-door.test.ts +2 -2
  417. package/test/capability-mapping.test.ts +1 -1
  418. package/test/checkpoint-refuses-empty.test.ts +2 -2
  419. package/test/connector-capability-declaration.test.ts +88 -0
  420. package/test/contract-layer-guard.test.ts +48 -0
  421. package/test/control-capability.test.ts +1 -1
  422. package/test/cursor-stall-not-read-failure.test.ts +2 -2
  423. package/test/declaration-reaches-model.test.ts +7 -5
  424. package/test/declared-stimulus.test.ts +4 -4
  425. package/test/discovery-result.test.ts +2 -2
  426. package/test/entity-delta.test.ts +4 -4
  427. package/test/equipment-identity-reaches.test.ts +57 -0
  428. package/test/event-subjects.test.ts +147 -0
  429. package/test/event-time-column.test.ts +3 -3
  430. package/test/event-type-split.test.ts +155 -0
  431. package/test/fact-scope-wiring.test.ts +96 -0
  432. package/test/fold-gap-speaks.test.ts +53 -0
  433. package/test/forecast-metrics.test.ts +1 -1
  434. package/test/forecast-tuning.test.ts +1 -1
  435. package/test/generated-not-counted.test.ts +99 -0
  436. package/test/ingest-bench.test.ts +2 -1
  437. package/test/ingest-health-engine.test.ts +2 -2
  438. package/test/ingest-health-wiring.test.ts +7 -5
  439. package/test/ingest-health.test.ts +6 -6
  440. package/test/ingest-history.test.ts +2 -2
  441. package/test/ingest-idempotent.test.ts +132 -0
  442. package/test/ingest-space.test.ts +3 -3
  443. package/test/ingest-wiring-guard.test.ts +197 -0
  444. package/test/integration-probes.test.ts +3 -3
  445. package/test/integration-runner.test.ts +1 -1
  446. package/test/item-ref.test.ts +1 -1
  447. package/test/journal-order-axis.test.ts +80 -0
  448. package/test/journal-read-discipline.test.ts +16 -6
  449. package/test/journal-retention.test.ts +171 -29
  450. package/test/journal-sort-axis.test.ts +1 -1
  451. package/test/journal-write-door.test.ts +3 -3
  452. package/test/journal-write-trend.test.ts +1 -1
  453. package/test/kernel-kind-guard.test.ts +3 -3
  454. package/test/knob-defaults.test.ts +1 -1
  455. package/test/kpi-baseline-db.test.ts +2 -2
  456. package/test/kpi-fold.test.ts +266 -14
  457. package/test/kpi-query-bench.test.ts +3 -3
  458. package/test/live-cursor-wiring.test.ts +6 -6
  459. package/test/live-kernel-facts.test.ts +11 -11
  460. package/test/live-mirror-parity.test.ts +9 -6
  461. package/test/load-meter.test.ts +1 -1
  462. package/test/local-declarations.test.ts +13 -12
  463. package/test/log-stamp.test.ts +1 -1
  464. package/test/master-to-twin.test.ts +2 -2
  465. package/test/measured-yield.test.ts +2 -2
  466. package/test/merge-streams.test.ts +70 -0
  467. package/test/mirror-resumes-from-checkpoint.test.ts +13 -13
  468. package/test/model-basis.test.ts +2 -2
  469. package/test/model-gap.test.ts +4 -4
  470. package/test/model-vocabulary.test.ts +2 -2
  471. package/test/move-space.test.ts +1 -1
  472. package/test/oee-accumulator.test.ts +20 -5
  473. package/test/operational-vocabulary.test.ts +2 -2
  474. package/test/project-structure-db.test.ts +2 -2
  475. package/test/projection-reaches-screen.test.ts +7 -2
  476. package/test/projection-reads-declared.test.ts +109 -0
  477. package/test/property-effects.test.ts +10 -6
  478. package/test/read-failure-ledger.test.ts +106 -0
  479. package/test/read-failure-visible.test.ts +7 -7
  480. package/test/read-time.test.ts +118 -0
  481. package/test/reference-grounding.test.ts +1 -1
  482. package/test/reference-hook.test.ts +86 -0
  483. package/test/registry-key-guard.test.ts +1 -1
  484. package/test/rename-twin.test.ts +81 -0
  485. package/test/restart-policy.test.ts +3 -3
  486. package/test/resync-origin-site.test.ts +3 -3
  487. package/test/revision-axis.test.ts +14 -8
  488. package/test/runtime-key.test.ts +3 -3
  489. package/test/scale-twin-bench.test.ts +2 -1
  490. package/test/snapshot-freshness.test.ts +4 -4
  491. package/test/source-outcome-audit.test.ts +1 -1
  492. package/test/space-integrity.test.ts +1 -1
  493. package/test/space-representation.test.ts +66 -0
  494. package/test/space-scope-includes-stopped.test.ts +55 -0
  495. package/test/spec-coverage.test.ts +1 -1
  496. package/test/standard-coverage.test.ts +19 -21
  497. package/test/status-tally.test.ts +1 -1
  498. package/test/structure-revision-db.test.ts +7 -7
  499. package/test/subject-fast-path.test.ts +148 -0
  500. package/test/subject-read-model.test.ts +175 -0
  501. package/test/touched-items.test.ts +2 -2
  502. package/test/twin-audit.test.ts +1 -1
  503. package/test/twin-event-keys.test.ts +1 -1
  504. package/test/twin-model-item-db.test.ts +5 -5
  505. package/test/twin-model-tree-db.test.ts +35 -2
  506. package/test/twin-origin-resync.test.ts +1 -1
  507. package/test/vocabulary-guard.test.ts +1 -1
  508. package/test/warm-start-seam.test.ts +1 -1
  509. package/test/warm-start.test.ts +6 -6
  510. package/test/withheld-door.test.ts +2 -2
  511. package/test/yield-loop.test.ts +4 -4
  512. package/tsconfig.shared.tsbuildinfo +1 -1
  513. package/tsconfig.tsbuildinfo +1 -1
@@ -5,7 +5,7 @@
5
5
  * 이름을 그대로 id 로 쓰므로(`sap-ewm-1710`) 두 테넌트가 같은 벤더 시스템에 연결하면 같은 id 가 된다 —
6
6
  * 그때 남의 커널에 이벤트가 적용되거나, 웜스타트 씨앗이 남의 스냅샷이 됐다.
7
7
  *
8
- * 이 테스트가 지키는 것은 **키가 모호해지지 않는 것** 하나다. 모호한 키는 조용히 틀린다.
8
+ * 이 테스트가 지키는 것은 **키가 모호해지지 않는 것** 하나다. 모호한 키는 오류 없이 틀린 값을 낸다.
9
9
  */
10
10
  import { test } from 'node:test'
11
11
  import assert from 'node:assert/strict'
@@ -39,8 +39,8 @@ test('인스턴스 id 안의 구분자를 보존한다 — 첫 구분자에서
39
39
  assert.equal(instanceId, weird)
40
40
  })
41
41
 
42
- test('도메인 id 에 구분자가 있으면 오류를 낸다 — 조용히 모호한 키를 만들지 않는다', () => {
43
- /* 조용히 만들면 그게 곧 이 결함의 재발이다(어느 테넌트인지 알 수 없는 키). */
42
+ test('도메인 id 에 구분자가 있으면 오류를 낸다 — 알리지 않고 모호한 키를 만들지 않는다', () => {
43
+ /* 알리지 않고 만들면 그게 곧 이 결함의 재발이다(어느 테넌트인지 알 수 없는 키). */
44
44
  assert.throws(() => runtimeKey(`a${RUNTIME_KEY_SEP}b`, 'x'), /ambiguous/)
45
45
  })
46
46
 
@@ -7,7 +7,8 @@
7
7
  import { test } from 'node:test'
8
8
  import assert from 'node:assert/strict'
9
9
 
10
- import { StateProjector, objectEvent, BIZSTEP, DISP, OP_EVENT } from '@operato/twin-kernel'
10
+ import { StateProjector } from '@operato/twin-kernel'
11
+ import { objectEvent, BIZSTEP, DISP, OP_EVENT } from '@operato/ops-contract'
11
12
  import { masterToTwin } from '../server/service/reference/reference-master.ts'
12
13
  import { buildEntityDeltas } from '../shared/entity-delta.ts'
13
14
 
@@ -1,8 +1,8 @@
1
1
  /*
2
2
  * 영속 스냅샷이 **사실을 이기지 않는다** — 쓸 수 있는 조건을 못 박는다.
3
3
  *
4
- * 멈춘 트윈을 조회할 때마다 저널을 전량 접고 있었다(23,731건짜리 트윈에서 4.6초). 스냅샷은 같은
5
- * 폴드의 결과이므로 그 뒤로 아무 일도 없었다면 다시 접어 봐야 같은 값이다 — 그래서 쓴다.
4
+ * 멈춘 트윈을 조회할 때마다 저널을 전량 계산하고 있었다(23,731건짜리 트윈에서 4.6초). 스냅샷은 같은
5
+ * 폴드의 결과이므로 그 뒤로 아무 일도 없었다면 다시 계산해 봐야 같은 값이다 — 그래서 쓴다.
6
6
  *
7
7
  * 위험은 하나다: **낡은 스냅샷을 "지금" 이라고 내는 것.** 그래서 쓰는 조건을 좁게 잡았고, 그 조건을
8
8
  * 여기서 고정한다. 엔진 본체는 DB 를 물고 있어 부를 수 없으므로 판정식만 떼어 시험한다 —
@@ -22,7 +22,7 @@ const usable = (
22
22
  const TIP = { revision: 23731, structureRev: 2 }
23
23
  const FRESH = { revision: 23731, structureRev: 2, state: { locations: [] } }
24
24
 
25
- test('끝에 있고 구조도 같으면 쓴다 — 다시 접어도 같은 값이다', () => {
25
+ test('끝에 있고 구조도 같으면 쓴다 — 다시 계산해도 같은 값이다', () => {
26
26
  assert.equal(usable(FRESH, TIP, true), true)
27
27
  })
28
28
 
@@ -46,7 +46,7 @@ test('구조가 양쪽 다 없으면 그것은 같은 것이다 — 없는 것
46
46
  assert.equal(usable({ revision: 5, state: {} }, { revision: 5, structureRev: null }, true), true)
47
47
  })
48
48
 
49
- test('시간여행에는 쓰지 않는다 — 그 시점까지 접어야 한다', () => {
49
+ test('시간여행에는 쓰지 않는다 — 그 시점까지 계산해야 한다', () => {
50
50
  assert.equal(usable(FRESH, TIP, false), false)
51
51
  })
52
52
 
@@ -2,7 +2,7 @@
2
2
  * **허락과 일어남 사이를 같은 행에 닫는다** — ADR-0029 §1 결정 C (2026-08-19).
3
3
  *
4
4
  * ── 무엇이 열려 있었나 ──────────────────────────────────────────────────────
5
- * 명령이 원본으로 가면 감사는 「허락했다」까지만 증명한다: 어댑터가 조용히 실패하면 원장에는 승인만 남는다.
5
+ * 명령이 원본으로 가면 감사는 「허락했다」까지만 증명한다: 어댑터가 오류 없이 실패하면 원장에는 승인만 남는다.
6
6
  * 그리고 구동 문(`controlTwinSource`)은 감사를 **아예 남기지 않았다** — 자극을 바꾸고 배속을 올리는 것은
7
7
  * 그 트윈의 사실을 바꾸는 행위인데, 나중에 「무엇이 이 공장을 흔들었나」에 답할 수 없었다.
8
8
  *
@@ -217,7 +217,7 @@ test('정지한 트윈은 세지 않는다 — 확인하지 않은 것을 정합
217
217
  assert.equal(f.find(x => x.code === 'twin-unhandled-vocabulary'), undefined)
218
218
  })
219
219
 
220
- test('빈 배열은 결함이 아니다 — 받은 것이 없으면 조용하다', () => {
220
+ test('빈 배열은 결함이 아니다 — 받은 것이 없으면 들어온 것이 없다', () => {
221
221
  const f = checkSpaceIntegrity({ ...SOUND, instances: [{ ...SOUND.instances[0], unhandled: [] }] })
222
222
  assert.equal(f.find(x => x.code === 'twin-unhandled-vocabulary'), undefined)
223
223
  })
@@ -0,0 +1,66 @@
1
+ /*
2
+ * ★ 표현을 묶고 가른다 — 목록에 나올 것과 다른 표현에서 열리는 것.
3
+ *
4
+ * ── 왜 (2026-08-27 · 사용자 지시, UX 레인 요청) ─────────────────────────────
5
+ * 「각 표현이 많아지게 되면 관리가 어려우니, 그루핑과 정렬 기능도 필요하다.」
6
+ * 「표현 인덱스에 포함될 표현과 팝업 같은 파생 표현이 구분될 수 있어야 한다.」
7
+ *
8
+ * 칸 둘을 더한다.
9
+ *
10
+ * group 자유 문자열. 묶는 축이 현장마다 다르다(건물별·용도별·층별)
11
+ * role index(기본) · derived. 닫힌 목록이다
12
+ */
13
+ import { test } from 'node:test'
14
+ import assert from 'node:assert/strict'
15
+ import { readFileSync } from 'node:fs'
16
+ import { join } from 'node:path'
17
+ import { REPRESENTATION_ROLES } from '../server/service/twin-space/representation-role.js'
18
+
19
+ const dir = (...p: string[]) => join(import.meta.dirname, '..', 'server', 'service', 'twin-space', ...p)
20
+ const entity = readFileSync(dir('twin-space-representation.ts'), 'utf8')
21
+ const resolver = readFileSync(dir('twin-space-resolver.ts'), 'utf8')
22
+
23
+ test('★ 묶는 이름은 자유 문자열이다 — 묶는 축이 현장마다 다르다', () => {
24
+ assert.match(entity, /group\?: string/, 'group 칸이 있어야 한다')
25
+ /* 고정 목록으로 두면 현장마다 맞지 않는다 — 값을 검사하지 않는다. */
26
+ assert.doesNotMatch(resolver, /GROUPS as readonly|REPRESENTATION_GROUPS/, '묶는 이름을 목록으로 닫지 않는다')
27
+ })
28
+
29
+ test('★ 그룹 자체의 순서는 칸으로 두지 않는다 — 그 그룹의 최소 seq 가 그 순서다', () => {
30
+ /* 칸을 두면 그룹 이름을 바꿀 때 두 곳을 맞춰야 하고, 어긋나면 목록 순서가 오류 없이 달라진다. */
31
+ assert.doesNotMatch(entity, /groupSeq|groupOrder/, '그룹 순서 칸을 만들지 않는다')
32
+ })
33
+
34
+ test('★ role 은 닫힌 목록이고, 모르는 값은 저장하지 않는다', () => {
35
+ assert.deepEqual([...REPRESENTATION_ROLES], ['index', 'derived'])
36
+ /*
37
+ * 모르는 값을 그냥 저장하면 화면이 그것을 어느 쪽으로도 그리지 못하고, 그 표현이 목록에서 알리지 않고
38
+ * 사라진다. 저장하는 자리에서 거절한다.
39
+ */
40
+ assert.match(resolver, /REPRESENTATION_ROLES as readonly string\[\]\)\.includes\(role\)/, '닫힌 목록으로 검사한다')
41
+ assert.match(resolver, /unknown representation role/, '거절 사유를 말한다')
42
+ })
43
+
44
+ test('★ 비어 있으면 index 다 — 흔한 쪽을 기본으로 둔다', () => {
45
+ assert.match(entity, /role\?: string/, 'role 은 선택 칸이다')
46
+ assert.match(entity, /default when absent/, '비어 있을 때의 뜻이 설명에 있어야 한다')
47
+ /* 공백만 있는 값은 「없음」과 같다 — 그것을 값으로 저장하면 검사를 지나며 뜻이 없는 값이 남는다. */
48
+ assert.match(resolver, /String\(rep\.role\)\.trim\(\) === ''/, '공백만 있는 값은 비운다')
49
+ assert.match(resolver, /String\(rep\.group\)\.trim\(\) === ''/, '묶는 이름도 같다')
50
+ })
51
+
52
+ test('★ isPrimary 에 얹지 않는다 — 다른 질문에 답하는 칸이다', () => {
53
+ /*
54
+ * `isPrimary` 는 「현장 현황에 나올 하나」를 정한다. `role` 은 「목록에 나오나」를 정한다. 한 칸이
55
+ * 두 질문에 답하면 한쪽을 고칠 때 다른 쪽이 오류 없이 바뀐다.
56
+ */
57
+ assert.match(entity, /isPrimary\?: boolean/, 'isPrimary 는 그대로 있어야 한다')
58
+ assert.match(entity, /Kept separate from isPrimary/, '왜 따로 두는지 설명에 있어야 한다')
59
+ })
60
+
61
+ test('★ 조회가 두 값을 실어 보낸다 — 화면이 묶을 수 있어야 한다', () => {
62
+ assert.match(resolver, /group: rep\.group, role: rep\.role/, '표현 목록에 두 값을 싣는다')
63
+ assert.match(resolver, /order: \{ group: 'ASC', seq: 'ASC' \}/, '묶은 이름 다음에 순서로 정렬한다')
64
+ /* 빈 값이 앞에 오는지 뒤에 오는지는 드라이버마다 다르다 — 그 순서에 기대지 않는다. */
65
+ assert.match(resolver, /드라이버마다\s*\n?\s*\*?\s*다르므로/, '그 순서에 기대지 않는다는 것을 적어 둔다')
66
+ })
@@ -0,0 +1,55 @@
1
+ /*
2
+ * ★ 현장으로 물으면 멈춘 트윈의 기록도 나온다 — 저널은 기록이고, 지금 도는지는 다른 축이다.
3
+ *
4
+ * ── 무엇이 났나 (2026-08-28 실측) ───────────────────────────────────────────
5
+ * 대상 해소에 `running` 조건이 있었다. 그래서 현장으로 물으면 멈춘 트윈이 대상에서 빠지고, 화면은
6
+ * 「기록이 없습니다」라고 말했다 — 저널에는 있는데.
7
+ *
8
+ * twinEventList(instanceId: "ems-plant-1") 1,792건
9
+ * twinEventList(spaceId: "ems-plant-1-site") 0건 ← 같은 트윈 · 같은 도메인
10
+ *
11
+ * 규모로 보면 예외가 아니라 기본값이었다: 등록된 트윈 30개 중 도는 것 2개, 멈춘 것 28개.
12
+ *
13
+ * ── 왜 지웠나 ──────────────────────────────────────────────────────────────
14
+ * 옛 주석이 대는 이유(「벤치 사본은 사용자가 보는 현장이 아니다」)는 벤치 제외만 설명한다. `running` 에는
15
+ * 근거가 적혀 있지 않았다. 그리고 바로 다음 줄이 반대 원칙을 적어 두었다 — 「정지·비등록이어도 저널은
16
+ * 남아 있을 수 있다」. 그 원칙이 현장 경로에만 적용되지 않고 있었다.
17
+ */
18
+ import { test } from 'node:test'
19
+ import assert from 'node:assert/strict'
20
+ import { readFileSync } from 'node:fs'
21
+ import { join } from 'node:path'
22
+
23
+ const KPI = join(import.meta.dirname, '..', 'server', 'engine', 'kpi-query.ts')
24
+ const src = readFileSync(KPI, 'utf8')
25
+
26
+ /** 대상을 고르는 그 한 줄만 떼어 본다. */
27
+ const targetLine = (() => {
28
+ const from = src.indexOf('async function resolveTargets')
29
+ const body = src.slice(from, from + 1200)
30
+ const m = /: list\.filter\(\(x: any\) => [^\n]+\)/.exec(body)
31
+ return m?.[0] ?? ''
32
+ })()
33
+
34
+ test('★ 현장 경로가 「지금 도는가」로 걸러내지 않는다', () => {
35
+ assert.ok(targetLine, '대상을 고르는 줄을 찾아야 한다')
36
+ assert.doesNotMatch(targetLine, /running/, '멈춘 트윈을 현장에서 빼면 화면이 거짓 「없다」를 낸다')
37
+ })
38
+
39
+ test('벤치 사본은 계속 뺀다 — 그 제외에는 근거가 있다', () => {
40
+ /* 부하 시험용 사본은 사용자가 보는 현장이 아니다. 이 제외는 지우지 않는다. */
41
+ assert.match(targetLine, /purpose !== 'bench'/)
42
+ })
43
+
44
+ test('현장과 트윈 두 경로가 같은 원칙을 쓴다 — 저널은 기록이다', () => {
45
+ /* 트윈을 지정한 경로는 처음부터 「정지·비등록이어도 저널은 남아 있을 수 있다」였다. */
46
+ assert.match(src, /정지·비등록\)? ?지정된 트윈은 그대로 조회한다|정지·비등록/)
47
+ /* 왜 지웠는지가 코드에 남아 있어야 한다 — 다음 사람이 성능을 이유로 되넣지 않게. */
48
+ assert.match(src, /「지금 도는가」로 걸러내지 않는다/)
49
+ assert.match(src, /1,792건/, '무엇을 재서 알았는지 적혀 있어야 한다')
50
+ })
51
+
52
+ test('★ 「지금 도는 것만」이 필요하면 이름 있는 축으로 받는다 — 여기 숨기지 않는다', () => {
53
+ /* 한 칸에 두 물음을 담으면 어느 화면이 무엇을 보는지 아무도 모른다. */
54
+ assert.match(src, /\*\*이름 있는 축\*\*으로 받는다/)
55
+ })
@@ -32,7 +32,7 @@ test('전부 선언되면 절대 예측 시도 가능 — 단 정확도는 별
32
32
  assert.equal(r!.varyingDurations, 2)
33
33
  })
34
34
 
35
- test('섞이면 partial — 한쪽만 접지된 사실을 감추지 않는다', () => {
35
+ test('섞이면 partial — 한쪽만 계산하지된 사실을 감추지 않는다', () => {
36
36
  const r = classifySpecCoverage({
37
37
  operations: [
38
38
  { kind: 'cut', duration: 'declared' },
@@ -76,7 +76,7 @@ test('개폐·발전·저장이 세 층 모두 올라갔다 — 값이 먼저,
76
76
  assert.equal(iec.totals.behavior, 5, '다섯 논리 노드가 모두 실제로 돈다')
77
77
  })
78
78
 
79
- test('커널 축이 사라지면 구조가 없는 것으로 떨어진다 — 오타·개명이 조용히 통과하지 않게', () => {
79
+ test('커널 축이 사라지면 구조가 없는 것으로 떨어진다 — 오타·개명이 알리지 않고 통과하지 않게', () => {
80
80
  const iec = iec61850Coverage(['equipment']) // demandWindows 축이 없는 세상
81
81
  const mmxu = iec.concepts.find(c => c.std === 'MMXU')!
82
82
  assert.equal(mmxu.structure, 'none')
@@ -106,21 +106,17 @@ test('계측은 EPCIS 센서 필드로 오지 않는다 — 그 사실을 표가
106
106
  assert.ok(sensor.note, '에너지가 다른 층으로 들어온다는 사실을 근거로 적는다')
107
107
  })
108
108
 
109
- test('★ 「어디서 보이나」가 화면 등급과 어긋나지 않는다 — 표가 없는 화면으로 사람을 보내지 않게', () => {
109
+ test('★ 자기 축이 없는 개념은 **어디에 속하는지** 말한다 — 「어디에도 없다」로 읽히지 않게', () => {
110
110
  /*
111
- * ── 무엇이 틀려 있었나 (2026-08-24 실측) ──────────────────────────────────
112
- * `shownOn` 은 「그 사실이 어느 축에 **담겨 있나**」가 아니라 「**어디서 보이나**」다. 화면이 그것을
113
- * 「해당 축 항목에 표시됩니다」로 읽어 내므로, `surface: 'none'` 인 줄에 갈 곳이 적혀 있으면 표가
114
- * **거짓을 말한다** — 사람을 그 개념이 보이지 않는 화면으로 보낸다.
111
+ * ── 사용자 지적 (2026-08-24) ──────────────────────────────────────────────
112
+ * 화면이 그런 줄을 「개념 축이 없습니다」로만 냈다. 자재 로트·자원 속성·로케이션 관측 기록이 그렇게
113
+ * 보였는데, 셋 다 **어디엔가 속해 있다** — 로트는 물품 목록이고, 속성은 설비·사람·자산 안에 붙고,
114
+ * 관측은 로케이션에 붙는다. 「축이 없다」는 사실이지만 사용자에게는 **없는 것**으로 읽힌다.
115
115
  *
116
- * 같은 날 넷이 그랬다. 셋은 새로 넣으면서(`TestResult`·`OperationsEvent`), 하나는 오래(`errorDeclaration`
117
- * — 그 줄의 `note` 는 「화면에 보이는 곳은 아직 없습니다」라고 말하는데 배지는 반대를 말했다).
116
+ * 그래서 `shownOn` 의 뜻을 고쳤다 — 「어디서 보이나」가 아니라 **「어디에 속하나」**다. 화면에
117
+ * 그려지는지는 `surface` 가 따로 답하고, 둘은 다른 물음이다.
118
118
  *
119
- * 반대 방향도 본다: 화면에 보이는데 축도 갈 곳도 주석도 없으면 그 줄은 **「진짜 빈칸」으로 읽힌다**
120
- * (`shownOn` 주석이 정한 규칙이다). `MaterialSublot` 이 그랬다 — 물품 이름으로 보이는데 갈 곳이 비어
121
- * 있었다.
122
- *
123
- * 사람이 눈으로 보고 잡았다. 그래서 기계가 잡게 한다.
119
+ * (그리고 화면의 「해당 축 항목에 표시됩니다」 문장은 걷었다 — 칩이 이미 갈 곳을 보인다.)
124
120
  */
125
121
  const AXES = [
126
122
  'items', 'locations', 'equipment', 'persons', 'assets', 'tasks', 'orders', 'operations', 'recipes',
@@ -133,18 +129,20 @@ test('★ 「어디서 보이나」가 화면 등급과 어긋나지 않는다
133
129
  ['IEC 61850', iec61850Coverage(AXES)]
134
130
  ]
135
131
 
136
- const lies: string[] = []
132
+ /*
133
+ * 규칙: **세 층 중 하나라도 있는 개념은 어디에 속하는지 말해야 한다.** 셋 다 없으면 진짜 빈칸이고,
134
+ * 그때만 「축이 없다」가 정직한 답이다.
135
+ */
137
136
  const mute: string[] = []
138
137
  for (const [name, table] of tables) {
139
138
  for (const c of table.concepts) {
140
- if (c.surface === 'none' && c.shownOn?.length) lies.push(`${name}/${c.std} → ${c.shownOn.join(',')}`)
141
- if (c.surface !== 'none' && !c.axis && !c.shownOn?.length && !c.note) mute.push(`${name}/${c.std}`)
139
+ const lives = c.structure !== 'none' || c.behavior !== 'none' || c.surface !== 'none'
140
+ if (lives && !c.axis && !c.shownOn?.length) mute.push(`${name}/${c.std}`)
142
141
  }
143
142
  }
144
- assert.deepEqual(lies, [], `화면이 없는데 갈 곳을 적은 줄 — 없는 화면으로 사람을 보낸다:\n${lies.join('\n')}`)
145
- assert.deepEqual(mute, [], `화면에 보이는데 갈 곳도 주석도 없는 줄 — 「진짜 빈칸」으로 읽힌다:\n${mute.join('\n')}`)
143
+ assert.deepEqual(mute, [], `있는데 어디에 속하는지 말하지 않는 줄 — 「어디에도 없다」로 읽힌다:\n${mute.join('\n')}`)
146
144
 
147
- /* 빈 비교로 통과하지 않게 — 표가 실제로 여러 줄을 들고 있다. */
148
- const total = tables.reduce((n, [, t]) => n + t.concepts.length, 0)
149
- assert.ok(total > 30, `표 셋에 줄이 실제로 있어야 한다 (실제 ${total})`)
145
+ /* 빈 비교로 통과하지 않게 — 실제로 그런 줄이 여럿 있다(자재 로트·서브로트·속성·관측·시험 결과). */
146
+ const attached = tables.flatMap(([, t]) => t.concepts).filter(c => !c.axis && c.shownOn?.length)
147
+ assert.ok(attached.length >= 4, `속해 있다고 말하는 줄이 실제로 있어야 한다 (실제 ${attached.length})`)
150
148
  })
@@ -33,7 +33,7 @@ test('낱말은 커널의 것 그대로 — 묶거나 고쳐 부르지 않는다
33
33
  assert.deepEqual(
34
34
  r.statusCounts?.map(s => s.status).sort(),
35
35
  ['assigned', 'created'],
36
- '「대기」처럼 앱의 낱말로 바꾸면 커널이 늘린 낱말을 화면이 조용히 잘못 부른다'
36
+ '「대기」처럼 앱의 낱말로 바꾸면 커널이 늘린 낱말을 화면이 알리지 않고 잘못 부른다'
37
37
  )
38
38
  })
39
39
 
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * ★ 구조 리비전 — **실 DB로 경계를 넘어 본다.**
3
3
  *
4
- * 커널의 마디 접기(`replaySegments`)는 그쪽 테스트가 지킨다. 여기서 지키는 것은 **호스트 배선**이다:
4
+ * 커널의 마디 계산(`replaySegments`)는 그쪽 테스트가 지킨다. 여기서 지키는 것은 **호스트 배선**이다:
5
5
  * 리비전을 언제 새로 만드는가 · 이벤트에 번호를 찍는가 · 재생이 그 번호로 마디를 나누는가 ·
6
6
  * 컬럼이 생기기 전 행(번호 없음)을 어디에 붙이는가.
7
7
  *
@@ -152,9 +152,9 @@ test('★ 재생이 경계를 넘는다 — 앞선 사실은 남고, 새 구조
152
152
  assert.equal(state.locations.filter((n: any) => n.type === 'storage').length, 6, '늘린 자리가 안 보이면 늘린 의미가 없다')
153
153
  })
154
154
 
155
- test('구조 번호가 없는 옛 행은 **가장 오래된 구조**로 접는다 — 새 공장에 대고 접지 않는다', async () => {
155
+ test('구조 번호가 없는 옛 행은 **가장 오래된 구조**로 처리한다 — 새 공장에 대고 계산하지 않는다', async () => {
156
156
  /*
157
- * 컬럼이 생기기 전에 쓰인 행은 `structureRev` 가 비어 있다. 그것을 현재 보드로 접으면 그때 없던
157
+ * 컬럼이 생기기 전에 쓰인 행은 `structureRev` 가 비어 있다. 그것을 현재 보드로 계산하면 그때 없던
158
158
  * 설비에서 일이 있었던 것처럼 보인다 — 이 규칙이 그 행들을 제자리에 붙인다.
159
159
  */
160
160
  await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(2, 1), 'reset')
@@ -165,11 +165,11 @@ test('구조 번호가 없는 옛 행은 **가장 오래된 구조**로 접는
165
165
 
166
166
  await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(6, 3), 'reset')
167
167
  const state = await TwinEngine.recover(domainId, INSTANCE)
168
- assert.ok(state.items.length > 0, '번호 없는 옛 행도 읽혀야 한다 — 조용히 버리지 않는다')
168
+ assert.ok(state.items.length > 0, '번호 없는 옛 행도 읽혀야 한다 — 알리지 않고 버리지 않는다')
169
169
  assert.equal(state.equipment.length, 3, '마지막 구조가 자원의 진실이다')
170
170
  })
171
171
 
172
- test('리비전이 하나뿐이면 예전과 똑같이 접는다 — 구조 전환이 결과를 바꾸지 않는다', async () => {
172
+ test('리비전이 하나뿐이면 예전과 똑같이 계산한다 — 구조 전환이 결과를 바꾸지 않는다', async () => {
173
173
  await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(3, 2), 'reset')
174
174
  const events = realEvents(boardWith(3, 2), 300)
175
175
  await TwinEngine.persistBatch(domainId, INSTANCE, events, 0)
@@ -243,7 +243,7 @@ test('번호 없는 옛 행도 가장 오래된 리비전의 몫으로 센다
243
243
  await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(6, 3), 'reset')
244
244
 
245
245
  const history = await TwinEngine.structureHistory(domainId, INSTANCE)
246
- assert.equal(history[0].events, legacy.length, '재생이 rev 1 로 접는 행들을 세는 쪽만 빠뜨리면 두 화면이 다른 말을 한다')
246
+ assert.equal(history[0].events, legacy.length, '재생이 rev 1 로 계산하는 행들을 세는 쪽만 빠뜨리면 두 화면이 다른 말을 한다')
247
247
  assert.equal(history[1].events, 0)
248
248
  })
249
249
 
@@ -270,7 +270,7 @@ test('지문 표기가 낡았을 뿐이면 리비전을 만들지 않는다 —
270
270
  *
271
271
  * 예전에는 sim-world 선언이 저널을 지우고 fresh 로 띄웠다. 실제로 없던 것은 **번호 이어 세기
272
272
  * 하나**였다: 상태 복원은 이미 저널 재생으로 하고 있었고, 막힌 것은 커널이 0부터 다시 세어 기존
273
- * 행과 겹치는 문제였다. 그 겹침은 오류를 내지 않고 조용히 재생 순서를 뒤섞는다.
273
+ * 행과 겹치는 문제였다. 그 겹침은 오류를 내지 않고 알리지 않고 재생 순서를 뒤섞는다.
274
274
  */
275
275
  test('sim-world 로 다시 띄워도 저널이 살아남는다 — 선언이 지키는 약속', async () => {
276
276
  await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(2, 1), 'reset')
@@ -0,0 +1,148 @@
1
+ /*
2
+ * ★ 화면이 이미 부르는 문에서 파생 테이블을 쓴다 — 그리고 못 답할 때는 물러난다.
3
+ *
4
+ * ── 무엇을 놓쳤던가 (2026-08-28) ────────────────────────────────────────────
5
+ * 대상별 사건 목록을 만들고 8,382,017줄을 채웠는데, 360 화면은 예전부터 `twinEventList` 를 부르고
6
+ * 있었다. 그래서 조회는 그대로 64초였다. **자리는 있고 길이 없었다** — 가장 자주 나는 결함이다.
7
+ *
8
+ * 이 시험이 지키는 것은 그 길이다.
9
+ * ① 어떤 물음이 파생 테이블로 가는가 — 좁게, 그리고 그 판단이 순수한가
10
+ * ② 못 답하는 물음에 억지로 답하지 않는가
11
+ * ③ 답할 수 없는 구간을 「사건이 없다」로 그리게 만들지 않는가
12
+ * ④ 화면이 부르는 자리에 실제로 붙어 있는가
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 { readModelShape, AXIS_KIND } from '../server/service/twin-subject/subject-query-shape.js'
20
+
21
+ const dir = (...p: string[]) => join(import.meta.dirname, '..', 'server', ...p)
22
+ const fastPath = readFileSync(dir('service', 'twin-subject', 'twin-subject-fast-path.ts'), 'utf8')
23
+ const journal = readFileSync(dir('service', 'twin-journal', 'twin-journal-query.ts'), 'utf8')
24
+
25
+ /** 360 화면이 실제로 보내는 모양(§`loadTwinEventPage`) — 축 하나를 부분일치로, 시각 내림차순. */
26
+ const panelParams = (axis: string, value: string, extra: any[] = []) => ({
27
+ filters: [{ name: axis, operator: 'search', value }, ...extra],
28
+ sortings: [{ name: 'eventTime', desc: true }]
29
+ })
30
+
31
+ const DESC = [{ name: 'eventTime', desc: true }]
32
+
33
+ test('★ 360 화면이 보내는 네 축이 모두 파생 테이블로 간다', () => {
34
+ /* 이것이 이 작업의 목적이다. 하나라도 빠지면 그 축의 이력은 여전히 64초다. */
35
+ const expected: Record<string, string> = { epc: 'item', orderId: 'order', locationId: 'location', moverId: 'equipment' }
36
+ for (const [axis, kind] of Object.entries(expected)) {
37
+ const shape = readModelShape(panelParams(axis, 'V-1'), DESC)
38
+ assert.ok(shape, `${axis} 축이 파생 테이블로 가야 한다`)
39
+ assert.equal(shape!.kind, kind)
40
+ assert.equal(shape!.value, 'V-1')
41
+ }
42
+ assert.deepEqual(Object.keys(AXIS_KIND).sort(), ['epc', 'locationId', 'moverId', 'orderId'])
43
+ })
44
+
45
+ test('시각 상한(시간여행)이 함께 와도 간다 — 화면이 과거를 볼 때 보내는 모양', () => {
46
+ const shape = readModelShape(panelParams('epc', 'ITEM-1', [{ name: 'eventTime', operator: 'lte', value: '2026-08-01T00:00:00.000Z' }]), DESC)
47
+ assert.ok(shape)
48
+ assert.equal(shape!.untilTime?.toISOString(), '2026-08-01T00:00:00.000Z')
49
+ })
50
+
51
+ test('구간으로 물으면 두 끝을 모두 읽는다', () => {
52
+ const shape = readModelShape(
53
+ panelParams('locationId', 'BUFFER-CK1', [
54
+ { name: 'eventTime', operator: 'between', value: ['2026-08-01T00:00:00.000Z', '2026-08-02T00:00:00.000Z'] }
55
+ ]),
56
+ DESC
57
+ )
58
+ assert.ok(shape)
59
+ assert.equal(shape!.fromTime?.toISOString(), '2026-08-01T00:00:00.000Z')
60
+ assert.equal(shape!.untilTime?.toISOString(), '2026-08-02T00:00:00.000Z')
61
+ })
62
+
63
+ test('★ 맞지 않는 물음은 답하지 않고 저널로 보낸다 — 억지로 답하면 목록이 알리지 않고 달라진다', () => {
64
+ const cases: [string, any, any[]][] = [
65
+ ['자유 검색어', { ...panelParams('epc', 'ITEM-1'), search: '냉동' }, DESC],
66
+ ['종류 필터', panelParams('epc', 'ITEM-1', [{ name: 'eventType', operator: 'in', value: ['task.status'] }]), DESC],
67
+ ['상관 식별자', panelParams('epc', 'ITEM-1', [{ name: 'correlationId', operator: 'eq', value: 'C1' }]), DESC],
68
+ ['축 둘', { filters: [{ name: 'epc', operator: 'eq', value: 'A' }, { name: 'orderId', operator: 'eq', value: 'B' }], sortings: DESC }, DESC],
69
+ ['축 없음', { filters: [], sortings: DESC }, DESC],
70
+ ['여러 값(in)', panelParams('epc', 'A').filters && { filters: [{ name: 'epc', operator: 'in', value: ['A', 'B'] }], sortings: DESC }, DESC],
71
+ ['리비전 정렬', panelParams('epc', 'ITEM-1'), [{ name: 'revision', desc: true }]],
72
+ ['오름차순 정렬', panelParams('epc', 'ITEM-1'), [{ name: 'eventTime', desc: false }]],
73
+ ['정렬 둘', panelParams('epc', 'ITEM-1'), [{ name: 'eventTime', desc: true }, { name: 'revision', desc: true }]],
74
+ ['시각 하한만', panelParams('epc', 'ITEM-1', [{ name: 'eventTime', operator: 'gte', value: '2026-08-01T00:00:00.000Z' }]), DESC],
75
+ ['못 읽는 시각', panelParams('epc', 'ITEM-1', [{ name: 'eventTime', operator: 'lte', value: '어제' }]), DESC],
76
+ ['빈 축 값', panelParams('epc', ' '), DESC]
77
+ ]
78
+ for (const [why, params, sortings] of cases) {
79
+ assert.equal(readModelShape(params, sortings), null, `${why} 는 저널로 가야 한다`)
80
+ }
81
+ })
82
+
83
+ test('★ 적지 못한 줄이 있으면 이 경로를 쓰지 않는다 — 그 창에는 구멍이 있다', () => {
84
+ assert.match(
85
+ fastPath,
86
+ /subjectWriteFailures\(domainId, instanceId\) > 0\) return null/,
87
+ '실패가 세어져 있으면 온전한 쪽(저널)으로 보내야 한다'
88
+ )
89
+ })
90
+
91
+ test('★ 원시 값을 직접 날짜로 읽지 않는다 — 시간대가 어긋난다', () => {
92
+ /*
93
+ * sqlite 가 주는 값은 시간대가 없는 문자열이고, `new Date()` 로 읽으면 한국 시간으로 해석해 UTC 로
94
+ * 9시간을 뺀다. 실측: 커서의 시각이 9시간 이르게 나가 이어 읽기가 구간을 건너뛰고 본 줄이 또 나왔다
95
+ * (120건 중 15건). 그래서 경계는 엔티티로 받고, 커서의 시각은 저널 줄에서 가져온다.
96
+ */
97
+ /* 경계는 엔티티로 받는다 — 숫자(리비전)는 원시 값이어도 뜻이 변하지 않으므로 날짜에만 걸린다. */
98
+ assert.doesNotMatch(fastPath, /getRawOne<\{ at:/, '날짜를 원시 값으로 받던 자리가 남아 있다')
99
+ assert.match(fastPath, /sub\?\.eventTime instanceof Date/)
100
+ assert.match(fastPath, /journal\?\.eventTime instanceof Date/)
101
+ assert.match(fastPath, /row\?\.eventTime instanceof Date/, '커서의 시각은 저널 줄의 날짜여야 한다')
102
+ })
103
+
104
+ test('★ 담지 못한 구간을 「사건이 없다」로 그리게 만들지 않는다', () => {
105
+ /* 경계 아래를 물으면 물러난다. */
106
+ assert.match(fastPath, /cursor\.at\.getTime\(\) <= cov\.from\.getTime\(\)\) return null/)
107
+ assert.match(fastPath, /shape\.untilTime\.getTime\(\) < cov\.from\.getTime\(\)\) return null/)
108
+ /*
109
+ * 이 대상의 줄을 다 냈을 때가 가장 위험한 자리다. 커서를 내지 않으면 화면은 「여기서 끝」으로 읽고,
110
+ * 경계 아래에 저널만 아는 사건이 있으면 그것이 거짓이 된다.
111
+ */
112
+ assert.match(fastPath, /boundaryCursor\(cov\.from\)/, '짧게 끝난 페이지는 경계 아래를 읽을 커서를 내야 한다')
113
+ assert.match(fastPath, /function boundaryCursor/, '그 커서를 만드는 곳이 있어야 한다')
114
+ /* 저널의 처음까지 덮을 때만 끝이다. */
115
+ assert.match(fastPath, /cov\.full\s*\n?\s*\?\s*undefined/, '덮고 있을 때만 커서를 내지 않는다')
116
+ })
117
+
118
+ test('★ 전체 건수와 「더 있나」를 함께 정직하게 낸다 — 화면은 건수를 모를 때 capped 로 판단한다', () => {
119
+ /*
120
+ * 화면의 `hasMore` 는 전체 건수를 모르면 `capped` 를 본다. 그래서 「모른다」면서 `capped` 를 내리면
121
+ * 목록이 알리지 않고 끝난 것처럼 보인다. 두 칸이 한 판단에서 나와야 한다.
122
+ */
123
+ assert.match(fastPath, /const capped = total === undefined/)
124
+ assert.match(fastPath, /if \(page\.fullyCovered\)/, '덮고 있을 때만 정확한 건수를 낸다')
125
+ })
126
+
127
+ test('★ 화면이 부르는 자리에 붙어 있다 — 새 문을 열어 두고 기다리지 않는다', () => {
128
+ /* 이 한 줄이 없으면 테이블은 채워지고도 쓰이지 않는다(2026-08-27 에 실제로 그랬다). */
129
+ assert.match(journal, /const fast = await readModelPage\(domain\.id, instanceIds\[0\], effective, limit\)/)
130
+ assert.match(journal, /if \(fast\) return fast/)
131
+ /* 붙는 자리가 저널 질의를 만들기 **전**이어야 한다 — 뒤에 두면 느린 질의를 이미 치른 뒤다. */
132
+ assert.ok(
133
+ journal.indexOf('readModelPage(domain.id') < journal.indexOf('const qb = getQueryBuilderFromListParams'),
134
+ '저널 질의를 만들기 전에 물어야 한다'
135
+ )
136
+ /* 트윈 하나일 때만 — 여러 트윈을 합친 목록은 이 테이블의 물음이 아니다. */
137
+ assert.match(journal, /if \(instanceIds\.length === 1\) \{\s*\n\s*const fast = await readModelPage/)
138
+ })
139
+
140
+ test('품목만 식별자를 되돌린다 — 적는 쪽과 같은 함수로', () => {
141
+ /*
142
+ * 화면은 짧게 줄인 품목 식별자를 들고 있고(`net/class/product-M1`) 저장된 것은 전체 식별자다.
143
+ * 되돌림을 두 벌로 적으면 어느 날 서로 다른 답을 내고, 그때 이력이 빈다.
144
+ */
145
+ assert.match(fastPath, /if \(kind !== 'item'\) return value/)
146
+ assert.match(fastPath, /itemResolverFor\(domainId, instanceId, det\.model\)/)
147
+ assert.match(fastPath, /if \(!itemOf\) return null/, '선언을 모르면 짐작하지 않고 물러난다')
148
+ })