@things-factory/headless-twin 10.0.9 → 10.0.10

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 (353) hide show
  1. package/dist-server/engine/canonical-ingest.d.ts +2 -2
  2. package/dist-server/engine/canonical-ingest.js +24 -2
  3. package/dist-server/engine/canonical-ingest.js.map +1 -1
  4. package/dist-server/engine/command-routing.js +1 -1
  5. package/dist-server/engine/command-routing.js.map +1 -1
  6. package/dist-server/engine/energy-topology.d.ts +86 -0
  7. package/dist-server/engine/energy-topology.js +144 -0
  8. package/dist-server/engine/energy-topology.js.map +1 -0
  9. package/dist-server/engine/index.d.ts +8 -3
  10. package/dist-server/engine/index.js +10 -3
  11. package/dist-server/engine/index.js.map +1 -1
  12. package/dist-server/engine/kpi-baseline.d.ts +1 -1
  13. package/dist-server/engine/kpi-baseline.js.map +1 -1
  14. package/dist-server/engine/kpi-fold.d.ts +137 -1
  15. package/dist-server/engine/kpi-fold.js +225 -0
  16. package/dist-server/engine/kpi-fold.js.map +1 -1
  17. package/dist-server/engine/kpi-query.d.ts +1 -1
  18. package/dist-server/engine/kpi-query.js +212 -18
  19. package/dist-server/engine/kpi-query.js.map +1 -1
  20. package/dist-server/engine/kpi-target.d.ts +17 -0
  21. package/dist-server/engine/kpi-target.js +24 -2
  22. package/dist-server/engine/kpi-target.js.map +1 -1
  23. package/dist-server/engine/live-feed-registry.d.ts +25 -0
  24. package/dist-server/engine/live-feed-registry.js +51 -0
  25. package/dist-server/engine/live-feed-registry.js.map +1 -0
  26. package/dist-server/engine/load-meter.d.ts +181 -0
  27. package/dist-server/engine/load-meter.js +267 -0
  28. package/dist-server/engine/load-meter.js.map +1 -0
  29. package/dist-server/engine/local-declarations.d.ts +262 -0
  30. package/dist-server/engine/local-declarations.js +528 -0
  31. package/dist-server/engine/local-declarations.js.map +1 -0
  32. package/dist-server/engine/model-basis.d.ts +39 -6
  33. package/dist-server/engine/model-basis.js +69 -9
  34. package/dist-server/engine/model-basis.js.map +1 -1
  35. package/dist-server/engine/{board-vocabulary.d.ts → model-vocabulary.d.ts} +4 -4
  36. package/dist-server/engine/{board-vocabulary.js → model-vocabulary.js} +37 -19
  37. package/dist-server/engine/model-vocabulary.js.map +1 -0
  38. package/dist-server/engine/oee-accumulator.d.ts +28 -0
  39. package/dist-server/engine/oee-accumulator.js +26 -1
  40. package/dist-server/engine/oee-accumulator.js.map +1 -1
  41. package/dist-server/engine/operation-basis.d.ts +32 -0
  42. package/dist-server/engine/operation-basis.js +87 -0
  43. package/dist-server/engine/operation-basis.js.map +1 -0
  44. package/dist-server/engine/property-effects.d.ts +30 -0
  45. package/dist-server/engine/property-effects.js +192 -0
  46. package/dist-server/engine/property-effects.js.map +1 -0
  47. package/dist-server/engine/runtime-key.d.ts +1 -1
  48. package/dist-server/engine/runtime-key.js +1 -1
  49. package/dist-server/engine/runtime-key.js.map +1 -1
  50. package/dist-server/engine/spec-coverage.js +3 -3
  51. package/dist-server/engine/spec-coverage.js.map +1 -1
  52. package/dist-server/engine/state-axes.d.ts +7 -2
  53. package/dist-server/engine/state-axes.js +8 -3
  54. package/dist-server/engine/state-axes.js.map +1 -1
  55. package/dist-server/engine/structure-diff.js +1 -1
  56. package/dist-server/engine/structure-diff.js.map +1 -1
  57. package/dist-server/engine/travel-estimator.d.ts +9 -2
  58. package/dist-server/engine/travel-estimator.js +11 -6
  59. package/dist-server/engine/travel-estimator.js.map +1 -1
  60. package/dist-server/engine/twin-engine.d.ts +332 -47
  61. package/dist-server/engine/twin-engine.js +1314 -279
  62. package/dist-server/engine/twin-engine.js.map +1 -1
  63. package/dist-server/engine/warm-start.d.ts +38 -4
  64. package/dist-server/engine/warm-start.js +38 -9
  65. package/dist-server/engine/warm-start.js.map +1 -1
  66. package/dist-server/index.d.ts +1 -0
  67. package/dist-server/index.js +17 -11
  68. package/dist-server/index.js.map +1 -1
  69. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.d.ts +5 -0
  70. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js +48 -0
  71. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js.map +1 -0
  72. package/dist-server/migrations/index.d.ts +2 -0
  73. package/dist-server/migrations/index.js +10 -0
  74. package/dist-server/migrations/index.js.map +1 -0
  75. package/dist-server/service/index.d.ts +3 -2
  76. package/dist-server/service/index.js +25 -17
  77. package/dist-server/service/index.js.map +1 -1
  78. package/dist-server/service/reference/discovery-result.d.ts +1 -1
  79. package/dist-server/service/reference/discovery-result.js +2 -2
  80. package/dist-server/service/reference/discovery-result.js.map +1 -1
  81. package/dist-server/service/reference/ingest-space.d.ts +24 -1
  82. package/dist-server/service/reference/ingest-space.js +17 -1
  83. package/dist-server/service/reference/ingest-space.js.map +1 -1
  84. package/dist-server/service/reference/reference-live.d.ts +23 -0
  85. package/dist-server/service/reference/reference-live.js +121 -4
  86. package/dist-server/service/reference/reference-live.js.map +1 -1
  87. package/dist-server/service/reference/reference-master.d.ts +194 -13
  88. package/dist-server/service/reference/reference-master.js +159 -28
  89. package/dist-server/service/reference/reference-master.js.map +1 -1
  90. package/dist-server/service/reference/reference-resolver.d.ts +31 -1
  91. package/dist-server/service/reference/reference-resolver.js +223 -12
  92. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  93. package/dist-server/service/reference/template-registry.d.ts +19 -3
  94. package/dist-server/service/reference/template-registry.js.map +1 -1
  95. package/dist-server/service/twin-audit/command-audit.d.ts +37 -0
  96. package/dist-server/service/twin-audit/command-audit.js +53 -0
  97. package/dist-server/service/twin-audit/command-audit.js.map +1 -0
  98. package/dist-server/service/twin-audit/index.d.ts +4 -0
  99. package/dist-server/service/twin-audit/index.js +8 -0
  100. package/dist-server/service/twin-audit/index.js.map +1 -0
  101. package/dist-server/service/twin-audit/twin-audit-event.d.ts +24 -0
  102. package/dist-server/service/twin-audit/twin-audit-event.js +125 -0
  103. package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -0
  104. package/dist-server/service/twin-audit/twin-audit-query.d.ts +4 -0
  105. package/dist-server/service/twin-audit/twin-audit-query.js +76 -0
  106. package/dist-server/service/twin-audit/twin-audit-query.js.map +1 -0
  107. package/dist-server/service/twin-control/twin-control-mutation.js +21 -1
  108. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  109. package/dist-server/service/twin-event/twin-event-keys.d.ts +1 -1
  110. package/dist-server/service/twin-event/twin-event-keys.js +1 -1
  111. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  112. package/dist-server/service/twin-event/twin-event.d.ts +14 -5
  113. package/dist-server/service/twin-event/twin-event.js +46 -14
  114. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  115. package/dist-server/service/twin-forecast/forecast-metrics.d.ts +13 -0
  116. package/dist-server/service/twin-forecast/forecast-metrics.js +57 -0
  117. package/dist-server/service/twin-forecast/forecast-metrics.js.map +1 -0
  118. package/dist-server/service/twin-forecast/forecast-tuning.d.ts +12 -0
  119. package/dist-server/service/twin-forecast/forecast-tuning.js +44 -0
  120. package/dist-server/service/twin-forecast/forecast-tuning.js.map +1 -0
  121. package/dist-server/service/twin-forecast/gap-analytics.d.ts +28 -2
  122. package/dist-server/service/twin-forecast/gap-analytics.js +43 -17
  123. package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
  124. package/dist-server/service/twin-forecast/twin-forecast-query.js +217 -38
  125. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  126. package/dist-server/service/twin-instance/twin-instance.d.ts +38 -5
  127. package/dist-server/service/twin-instance/twin-instance.js +66 -20
  128. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  129. package/dist-server/service/twin-journal/twin-journal-query.d.ts +5 -2
  130. package/dist-server/service/twin-journal/twin-journal-query.js +71 -9
  131. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  132. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +3 -5
  133. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +24 -27
  134. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  135. package/dist-server/service/twin-metrics/twin-metrics-query.d.ts +1 -0
  136. package/dist-server/service/twin-metrics/twin-metrics-query.js +62 -0
  137. package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
  138. package/dist-server/service/twin-model/epcis-coverage.d.ts +14 -0
  139. package/dist-server/service/twin-model/epcis-coverage.js +174 -0
  140. package/dist-server/service/twin-model/epcis-coverage.js.map +1 -0
  141. package/dist-server/service/twin-model/iec61850-coverage.d.ts +14 -0
  142. package/dist-server/service/twin-model/iec61850-coverage.js +98 -0
  143. package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -0
  144. package/dist-server/service/twin-model/index.d.ts +13 -0
  145. package/dist-server/service/twin-model/index.js +29 -0
  146. package/dist-server/service/twin-model/index.js.map +1 -0
  147. package/dist-server/service/twin-model/isa95-coverage.d.ts +48 -0
  148. package/dist-server/service/twin-model/isa95-coverage.js +155 -0
  149. package/dist-server/service/twin-model/isa95-coverage.js.map +1 -0
  150. package/dist-server/service/twin-model/project-structure.d.ts +16 -0
  151. package/dist-server/service/twin-model/project-structure.js +173 -0
  152. package/dist-server/service/twin-model/project-structure.js.map +1 -0
  153. package/dist-server/service/twin-model/standard-coverage.d.ts +9 -0
  154. package/dist-server/service/twin-model/standard-coverage.js +37 -0
  155. package/dist-server/service/twin-model/standard-coverage.js.map +1 -0
  156. package/dist-server/service/twin-model/twin-equipment.d.ts +64 -0
  157. package/dist-server/service/twin-model/twin-equipment.js +134 -0
  158. package/dist-server/service/twin-model/twin-equipment.js.map +1 -0
  159. package/dist-server/service/twin-model/twin-lineage-query.d.ts +3 -0
  160. package/dist-server/service/twin-model/twin-lineage-query.js +206 -0
  161. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -0
  162. package/dist-server/service/twin-model/twin-location.d.ts +58 -0
  163. package/dist-server/service/twin-model/twin-location.js +130 -0
  164. package/dist-server/service/twin-model/twin-location.js.map +1 -0
  165. package/dist-server/service/twin-model/twin-model-item-query.d.ts +141 -0
  166. package/dist-server/service/twin-model/twin-model-item-query.js +852 -0
  167. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -0
  168. package/dist-server/service/twin-model/twin-model-mutation.d.ts +57 -0
  169. package/dist-server/service/twin-model/twin-model-mutation.js +490 -0
  170. package/dist-server/service/twin-model/twin-model-mutation.js.map +1 -0
  171. package/dist-server/service/twin-model/twin-model-query.d.ts +43 -0
  172. package/dist-server/service/twin-model/twin-model-query.js +551 -0
  173. package/dist-server/service/twin-model/twin-model-query.js.map +1 -0
  174. package/dist-server/service/twin-model/twin-model-tree-query.d.ts +3 -0
  175. package/dist-server/service/twin-model/twin-model-tree-query.js +158 -0
  176. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -0
  177. package/dist-server/service/twin-model/twin-operation.d.ts +61 -0
  178. package/dist-server/service/twin-model/twin-operation.js +137 -0
  179. package/dist-server/service/twin-model/twin-operation.js.map +1 -0
  180. package/dist-server/service/twin-space/move-space.d.ts +65 -0
  181. package/dist-server/service/twin-space/move-space.js +166 -0
  182. package/dist-server/service/twin-space/move-space.js.map +1 -0
  183. package/dist-server/service/twin-space/space-integrity.d.ts +97 -0
  184. package/dist-server/service/twin-space/space-integrity.js +182 -0
  185. package/dist-server/service/twin-space/space-integrity.js.map +1 -0
  186. package/dist-server/service/twin-space/twin-space-area.js +4 -4
  187. package/dist-server/service/twin-space/twin-space-area.js.map +1 -1
  188. package/dist-server/service/twin-space/twin-space-representation.js +6 -6
  189. package/dist-server/service/twin-space/twin-space-representation.js.map +1 -1
  190. package/dist-server/service/twin-space/twin-space-resolver.d.ts +63 -1
  191. package/dist-server/service/twin-space/twin-space-resolver.js +356 -18
  192. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  193. package/dist-server/service/twin-space/twin-space.d.ts +17 -1
  194. package/dist-server/service/twin-space/twin-space.js +15 -5
  195. package/dist-server/service/twin-space/twin-space.js.map +1 -1
  196. package/dist-server/service/twin-structure/twin-structure.d.ts +1 -1
  197. package/dist-server/service/twin-structure/twin-structure.js +8 -8
  198. package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
  199. package/dist-shared/axis-read.d.ts +12 -0
  200. package/dist-shared/axis-read.js +32 -0
  201. package/dist-shared/axis-read.js.map +1 -0
  202. package/{dist-server/engine → dist-shared}/entity-delta.d.ts +3 -0
  203. package/{dist-server/engine → dist-shared}/entity-delta.js +102 -2
  204. package/dist-shared/entity-delta.js.map +1 -0
  205. package/dist-shared/kpi-broadcast.js.map +1 -0
  206. package/dist-shared/twin-level.js.map +1 -0
  207. package/package.json +14 -12
  208. package/server/engine/canonical-ingest.ts +36 -4
  209. package/server/engine/command-routing.ts +1 -1
  210. package/server/engine/energy-topology.ts +189 -0
  211. package/server/engine/index.ts +10 -3
  212. package/server/engine/kpi-baseline.ts +1 -1
  213. package/server/engine/kpi-fold.ts +389 -1
  214. package/server/engine/kpi-query.ts +214 -19
  215. package/server/engine/kpi-target.ts +24 -2
  216. package/server/engine/live-feed-registry.ts +58 -0
  217. package/server/engine/load-meter.ts +384 -0
  218. package/server/engine/local-declarations.ts +700 -0
  219. package/server/engine/model-basis.ts +78 -10
  220. package/server/engine/{board-vocabulary.ts → model-vocabulary.ts} +36 -15
  221. package/server/engine/oee-accumulator.ts +34 -1
  222. package/server/engine/operation-basis.ts +100 -0
  223. package/server/engine/property-effects.ts +199 -0
  224. package/server/engine/runtime-key.ts +1 -1
  225. package/server/engine/spec-coverage.ts +3 -3
  226. package/server/engine/state-axes.ts +8 -3
  227. package/server/engine/structure-diff.ts +1 -1
  228. package/server/engine/travel-estimator.ts +11 -6
  229. package/server/engine/twin-engine.ts +1414 -274
  230. package/server/engine/warm-start.ts +83 -12
  231. package/server/index.ts +13 -10
  232. package/server/migrations/1786000000000-RenameTwinInstanceBoardToModel.ts +43 -0
  233. package/server/migrations/index.ts +7 -0
  234. package/server/service/index.ts +9 -1
  235. package/server/service/reference/discovery-result.ts +2 -2
  236. package/server/service/reference/ingest-space.ts +35 -1
  237. package/server/service/reference/reference-live.ts +119 -4
  238. package/server/service/reference/reference-master.ts +276 -35
  239. package/server/service/reference/reference-resolver.ts +232 -12
  240. package/server/service/reference/template-registry.ts +20 -5
  241. package/server/service/twin-audit/command-audit.ts +81 -0
  242. package/server/service/twin-audit/index.ts +5 -0
  243. package/server/service/twin-audit/twin-audit-event.ts +112 -0
  244. package/server/service/twin-audit/twin-audit-query.ts +72 -0
  245. package/server/service/twin-control/twin-control-mutation.ts +22 -1
  246. package/server/service/twin-event/twin-event-keys.ts +1 -1
  247. package/server/service/twin-event/twin-event.ts +52 -15
  248. package/server/service/twin-forecast/forecast-metrics.ts +60 -0
  249. package/server/service/twin-forecast/forecast-tuning.ts +40 -0
  250. package/server/service/twin-forecast/gap-analytics.ts +48 -9
  251. package/server/service/twin-forecast/twin-forecast-query.ts +208 -36
  252. package/server/service/twin-instance/twin-instance.ts +126 -25
  253. package/server/service/twin-journal/twin-journal-query.ts +74 -10
  254. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +23 -22
  255. package/server/service/twin-metrics/twin-metrics-query.ts +59 -2
  256. package/server/service/twin-model/epcis-coverage.ts +195 -0
  257. package/server/service/twin-model/iec61850-coverage.ts +117 -0
  258. package/server/service/twin-model/index.ts +25 -0
  259. package/server/service/twin-model/isa95-coverage.ts +190 -0
  260. package/server/service/twin-model/project-structure.ts +199 -0
  261. package/server/service/twin-model/standard-coverage.ts +35 -0
  262. package/server/service/twin-model/twin-equipment.ts +150 -0
  263. package/server/service/twin-model/twin-lineage-query.ts +193 -0
  264. package/server/service/twin-model/twin-location.ts +140 -0
  265. package/server/service/twin-model/twin-model-item-query.ts +848 -0
  266. package/server/service/twin-model/twin-model-mutation.ts +503 -0
  267. package/server/service/twin-model/twin-model-query.ts +537 -0
  268. package/server/service/twin-model/twin-model-tree-query.ts +180 -0
  269. package/server/service/twin-model/twin-operation.ts +150 -0
  270. package/server/service/twin-space/move-space.ts +262 -0
  271. package/server/service/twin-space/space-integrity.ts +293 -0
  272. package/server/service/twin-space/twin-space-area.ts +4 -4
  273. package/server/service/twin-space/twin-space-representation.ts +6 -6
  274. package/server/service/twin-space/twin-space-resolver.ts +355 -21
  275. package/server/service/twin-space/twin-space.ts +40 -6
  276. package/server/service/twin-structure/twin-structure.ts +10 -10
  277. package/shared/axis-read.ts +28 -0
  278. package/{server/engine → shared}/entity-delta.ts +96 -1
  279. package/test/adopt-structure-live.test.ts +133 -0
  280. package/test/attention-digest.test.ts +3 -3
  281. package/test/axis-read.test.ts +87 -0
  282. package/test/boot-resume.test.ts +205 -0
  283. package/test/canonical-ingest-vocabularies.test.ts +118 -0
  284. package/test/capability-mapping.test.ts +1 -1
  285. package/test/command-routing.test.ts +1 -1
  286. package/test/declaration-reaches-model.test.ts +178 -0
  287. package/test/discovery-result.test.ts +1 -1
  288. package/test/energy-topology.test.ts +113 -0
  289. package/test/entity-delta.test.ts +171 -1
  290. package/test/event-time-column.test.ts +67 -0
  291. package/test/forecast-metrics.test.ts +71 -0
  292. package/test/forecast-tuning.test.ts +42 -0
  293. package/test/gap-analytics.test.ts +34 -7
  294. package/test/ingest-bench.test.ts +6 -6
  295. package/test/ingest-running-guard.test.ts +136 -0
  296. package/test/ingest-space.test.ts +29 -1
  297. package/test/instance-cache-lifecycle.test.ts +110 -0
  298. package/test/kernel-kind-guard.test.ts +96 -0
  299. package/test/kpi-baseline-db.test.ts +3 -1
  300. package/test/kpi-baseline.test.ts +3 -3
  301. package/test/kpi-fold.test.ts +393 -2
  302. package/test/kpi-query-bench.test.ts +5 -3
  303. package/test/lineage-survives-restart.test.ts +203 -0
  304. package/test/live-feed-registry.test.ts +67 -0
  305. package/test/live-kernel-facts.test.ts +161 -0
  306. package/test/live-mirror-parity.test.ts +22 -15
  307. package/test/load-meter.test.ts +314 -0
  308. package/test/local-declarations.test.ts +554 -0
  309. package/test/master-to-twin.test.ts +121 -33
  310. package/test/model-basis.test.ts +56 -1
  311. package/test/{board-vocabulary.test.ts → model-vocabulary.test.ts} +25 -25
  312. package/test/move-space.test.ts +149 -0
  313. package/test/mutation-gate.test.ts +19 -5
  314. package/test/oee-accumulator.test.ts +26 -7
  315. package/test/operation-basis.test.ts +100 -0
  316. package/test/operations-capability-db.test.ts +165 -0
  317. package/test/project-structure-db.test.ts +226 -0
  318. package/test/projection-reaches-screen.test.ts +272 -0
  319. package/test/property-effects.test.ts +96 -0
  320. package/test/resync-origin-site.test.ts +77 -0
  321. package/test/scale-twin-bench.test.ts +5 -5
  322. package/test/snapshot-freshness.test.ts +60 -0
  323. package/test/space-integrity.test.ts +223 -0
  324. package/test/standard-coverage.test.ts +94 -0
  325. package/test/state-axes.test.ts +1 -1
  326. package/test/streamline-e2e.test.ts +14 -14
  327. package/test/structure-revision-db.test.ts +11 -11
  328. package/test/tenant-registry-db.test.ts +5 -5
  329. package/test/twin-audit.test.ts +83 -0
  330. package/test/twin-model-item-db.test.ts +275 -0
  331. package/test/twin-model-tree-db.test.ts +176 -0
  332. package/test/twin-origin-resync.test.ts +114 -0
  333. package/test/warm-start-seam.test.ts +6 -6
  334. package/test/warm-start.test.ts +88 -3
  335. package/tsconfig.json +6 -1
  336. package/tsconfig.shared.json +23 -0
  337. package/tsconfig.shared.tsbuildinfo +1 -0
  338. package/tsconfig.tsbuildinfo +1 -0
  339. package/dist-server/engine/board-vocabulary.js.map +0 -1
  340. package/dist-server/engine/entity-delta.js.map +0 -1
  341. package/dist-server/engine/kpi-broadcast.js.map +0 -1
  342. package/dist-server/engine/twin-level.js.map +0 -1
  343. package/dist-server/service/twin-event/backfill-keys.d.ts +0 -11
  344. package/dist-server/service/twin-event/backfill-keys.js +0 -63
  345. package/dist-server/service/twin-event/backfill-keys.js.map +0 -1
  346. package/dist-server/tsconfig.tsbuildinfo +0 -1
  347. package/server/service/twin-event/backfill-keys.ts +0 -72
  348. /package/{dist-server/engine → dist-shared}/kpi-broadcast.d.ts +0 -0
  349. /package/{dist-server/engine → dist-shared}/kpi-broadcast.js +0 -0
  350. /package/{dist-server/engine → dist-shared}/twin-level.d.ts +0 -0
  351. /package/{dist-server/engine → dist-shared}/twin-level.js +0 -0
  352. /package/{server/engine → shared}/kpi-broadcast.ts +0 -0
  353. /package/{server/engine → shared}/twin-level.ts +0 -0
@@ -7,9 +7,39 @@ import { readSpecCoverage } from '../../engine/spec-coverage.js'
7
7
  import { describeModelBasis, modelBasisChanged, type ModelBasis } from '../../engine/model-basis.js'
8
8
  import { TwinEvent } from '../twin-event/twin-event.js'
9
9
  import { GAP_SAMPLES, aggregatePoints, computeSpc, gapMetrics, learnedCalibration, type GapPoint } from './gap-analytics.js'
10
+ import { FORECAST_METRICS, type MetricFn } from './forecast-metrics.js'
11
+ import { forecastStepMs } from './forecast-tuning.js'
10
12
 
11
13
  /* 커널 예측 함수 — CJS require(twin-engine 과 동일 방식). */
12
- const { monteCarloForecast } = require('@operato/twin-kernel')
14
+ const { monteCarloForecastAsync } = require('@operato/twin-kernel')
15
+
16
+ /**
17
+ * 이벤트 루프에 자리를 내주는 방법 — **커널이 아니라 여기가 안다.**
18
+ *
19
+ * `setImmediate` 는 대기 중인 I/O 다음 차례에 돌아온다. 마이크로태스크(`Promise.resolve()`)로는
20
+ * 자리가 가지 않는다 — 그러면 예측이 도는 동안 라이브 틱도 HTTP 도 여전히 멈춘다.
21
+ *
22
+ * ── 내준 시간은 **부하가 아니다** ───────────────────────────────────────────
23
+ * 부하 계기는 「이 트윈이 루프를 얼마나 붙잡았나」를 재는 자다. 양보를 넣은 뒤로 벽시계 시간에는
24
+ * 남에게 넘긴 시간이 섞이는데, 그것까지 부하로 적으면 계기가 거짓말을 한다 — 하필 이 문제를 짚어낸
25
+ * 그 계기다. 그래서 내준 시간을 따로 세어 빼고 적는다.
26
+ */
27
+ function breather() {
28
+ let yieldedMs = 0
29
+ return {
30
+ fn: () =>
31
+ new Promise<void>(resolve => {
32
+ const t = performance.now()
33
+ setImmediate(() => {
34
+ yieldedMs += performance.now() - t
35
+ resolve()
36
+ })
37
+ }),
38
+ get ms() {
39
+ return yieldedMs
40
+ }
41
+ }
42
+ }
13
43
 
14
44
  /*
15
45
  * 트윈 예측 채널 — 상향 query. 트윈의 "예측(predict)" 면(설계 §3.5, ADR-0009).
@@ -23,14 +53,16 @@ const DEFAULT_SCENARIO = {
23
53
  { kind: 'inbound-arrival', rate: { distribution: 'poisson', meanPerHour: 60 }, content: { skuMix: [{ gtin: GTIN, weight: 1 }], qtyPerLine: { min: 4, max: 12 } } }
24
54
  ]
25
55
  }
26
- const METRICS: Record<string, (s: any) => number> = {
27
- items: s => (s.items || []).length,
28
- occupancy: s => (s.locations || []).reduce((a: number, n: any) => a + (n.occupancy || 0), 0),
29
- tasks: s => (s.tasks || []).length,
30
- orders: s => (s.orders || []).length,
31
- // 이행 지표 라이브 예측의 핵심 질문("언제 나가나·백로그 언제 풀리나").
32
- shipped: s => (s.orders || []).filter((o: any) => o.status === 'shipped').length,
33
- backlog: s => (s.orders || []).filter((o: any) => o.status !== 'shipped' && o.status !== 'cancelled').length
56
+ /*
57
+ * 지표 표는 `forecast-metrics.ts` 소유한다 — 여기 있던 것을 옮겼다(시험이 이 파일을 불러올 수 없다).
58
+ *
59
+ * **모르는 지표를 `items` 로 갈음하지 않는다.** 예전에는 `METRICS[metric] ?? METRICS.items` 였다.
60
+ * 에너지 트윈에서 `peakKW` 를 물으면 재고 곡선이 그려지고, 재고가 없으니 **0 이 평평하게** 나왔다 —
61
+ * 화면은 그것을 「피크가 0」으로 읽는다. 지어낸 답보다 모른다고 말하는 편이 낫다.
62
+ */
63
+ function metricFn(metric?: string): MetricFn | null {
64
+ if (!metric) return FORECAST_METRICS.items
65
+ return FORECAST_METRICS[metric] ?? null
34
66
  }
35
67
 
36
68
  /* 학습된 보정 저장(P3.2, 인메모리) — key=domain:instance:metric:horizon → {biasShift, halfWidth, …}.
@@ -42,25 +74,40 @@ const CALIBRATION = new Map<
42
74
  >()
43
75
  const calKey = (d: string, i: string, m: string, h: number) => `${d}:${i}:${m}:${h}`
44
76
 
45
- /* 갭 point 계산(공용) — N개 과거 vantage 각각 forecast-at-T vs actual-at-T+H(결정적 백테스트). gapTrend query·learn mutation 공용. */
46
- async function gapPoints(domainId: string, instanceId: string, m: (s: any) => number, H: number, N: number, scen: any): Promise<any[]> {
77
+ /**
78
+ * 트윈이 백테스트할 있는 구간 [첫 이벤트, 마지막 이벤트 H].
79
+ * 지평 H 만큼 앞을 봐야 "그때 예측한 것"과 "실제로 벌어진 것"을 맞대 볼 수 있으므로 끝에서 H 를 뺀다.
80
+ */
81
+ async function gapWindow(domainId: string, instanceId: string, H: number): Promise<{ t0: number; lastVantage: number } | null> {
47
82
  const where = { domain: { id: domainId }, instanceId }
48
83
  const repo = getRepository(TwinEvent)
49
84
  const first = await repo.findOne({ where, order: { eventTime: 'ASC' } })
50
85
  const last = await repo.findOne({ where, order: { eventTime: 'DESC' } })
51
- if (!first || !last) return []
52
- const t0 = Date.parse(first.eventTime)
53
- const lastVantage = Date.parse(last.eventTime) - H
54
- if (!(lastVantage > t0)) return []
55
- const points: any[] = []
56
- for (let i = 0; i < N; i++) {
57
- const T = t0 + ((lastVantage - t0) * i) / (N - 1)
86
+ if (!first?.eventTime || !last?.eventTime) return null
87
+ const t0 = first.eventTime.getTime()
88
+ const lastVantage = last.eventTime.getTime() - H
89
+ return lastVantage > t0 ? { t0, lastVantage } : null
90
+ }
91
+
92
+ /**
93
+ * 갭 point 계산(공용) — 주어진 vantage 각각에서 forecast-at-T vs actual-at-T+H(결정적 백테스트).
94
+ *
95
+ * **vantage 는 밖에서 준다.** 예전에는 이 함수가 자기 저널의 첫·마지막 이벤트로 격자를 스스로 만들었다.
96
+ * 그러면 트윈마다 격자가 달라져, 공동배치 합산에서 `points[3]` 끼리 더할 때 **서로 다른 시각의 값**이
97
+ * 더해졌다(자세한 사연은 `aggregatePoints` 주석).
98
+ *
99
+ * 반환 배열은 격자와 **같은 길이**이고, 그 vantage 를 재지 못했으면 그 자리에 `null` 을 둔다.
100
+ * 건너뛰고 밀어 담으면 배열 인덱스가 시간축이기를 그만둔다.
101
+ */
102
+ async function gapPoints(domainId: string, instanceId: string, m: (s: any) => number, H: number, grid: number[], scen: any): Promise<(any | null)[]> {
103
+ const points: (any | null)[] = []
104
+ for (const T of grid) {
58
105
  const k = await TwinEngine.buildForecastKernelAt(domainId, instanceId, new Date(T).toISOString()).catch(() => null)
59
- if (!k) continue
60
- const dist = monteCarloForecast(k, { runs: 12, horizonMs: H, scenario: scen, tickMs: 1000, metric: m })
106
+ if (!k) { points.push(null); continue }
107
+ const dist = await monteCarloForecastAsync(k, { runs: 12, horizonMs: H, scenario: scen, tickMs: forecastStepMs(H), metric: m, yieldFn: breather().fn })
61
108
  const naive = m(k.getSnapshot())
62
109
  const st = await TwinEngine.recover(domainId, instanceId, undefined, new Date(T + H).toISOString()).catch(() => null)
63
- if (!st) continue
110
+ if (!st) { points.push(null); continue }
64
111
  const a = m(st)
65
112
  const p50 = dist?.p50 ?? naive
66
113
  points.push({ t: T, p50, min: dist?.min ?? p50, max: dist?.max ?? p50, actual: a, error: a - p50, inBand: a >= (dist?.min ?? -Infinity) && a <= (dist?.max ?? Infinity), naiveAbsError: Math.abs(a - naive), modelAbsError: Math.abs(a - p50) })
@@ -68,6 +115,21 @@ async function gapPoints(domainId: string, instanceId: string, m: (s: any) => nu
68
115
  return points
69
116
  }
70
117
 
118
+ /**
119
+ * 여러 트윈이 **함께** 백테스트할 수 있는 구간과 그 위의 vantage 격자.
120
+ *
121
+ * 겹치는 구간은 각자의 구간을 교집합한 것 — 시작은 가장 늦은 t0, 끝은 가장 이른 lastVantage.
122
+ * 한 트윈이라도 그 시각에 저널이 없으면 공장 총량을 말할 수 없기 때문이다.
123
+ * 겹치는 구간이 없으면 `null` — "0" 이 아니라 "잴 수 없음" 이다.
124
+ */
125
+ function sharedGrid(windows: { t0: number; lastVantage: number }[], N: number): number[] | null {
126
+ if (!windows.length || N < 2) return null
127
+ const t0 = Math.max(...windows.map(w => w.t0))
128
+ const lastVantage = Math.min(...windows.map(w => w.lastVantage))
129
+ if (!(lastVantage > t0)) return null
130
+ return Array.from({ length: N }, (_, i) => t0 + ((lastVantage - t0) * i) / (N - 1))
131
+ }
132
+
71
133
  @Resolver()
72
134
  export class TwinForecastQuery {
73
135
  @Query(returns => ScalarObject, {
@@ -94,14 +156,29 @@ export class TwinForecastQuery {
94
156
  : undefined
95
157
  if (!k) return null
96
158
 
97
- const m = METRICS[metric] ?? METRICS.items
159
+ const m = metricFn(metric)
160
+ /* 이 트윈이 못 세는 지표 — 빈 예측이 아니라 「그 지표는 여기 없다」고 말한다. */
161
+ if (!m) return { metric, unsupportedMetric: true, distribution: null, trajectory: [] }
98
162
  const H = horizonMs ?? 300000
99
163
  const R = runs ?? 30
100
- const step = 1000
164
+ /*
165
+ * 틱 크기는 **지평선이 정한다**(`forecast-tuning`). 1초 고정이던 시절 5분 지평선이 회차마다
166
+ * 300틱이었고, 30회차면 9,000틱·28.5초였다 — 실측으로 5초 단위와 답이 같다.
167
+ */
168
+ const step = forecastStepMs(H)
101
169
  const scen = scenario ?? DEFAULT_SCENARIO
102
170
 
103
- /* 분포: N개 확률적 미래 표본(fork 마다 seed 변주, 원본 보존). */
104
- const distribution = monteCarloForecast(k, { runs: R, horizonMs: H, scenario: scen, tickMs: step, metric: m })
171
+ /*
172
+ * 분포: N개 확률적 미래 표본(fork 마다 seed 변주, 원본 보존).
173
+ *
174
+ * **이것이 요청 처리 중 메인 이벤트 루프에서 동기로 수행된다.** 기본값으로 fork 30개 × 300틱 = 9,000틱.
175
+ * 계기판에 남겨 두지 않으면 "왜 서버가 굳었나" 를 나중에 짚을 수 없다(실제로 한 번 겪었다).
176
+ */
177
+ const tMc = performance.now()
178
+ const mcBreath = breather()
179
+ const distribution = await monteCarloForecastAsync(k, { runs: R, horizonMs: H, scenario: scen, tickMs: step, metric: m, yieldFn: mcBreath.fn })
180
+ TwinEngine.recordLoad(domainId, instanceId, 'forecast', performance.now() - tMc - mcBreath.ms)
181
+ TwinEngine.countForks(domainId, instanceId, R)
105
182
 
106
183
  /* 학습된 보정 상시 적용(P3.2) — twinLearnCalibration 이 저장한 편향·밴드가 있으면 예측을 현실로 보정(라이브가 실제로 나아짐).
107
184
  갭트렌드(gapPoints)는 raw 모델을 그대로 측정(학습 신호 보존) — 보정은 표시용 forecast 에만. */
@@ -121,15 +198,57 @@ export class TwinForecastQuery {
121
198
  ` learned on: ${describeModelBasis(stored!.basis)} / now: ${describeModelBasis(basis)}`
122
199
  )
123
200
  }
201
+ /*
202
+ * 분위수 — 표본에서 직접 뽑는다.
203
+ *
204
+ * 커널이 표본 배열을 그대로 돌려주므로(forecast.ts) 여기서 정렬해 p05·p95 를 낸다. 이것이 없으면
205
+ * 화면이 보여 줄 수 있는 폭은 관측 **최소·최대** 뿐인데, 그건 신뢰구간이 아니라 극단값 두 개다.
206
+ * "운이 나쁜 5% 에서는 이만큼" 이라고 말하려면 분위수여야 한다. 표본이 적으면(runs 가 작으면)
207
+ * 분위수도 거칠다 — 그래서 표본 수를 함께 싣는다(sampleCount). 숫자만 주고 근거를 감추지 않는다.
208
+ */
209
+ if (distribution) {
210
+ const samples: number[] = Array.isArray(distribution.samples) ? distribution.samples : []
211
+ if (samples.length) {
212
+ const sorted = [...samples].sort((a, b) => a - b)
213
+ const pct = (p: number) => sorted[Math.min(sorted.length - 1, Math.floor(p * sorted.length))]
214
+ distribution.p05 = pct(0.05)
215
+ distribution.p95 = pct(0.95)
216
+ }
217
+ distribution.sampleCount = samples.length
218
+ }
219
+
124
220
  const cal = stored && !stale ? stored : undefined
125
221
  if (cal && distribution) {
126
- const p50c = (distribution.p50 ?? 0) + cal.biasShift
222
+ /*
223
+ * `p50` 은 커널 계약상 항상 있다(`MonteCarloResult.p50: number`). 그래도 메우지 않는다 —
224
+ * 예전에는 `?? 0` 이었는데, 만약 비면 보정된 p50 이 **편향값 그 자체**가 되고 min·max 는
225
+ * 그 둘레로 그려진다. 예측이 아닌 숫자가 예측의 자리에 앉는데 아무 신호가 없다.
226
+ */
227
+ if (typeof distribution.p50 !== 'number') throw new Error('forecast distribution has no p50 — kernel contract broken')
228
+ const p50c = distribution.p50 + cal.biasShift
127
229
  distribution.p50 = Math.round(p50c)
128
230
  distribution.min = Math.round(p50c - cal.halfWidth)
129
231
  distribution.max = Math.round(p50c + cal.halfWidth)
232
+ /*
233
+ * 분위수는 **옮기기만** 한다. 보정이 배운 것은 "모델이 이만큼 치우쳐 있다"(위치)이지 분포의
234
+ * 모양이 아니다. p50 처럼 밀어 두어야 p05~p95 폭이 표본이 말한 그대로 남는다.
235
+ * (min·max 는 위에서 학습 밴드로 대체된다 — 보정이 걸리면 그 둘의 뜻이 '관측 극단' 에서
236
+ * '학습 밴드' 로 바뀐다는 뜻이다. 화면이 둘을 같은 것으로 그리면 안 된다.)
237
+ */
238
+ if (distribution.p05 !== undefined) distribution.p05 = Math.round(distribution.p05 + cal.biasShift)
239
+ if (distribution.p95 !== undefined) distribution.p95 = Math.round(distribution.p95 + cal.biasShift)
240
+ if (distribution.p90 !== undefined) distribution.p90 = Math.round(distribution.p90 + cal.biasShift)
241
+ distribution.calibrated = true
130
242
  }
131
243
 
132
- /* 예측 궤적(예측선): 단일 fork 를 horizon 까지 굴리며 지표 기록. */
244
+ /*
245
+ * 예측 궤적(예측선): 단일 fork 를 horizon 까지 실행하며 지표 기록. 이것도 잰다.
246
+ *
247
+ * 여기도 자리를 내준다 — 실측으로 이 한 회차가 1.3초였다(300틱 + 스냅샷 300번). 분포만큼은
248
+ * 아니지만, 라이브 틱 간격(1초)보다 길게 붙잡으면 그 사이 트윈들이 한 박자씩 밀린다.
249
+ */
250
+ const tTraj = performance.now()
251
+ const trajBreath = breather()
133
252
  const fc = k.fork()
134
253
  fc.scenario.load({ ...scen, seed: 1 })
135
254
  fc.scenario.start()
@@ -137,8 +256,13 @@ export class TwinForecastQuery {
137
256
  for (let t = 0; t < H; t += step) {
138
257
  fc.tick(step)
139
258
  trajectory.push({ t: t + step, value: m(fc.getSnapshot()) })
259
+ /* 틱마다 내주면 양보 자체가 비용이 된다 — 라이브 틱 간격보다 훨씬 짧은 단위로 끊는다. */
260
+ if (trajectory.length % 25 === 0) await trajBreath.fn()
140
261
  }
141
262
 
263
+ TwinEngine.recordLoad(domainId, instanceId, 'forkTick', performance.now() - tTraj - trajBreath.ms)
264
+ TwinEngine.countForks(domainId, instanceId, 1)
265
+
142
266
  const current = m(k.getSnapshot()) // vantage T 의 실제값(= naive '그대로 유지' 예측의 기준)
143
267
 
144
268
  /* 실측 오버레이 + 채점(P1) — vantage 가 과거(at)일 때만: T 이후 실제로 일어난 metric 을 T→min(now,T+H) 로 샘플.
@@ -173,15 +297,36 @@ export class TwinForecastQuery {
173
297
  }
174
298
 
175
299
  /*
176
- * 시뮬 명세 자기보고를 함께 낸다 — **상수로 굴린 예측과 현장 명세로 굴린 예측이 화면에서 같아
300
+ * 시뮬 명세 자기보고를 함께 낸다 — **상수로 계산한 예측과 현장 명세로 계산한 예측이 화면에서 같아
177
301
  * 보이지 않게.** 코드·수치만 싣고 사람이 읽는 문장은 클라가 만든다(언어 중립).
178
302
  * 커널이 이 API 를 갖지 않은 버전이면 키를 만들지 않는다(모르는 것을 "전부 기본값" 이라 단정 금지).
179
303
  */
180
- /* 보고는 **실제로 굴린 fork** 에서 읽는다 — 부모 커널(k)은 tick 하지 않으므로(예측은 fork 에서
181
- * 돈다) 부모에서 읽으면 씨앗으로 세운 예측에서는 늘 비어 있다. 숫자를 낸 주체가 보고해야 한다. */
304
+ /* 보고는 **실제로 실행한 fork** 에서 읽는다 — 부모 커널(k)은 tick 하지 않으므로(예측은 fork 에서
305
+ * 수행된다) 부모에서 읽으면 씨앗으로 세운 예측에서는 늘 비어 있다. 숫자를 낸 주체가 보고해야 한다. */
182
306
  const specCoverage = readSpecCoverage(fc) ?? readSpecCoverage(k)
183
307
  return {
184
308
  metric: metric ?? 'items', horizonMs: H, runs: R, current, distribution, trajectory, actual, scored,
309
+ /* 얼마나 잘게 굴렸나 — 숫자만 주고 근거를 감추지 않는다(같은 규율로 `specCoverage` 도 싣는다). */
310
+ tickMs: step,
311
+ /*
312
+ * **가정이 어떻게 됐나** — 걸린 개입·거절된 개입·덮어쓴 선언.
313
+ *
314
+ * 이것을 감추면 두 가지가 조용해진다: 걸리지 않은 가정(「걸었는데 왜 안 바뀌나」)과, 덮어쓴
315
+ * 선언(사용자가 what-if 결과를 트윈의 현재 상태로 읽는다). 실제로 실행한 fork(`fc`)에서 읽는다 —
316
+ * 가정을 겪은 것은 그쪽이다.
317
+ */
318
+ ...(typeof (fc as any).interventionOutcomes === 'function'
319
+ ? (() => {
320
+ const outcomes = (fc as any).interventionOutcomes()
321
+ return outcomes.length ? { interventions: outcomes } : {}
322
+ })()
323
+ : {}),
324
+ ...(typeof (fc as any).declarationOverrides === 'function'
325
+ ? (() => {
326
+ const overrides = (fc as any).declarationOverrides()
327
+ return overrides.length ? { overrides } : {}
328
+ })()
329
+ : {}),
185
330
  calibrated: !!cal,
186
331
  /* 학습해 뒀지만 모델이 바뀌어 적용하지 않았다 — 사용자가 다시 배울 수 있게 알린다. */
187
332
  ...(stale ? { calibrationStale: true } : {}),
@@ -209,18 +354,39 @@ export class TwinForecastQuery {
209
354
  @Arg('instanceIds', type => [String], { nullable: true }) instanceIds?: string[]
210
355
  ): Promise<any> {
211
356
  const domainId = context.state.domain.id
212
- const m = METRICS[metric] ?? METRICS.items
357
+ const m = metricFn(metric)
213
358
  const H = horizonMs ?? 300000
359
+ if (!m) return { metric, unsupportedMetric: true, horizonMs: H, samples: 0, points: [], spc: computeSpc([]), metrics: null, learn: null }
214
360
  const N = Math.min(30, Math.max(2, samples ?? GAP_SAMPLES)) // 표본은 서버가 관장(SPC 신뢰 하한 ~20).
215
361
  const scen = scenario ?? DEFAULT_SCENARIO
216
362
  // 공동배치(co-located) 집계도 서버에서 — 공장 단위 진단이 canonical. 미지정 시 단건.
217
363
  const ids = instanceIds?.length ? instanceIds : [instanceId]
218
- const per = await Promise.all(ids.map(id => gapPoints(domainId, id, m, H, N, scen)))
219
- const points = aggregatePoints(per as GapPoint[][])
364
+
365
+ /*
366
+ * 격자를 **먼저 하나** 만든다. 트윈마다 자기 저널로 격자를 만들면 같은 인덱스가 서로 다른
367
+ * 시각이 되어, 합산이 다른 시점의 값을 더한다(`aggregatePoints` 주석 참조).
368
+ *
369
+ * 저널이 없거나 지평 H 를 담을 만큼 길지 않은 트윈은 **백테스트에 참여할 수 없다.** 그런 트윈은
370
+ * 빼되 누구를 뺐는지 돌려준다 — 조용히 빼면 화면은 그 트윈까지 포함한 공장 총량으로 읽는다.
371
+ */
372
+ const windows = await Promise.all(ids.map(async id => ({ id, w: await gapWindow(domainId, id, H) })))
373
+ const usable = windows.filter(x => x.w)
374
+ const excluded = windows.filter(x => !x.w).map(x => x.id)
375
+ const grid = sharedGrid(usable.map(x => x.w!), N)
376
+ if (!grid) {
377
+ /* 겹치는 구간이 없다 — 잴 수 없는 것이지 정확도가 0 인 것이 아니다. */
378
+ return { metric: metric ?? 'items', horizonMs: H, samples: N, points: [], spc: computeSpc([]), metrics: null, learn: null,
379
+ coverage: { instances: ids.length, measured: 0, excluded, vantages: N, used: 0, dropped: 0, reason: 'no-common-window' } }
380
+ }
381
+
382
+ const per = await Promise.all(usable.map(x => gapPoints(domainId, x.id, m, H, grid, scen)))
383
+ const { points, droppedVantages } = aggregatePoints(per as (GapPoint | null)[][])
220
384
  const metrics = points.length ? gapMetrics(points) : null
221
385
  const spc = computeSpc(points.map(p => p.error))
222
386
  const learn = metrics ? learnedCalibration(points, metrics) : null
223
- return { metric: metric ?? 'items', horizonMs: H, samples: N, points, spc, metrics, learn }
387
+ return { metric: metric ?? 'items', horizonMs: H, samples: N, points, spc, metrics, learn,
388
+ /* 이 숫자가 **무엇으로 계산됐는지**. 24점 중 18점이면 화면이 그렇게 말해야 한다. */
389
+ coverage: { instances: ids.length, measured: usable.length, excluded, vantages: N, used: points.length, dropped: droppedVantages } }
224
390
  }
225
391
 
226
392
  /*
@@ -239,7 +405,8 @@ export class TwinForecastQuery {
239
405
  ): Promise<any> {
240
406
  const domainId = context.state.domain.id
241
407
  if (!TwinEngine.owns(domainId, instanceId)) return { ok: false } // 테넌트 격리
242
- const m = METRICS[metric] ?? METRICS.items
408
+ const m = metricFn(metric)
409
+ if (!m) return { ok: false, reason: 'unsupported-metric', metric }
243
410
  const H = horizonMs ?? 300000
244
411
  // 보정 해제 — 저장된 calibration 제거(이후 예측은 raw 로 복귀). 학습을 되돌리는 정직한 off 스위치.
245
412
  if (clear) {
@@ -247,7 +414,12 @@ export class TwinForecastQuery {
247
414
  return { ok: true, cleared: true, metric: metric ?? 'items', horizonMs: H }
248
415
  }
249
416
  // 표본·계산을 gap-analytics 로 일원화(gapTrend 진단과 동일 N·동일 수식 → 카드 예상치와 실제 보정 일치).
250
- const pts = await gapPoints(domainId, instanceId, m, H, GAP_SAMPLES, DEFAULT_SCENARIO)
417
+ /* 학습은 단건이라 격자도 트윈의 구간 하나다. 저널이 지평 H 담을 만큼 길지 않으면 배울 것이 없다. */
418
+ const w = await gapWindow(domainId, instanceId, H)
419
+ const grid = w ? sharedGrid([w], GAP_SAMPLES) : null
420
+ if (!grid) return { ok: false, reason: 'insufficient-history' }
421
+ /* 못 잰 vantage 는 `null` 로 온다 — 0 으로 세면 편향 학습이 그만큼 끌려간다. */
422
+ const pts = (await gapPoints(domainId, instanceId, m, H, grid, DEFAULT_SCENARIO)).filter(Boolean) as GapPoint[]
251
423
  if (pts.length < 3) return { ok: false, reason: 'insufficient-history' }
252
424
  const learn = learnedCalibration(pts, gapMetrics(pts))
253
425
  /* 학습한 모델의 지문을 함께 남긴다 — 이후 모델이 바뀌면 이 보정은 적용되지 않는다. */
@@ -1,20 +1,21 @@
1
1
  import { CreateDateColumn, Entity, Index, Column, RelationId, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'
2
- import { ObjectType, Field, ID, Int } from 'type-graphql'
2
+ import { ObjectType, Field, ID } from 'type-graphql'
3
3
 
4
4
  import { Domain, ScalarObject } from '@things-factory/shell'
5
- import { boardColumnTransformer } from '../../engine/board-vocabulary.js'
5
+ import { User } from '@things-factory/auth-base'
6
+ import { modelColumnTransformer } from '../../engine/model-vocabulary.js'
6
7
 
7
8
  /*
8
9
  * TwinInstance — 트윈 인스턴스 레지스트리(재부팅 복구의 앵커).
9
- * 복구(DB→replay)는 board(마스터 청사진)가 있어야 이벤트를 상태로 되짚을 수 있다 → board·kind 를 영속.
10
- * 이벤트(TwinEvent)와 달리 가변(status/revision 갱신) — 인스턴스의 "현재 등록 상태".
11
- * board 는 simple-json(멀티DB 이식 — postgres/mysql/sqlite/mssql/oracle 공통).
10
+ * 복구(DB→replay)는 model(마스터 청사진)가 있어야 이벤트를 상태로 다시 계산할 수 있다 → model·kind 를 영속.
11
+ * 이벤트(TwinEvent)와 달리 가변(status 갱신) — 인스턴스의 "현재 등록 상태".
12
+ * model 는 simple-json(멀티DB 이식 — postgres/mysql/sqlite/mssql/oracle 공통).
12
13
  * (CLAUDE.md: 모든 @ObjectType/@Field 는 영문 description 필수.)
13
14
  */
14
15
  @Entity()
15
16
  @Index('ix_twin_instance_0', (e: TwinInstance) => [e.domain, e.instanceId], { unique: true })
16
17
  @Index('ix_twin_instance_1', (e: TwinInstance) => [e.domain, e.spaceId]) // 공간→인스턴스 1급 질의(공간 중심 존재론)
17
- @ObjectType({ description: 'Registry of a hosted twin runtime instance — anchors reboot recovery (board + kind).' })
18
+ @ObjectType({ description: 'Registry of a hosted twin runtime instance — anchors reboot recovery (model + kind).' })
18
19
  export class TwinInstance {
19
20
  @PrimaryGeneratedColumn('uuid')
20
21
  @Field(type => ID, { description: 'Unique identifier of the registry record.' })
@@ -31,34 +32,82 @@ export class TwinInstance {
31
32
  @Field({ description: 'Twin runtime instance id (unique within a domain).' })
32
33
  instanceId: string
33
34
 
35
+ /**
36
+ * **사람이 부르는 이름** — 업무키(`instanceId`)와 나란히 두는 표시명(프레임워크 엔티티 규약).
37
+ *
38
+ * ── 왜 업무키와 이름을 나눠 두나 ─────────────────────────────────────────────
39
+ * `instanceId` 는 URL·저널·구독이 전부 쓰는 **계약**이라 바꿀 수 없다. 반대로 이름은 바뀌고,
40
+ * 중복되고(사이트 둘에 "도장" 이 있는 것이 정상), 언어마다 다르다. 둘을 한 컬럼에 담으면 개명이
41
+ * 곧 다른 트윈이 되고 재동기가 끊긴다 — 그래서 나눈다(이 저장소에서 식별자가 밖에서 오는 것은
42
+ * 전부 이 규약을 쓴다: `spaceId`·`locationId`·`equipmentId`·`operationKey`).
43
+ *
44
+ * 나눴으면 **이름을 반드시 함께 둬야 한다.** 이 자리가 없던 동안 사이트 이름은 model 안에만 있어서
45
+ * 목록이 `crew-two` 만 보였다 — 나누기만 하고 절반을 빼먹으면 화면에는 id 만 남는다.
46
+ *
47
+ * 원본이 이름을 말하지 않으면 비운다(id 를 베끼지 않는다).
48
+ */
49
+ @Column({ nullable: true })
50
+ @Field({ nullable: true, description: 'Human name of this twin (the site name from its master). Empty when the source does not name it — never copied from instanceId.' })
51
+ name?: string
52
+
34
53
  @Column({ nullable: true })
35
- @Field({ nullable: true, description: 'Domain kernel kind: wms | yms | mes.' })
36
- kind?: string
54
+ @Field({ nullable: true, description: 'Longer description when the source provides one.' })
55
+ description?: string
56
+
57
+ /* 어느 도메인 커널이 이 트윈을 운영하는가. 커널 없는 트윈은 없으므로 비어 있을 수 없다
58
+ (`TwinEngine.start`·`register` 모두 필수 인자로 받는다). */
59
+ @Column()
60
+ @Field({ description: 'Domain kernel kind: wms | yms | mes.' })
61
+ kind: string
37
62
 
38
63
  /*
39
64
  * realityMode — 이 트윈의 현실이 어디서 오는가(runtime-state-model §0·§1 "지속 현실 하나 + 여러 렌즈"의 ① 선언).
40
65
  * 'mirror' = 현실은 외부 실물 시스템. 트윈=순수 렌즈, 재기동 시 실물서 재동기(startLive).
41
66
  * 'sim-world' = 트윈이 낳은 타임라인이 곧 진실이며 이어진다 → 재기동 시 저널서 resume(현실 지속).
42
67
  * 'sim-experiment' = seed 재현형. 재기동 = 같은 우주 재실행(저널 reset, revision 0) — 백지 시작이 버그가 아니라 선언된 거동.
43
- * 미선언(레거시)은 'sim-experiment'로 간주(가장 보수적 — 기존 데모 거동 보존).
68
+ *
69
+ * **비어 있을 수 없다.** 이 값은 재기동 거동을 구분하는 선언이고, 없으면 읽는 쪽마다 각자
70
+ * 기본값을 고르게 된다 — 같은 트윈이 부르는 자리에 따라 다르게 재기동하는 길이다.
71
+ * 선언을 생략한 프로비저닝은 `register` 가 `DEFAULT_REALITY_MODE` 로 **각인해서 저장한다**.
72
+ * 즉 "미선언" 이라는 상태 자체를 저장소에 두지 않는다.
44
73
  */
45
- @Column({ nullable: true })
46
- @Field({ nullable: true, description: "Reality source declaration: 'mirror' (external system is truth, resync on reboot) | 'sim-world' (generated timeline is truth and persists, resume on reboot) | 'sim-experiment' (seed-reproducible, resets on reboot). Absent ⇒ treated as sim-experiment." })
47
- realityMode?: string
74
+ @Column()
75
+ @Field({ description: "Reality source declaration: 'mirror' (external system is truth, resync on reboot) | 'sim-world' (generated timeline is truth and persists, resume on reboot) | 'sim-experiment' (seed-reproducible, resets on reboot)." })
76
+ realityMode: string
48
77
 
49
78
  /* 읽을 때 어휘 세대를 흡수한다(`nodes`/`movers`/`equipmentList` → `locations`/`equipment`). (vocabulary-guard: allow — 세대 이름 없이는 설명할 수 없다.)
50
79
  자리마다 리더를 쓰게 하면 다음에 추가되는 자리가 또 빠지고, 빠진 것은 오류가 아니라 **빈 공장**이라
51
- 조용하다(2026-08-06: 모델러 엔티티 목록이 통째로 비었다). 상세 `engine/board-vocabulary.ts`. */
52
- @Column({ type: 'simple-json', nullable: true, transformer: boardColumnTransformer })
53
- @Field(type => ScalarObject, { nullable: true, description: 'BoardDef blueprint (locations/equipment) required to replay events into state. Older stored boards used nodes/movers/equipmentList; they are normalised on read so consumers see one vocabulary.' }) // vocabulary-guard: allow — 옛 어휘를 흡수하는 것이 이 코드의 일이다(세대 이름을 적어야 흡수할 수 있다)
54
- board?: any
80
+ 조용하다(2026-08-06: 모델러 엔티티 목록이 통째로 비었다). 상세 `engine/model-vocabulary.ts`. */
81
+ @Column({ type: 'simple-json', nullable: true, transformer: modelColumnTransformer })
82
+ @Field(type => ScalarObject, { nullable: true, description: 'TwinModelDef blueprint (locations/equipment) required to replay events into state. Older stored boards used nodes/movers/equipmentList; they are normalised on read so consumers see one vocabulary.' }) // vocabulary-guard: allow — 옛 어휘를 흡수하는 것이 이 코드의 일이다(세대 이름을 적어야 흡수할 수 있다)
83
+ model?: any
84
+
85
+ /*
86
+ * 로컬 선언 겹 — **구조는 원천이, 수는 우리가.**
87
+ *
88
+ * 요금 단가·계약 정보·축전지 충전율처럼 원 시스템이 모르는 값을 넣는 자리다. `model` 에 직접 쓰지
89
+ * 않는 이유는 그것이 **원천의 사본**이기 때문이다: 다시 읽기가 사본을 새로 그리면 직접 쓴 값은 조용히
90
+ * 사라진다(요금이 사라지면 금액도 사라지고, 사용자는 이유를 알 수 없다).
91
+ *
92
+ * 그래서 여기 남기고, 모델을 그릴 때마다 그 위에 얹는다(`engine/local-declarations`). 얹힌 값에는
93
+ * `source: 'local'` 이 붙는다 — 커널 계약의 `PropertySource` 축(`reference` ↔ `local`)이고, 「밖에서
94
+ * 참조해 온 값이냐, 이 안에서 정한 값이냐」에 답한다.
95
+ *
96
+ * **이름에 장소 낱말을 쓰지 않는다**(옛 이름 `siteDeclarations`): 이 코드에서 `site` 는 **외부
97
+ * 시스템이 부르는 사업장**이고(SAP `P3`·Oracle `W01` 이 같은 현장), 여기 담기는 것은 그 외부 시스템이
98
+ * **모르는** 값이다 — 이름이 내용과 정반대였다. `space` 도 아니다: 그것은 「어디」에 답하고 이 축은
99
+ * 「누가 말했나」에 답한다.
100
+ */
101
+ @Column({ type: 'simple-json', nullable: true })
102
+ @Field(type => ScalarObject, { nullable: true, description: "Locally-declared values layered on top of the reference-derived model (tariff, contract, state of charge) — values whose authority sits here, not in the source system. Kept apart from `model` so a resync cannot silently drop them." })
103
+ localDeclarations?: any
55
104
 
56
105
  @Column({ nullable: true })
57
- @Field({ nullable: true, description: 'Owning space id (co-location anchor). Promoted from board.spaceId — first-class "space → its twins" query (space-centered ontology).' })
106
+ @Field({ nullable: true, description: 'Owning space id (co-location anchor). Promoted from model.spaceId — first-class "space → its twins" query (space-centered ontology).' })
58
107
  spaceId?: string
59
108
 
60
109
  @Column({ nullable: true })
61
- @Field({ nullable: true, description: 'Owning area id within the space (optional scope). Promoted from board.areaId.' })
110
+ @Field({ nullable: true, description: 'Owning area id within the space (optional scope). Promoted from model.areaId.' })
62
111
  areaId?: string
63
112
 
64
113
  @Column({ nullable: true })
@@ -68,19 +117,49 @@ export class TwinInstance {
68
117
  /*
69
118
  * purpose — 이 인스턴스가 운영 트윈인지, 격리된 벤치 사본인지 1급 선언(이름 접두사가 아니라 속성으로 구별).
70
119
  * 'bench' 는 규모/부하 실험용 격리 사본 — 목록·정리·안전 가드가 이 속성을 키로 동작(운영 트윈 무오염 보장).
71
- * 미선언(레거시)은 'operational' 로 간주.
120
+ *
121
+ * **비어 있을 수 없다.** 프로비저닝이 `register` 에서 'operational' 로 각인하고, 벤치 사본만
122
+ * `setPurpose` 로 뒤집는다. 예전에는 비워 둔 채 읽는 쪽에서 메웠는데, 그러면 "운영이라고 선언된 것"
123
+ * 과 "아무도 말하지 않은 것" 이 같은 값으로 보여서 벤치 격리가 추측에 기대게 된다.
72
124
  */
73
- @Column({ nullable: true })
74
- @Field({ nullable: true, description: "Instance purpose: 'operational' (real twin) | 'bench' (isolated copy for scale/load experiments). Absent ⇒ operational." })
75
- purpose?: string
125
+ @Column()
126
+ @Field({ description: "Instance purpose: 'operational' (real twin) | 'bench' (isolated copy for scale/load experiments)." })
127
+ purpose: string
76
128
 
77
129
  @Column({ nullable: true })
78
130
  @Field({ nullable: true, description: 'For a bench copy: the source spaceId it was copied from (provenance).' })
79
131
  copyOf?: string
80
132
 
81
- @Column({ type: 'int', nullable: true })
82
- @Field(type => Int, { nullable: true, description: 'Last persisted state revision observed for this instance.' })
83
- revision?: number
133
+ /**
134
+ * **이 트윈을 다시 읽는 방법** 어떤 원본에서 나왔나(`MasterOrigin`).
135
+ *
136
+ * 트윈은 원본을 비출 뿐이므로(ADR-0028) 원본이 달라지면 다시 읽어야 한다. 그런데 이 자리가 없던
137
+ * 동안 트윈은 **자기가 무엇에서 나왔는지 몰랐다** — 어댑터로 들어온 트윈은 `TwinReference` 행이
138
+ * 있었지만 템플릿으로 세운 트윈에는 아무 표식도 없었고, 그래서 첫날의 모습에 영원히 머무는
139
+ * 트윈이 만들어졌다. 원천이 무엇이든 여기 한 자리에 남긴다.
140
+ *
141
+ * **비어 있을 수 있다** — 이 자리가 생기기 전에 만들어진 트윈이 그렇다. 그때는 "다시 읽을 수
142
+ * 없다" 가 사실이므로 화면이 그대로 말한다(짐작해 채우지 않는다).
143
+ */
144
+ @Column({ type: 'simple-json', nullable: true })
145
+ @Field(type => ScalarObject, {
146
+ nullable: true,
147
+ description:
148
+ "How this twin's master can be produced again — {kind:'reference', source} or {kind:'template', templateId, params}. Absent means this twin cannot be re-read from its source."
149
+ })
150
+ origin?: any
151
+
152
+ /*
153
+ * `revision` 컬럼은 **없앴다**(2026-08-08).
154
+ *
155
+ * `ADR-0015` 가 "정의만 있고 write 안 됨 — 프로비저닝에서 최신 저널 revision 을 반영하도록
156
+ * 채운다" 고 적어 두었는데, 채우지 않는 편이 맞다. 저널의 최신 번호는 **저널이 아는 사실**이고
157
+ * (`TwinEngine.list` 가 `MAX(revision)` 으로 낸다), 여기 복제하면 **같은 사실의 집이 둘**이 된다.
158
+ * 이 세션에만 그 부류를 셋 겪었다(`model.operations` 두 집 · `materials` 두 집 · tenantId 중복).
159
+ *
160
+ * 복제한 값은 반드시 어긋난다 — 저널은 틱마다 늘고 이 컬럼은 저장할 때만 갱신되므로, 화면이
161
+ * 어느 쪽을 읽느냐로 다른 숫자가 나온다. 그 조용한 어긋남을 만들 이유가 없다.
162
+ */
84
163
 
85
164
  @CreateDateColumn()
86
165
  @Field({ nullable: true, description: 'Timestamp when the instance was first registered.' })
@@ -89,4 +168,26 @@ export class TwinInstance {
89
168
  @UpdateDateColumn()
90
169
  @Field({ nullable: true, description: 'Timestamp when the instance registry row was last updated.' })
91
170
  updatedAt?: Date
171
+
172
+ /*
173
+ * 누가 만들고 누가 고쳤나 — **이 행은 사람이 만든다**(프로비저닝은 사용자 행위)이므로 규약의
174
+ * 풀세트를 갖는다. 투영 행(자리·설비·공정)은 반대로 인제스트가 그린 것이라 저자가 사람이 아니고,
175
+ * 그 저자는 `sourceRef`+`ingestedAt` 이 이미 말한다 — 그래서 그쪽에는 두지 않는다.
176
+ *
177
+ * 비어 있을 수 있다: 부팅 자동 프로비저닝처럼 **사람이 없는 경로**로 만들어진 행이 그렇다.
178
+ * 그때 아무 사용자를 적어 넣으면 감사 기록이 거짓이 된다.
179
+ */
180
+ @ManyToOne(type => User, { nullable: true })
181
+ @Field(type => User, { nullable: true, description: 'The user who first provisioned this twin. Empty when it was created by a system path (boot provisioning).' })
182
+ creator?: User
183
+
184
+ @RelationId((e: TwinInstance) => e.creator)
185
+ creatorId?: string
186
+
187
+ @ManyToOne(type => User, { nullable: true })
188
+ @Field(type => User, { nullable: true, description: 'The user who last changed this twin registry row.' })
189
+ updater?: User
190
+
191
+ @RelationId((e: TwinInstance) => e.updater)
192
+ updaterId?: string
92
193
  }