@things-factory/headless-twin 10.0.9 → 10.0.11

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 (354) 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 +141 -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 +45 -13
  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 +138 -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 +59 -14
  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 +157 -0
  263. package/server/service/twin-model/twin-lineage-query.ts +193 -0
  264. package/server/service/twin-model/twin-location.ts +147 -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 +157 -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/column-type-portability.test.ts +122 -0
  286. package/test/command-routing.test.ts +1 -1
  287. package/test/declaration-reaches-model.test.ts +178 -0
  288. package/test/discovery-result.test.ts +1 -1
  289. package/test/energy-topology.test.ts +113 -0
  290. package/test/entity-delta.test.ts +171 -1
  291. package/test/event-time-column.test.ts +67 -0
  292. package/test/forecast-metrics.test.ts +71 -0
  293. package/test/forecast-tuning.test.ts +42 -0
  294. package/test/gap-analytics.test.ts +34 -7
  295. package/test/ingest-bench.test.ts +6 -6
  296. package/test/ingest-running-guard.test.ts +136 -0
  297. package/test/ingest-space.test.ts +29 -1
  298. package/test/instance-cache-lifecycle.test.ts +110 -0
  299. package/test/kernel-kind-guard.test.ts +96 -0
  300. package/test/kpi-baseline-db.test.ts +3 -1
  301. package/test/kpi-baseline.test.ts +3 -3
  302. package/test/kpi-fold.test.ts +393 -2
  303. package/test/kpi-query-bench.test.ts +5 -3
  304. package/test/lineage-survives-restart.test.ts +203 -0
  305. package/test/live-feed-registry.test.ts +67 -0
  306. package/test/live-kernel-facts.test.ts +161 -0
  307. package/test/live-mirror-parity.test.ts +22 -15
  308. package/test/load-meter.test.ts +314 -0
  309. package/test/local-declarations.test.ts +554 -0
  310. package/test/master-to-twin.test.ts +121 -33
  311. package/test/model-basis.test.ts +56 -1
  312. package/test/{board-vocabulary.test.ts → model-vocabulary.test.ts} +25 -25
  313. package/test/move-space.test.ts +149 -0
  314. package/test/mutation-gate.test.ts +19 -5
  315. package/test/oee-accumulator.test.ts +26 -7
  316. package/test/operation-basis.test.ts +100 -0
  317. package/test/operations-capability-db.test.ts +165 -0
  318. package/test/project-structure-db.test.ts +226 -0
  319. package/test/projection-reaches-screen.test.ts +272 -0
  320. package/test/property-effects.test.ts +96 -0
  321. package/test/resync-origin-site.test.ts +77 -0
  322. package/test/scale-twin-bench.test.ts +5 -5
  323. package/test/snapshot-freshness.test.ts +60 -0
  324. package/test/space-integrity.test.ts +223 -0
  325. package/test/standard-coverage.test.ts +94 -0
  326. package/test/state-axes.test.ts +1 -1
  327. package/test/streamline-e2e.test.ts +14 -14
  328. package/test/structure-revision-db.test.ts +11 -11
  329. package/test/tenant-registry-db.test.ts +5 -5
  330. package/test/twin-audit.test.ts +83 -0
  331. package/test/twin-model-item-db.test.ts +275 -0
  332. package/test/twin-model-tree-db.test.ts +176 -0
  333. package/test/twin-origin-resync.test.ts +114 -0
  334. package/test/warm-start-seam.test.ts +6 -6
  335. package/test/warm-start.test.ts +88 -3
  336. package/tsconfig.json +6 -1
  337. package/tsconfig.shared.json +23 -0
  338. package/tsconfig.shared.tsbuildinfo +1 -0
  339. package/tsconfig.tsbuildinfo +1 -0
  340. package/dist-server/engine/board-vocabulary.js.map +0 -1
  341. package/dist-server/engine/entity-delta.js.map +0 -1
  342. package/dist-server/engine/kpi-broadcast.js.map +0 -1
  343. package/dist-server/engine/twin-level.js.map +0 -1
  344. package/dist-server/service/twin-event/backfill-keys.d.ts +0 -11
  345. package/dist-server/service/twin-event/backfill-keys.js +0 -63
  346. package/dist-server/service/twin-event/backfill-keys.js.map +0 -1
  347. package/dist-server/tsconfig.tsbuildinfo +0 -1
  348. package/server/service/twin-event/backfill-keys.ts +0 -72
  349. /package/{dist-server/engine → dist-shared}/kpi-broadcast.d.ts +0 -0
  350. /package/{dist-server/engine → dist-shared}/kpi-broadcast.js +0 -0
  351. /package/{dist-server/engine → dist-shared}/twin-level.d.ts +0 -0
  352. /package/{dist-server/engine → dist-shared}/twin-level.js +0 -0
  353. /package/{server/engine → shared}/kpi-broadcast.ts +0 -0
  354. /package/{server/engine → shared}/twin-level.ts +0 -0
@@ -10,7 +10,7 @@
10
10
  * 적용하지 않고 **그 사실을 응답에 싣는다**(조용히 사라지면 사용자는 학습이 왜 안 듣는지 모른다).
11
11
  *
12
12
  * 비교는 **상대 허용오차**로 한다(양자화 지문이 아니라). 실측 p50 은 새 기록이 쌓일 때마다 1~2초씩
13
- * 흔들리는데, 값을 버킷으로 뭉개 비교하면 버킷 경계에 걸린 값이 1초 드리프트로도 달라져 버린다 —
13
+ * 흔들리는데, 값을 버킷으로 합쳐 비교하면 버킷 경계에 걸린 값이 1초 드리프트로도 달라져 버린다 —
14
14
  * "작은 드리프트로 학습을 무효화하지 않는다" 는 목적을 경계에서 못 지킨다. 비율로 비교하면 어디서든
15
15
  * 같은 규칙이 성립한다: 10% 이내는 같은 모델, 30초→47초(57%)는 다른 모델.
16
16
  *
@@ -18,7 +18,17 @@
18
18
  * ① 실측으로 배운 작업 종류별 소요 ② 자원 종류별 속도 ③ 선언된 공정 소요.
19
19
  */
20
20
 
21
- /** 같은 모델로 볼 상대 허용오차 — 이 비율 안의 변화는 드리프트, 넘으면 다른 모델. */
21
+ /**
22
+ * 허용오차의 **기본값** — 선언이 아무 말도 없을 때만 쓴다.
23
+ *
24
+ * 10% 는 근거에서 나온 수가 아니라 **판단값**이다(그 사실을 적어 두는 것이 이 주석의 목적이다). 그리고
25
+ * 단일 잣대로 쓰면 틀린다 — 우리 트레일러 템플릿이 그것을 보여 준다: 용접은 변동폭을 `PT55M~PT1H35M`
26
+ * (선언 소요 `PT1H10M` 기준 −21%~+36%)로 **스스로 선언한다.** 10% 관문은 그 공정의 **정상 변동조차**
27
+ * "다른 모델" 로 판정해 보정을 끊는다. 반대로 1% 변동도 치명적인 공정에는 10% 가 너무 느슨하다.
28
+ *
29
+ * 그래서 허용오차는 **그 공정이 선언한 변동폭에서 나온다**(§toleranceOf). 선언이 있으면 선언이 이기고,
30
+ * 없을 때만 이 값으로 떨어진다 — 도메인 설정을 새로 만들지 않는다(현장이 이미 말한 것을 읽는다).
31
+ */
22
32
  export const BASIS_TOLERANCE = 0.1
23
33
 
24
34
  export interface ModelBasis {
@@ -26,8 +36,51 @@ export interface ModelBasis {
26
36
  measured?: Record<string, number>
27
37
  /** 자원 종류 → 속도(m/s). `buildTravelEstimator().speedsByKind`. */
28
38
  speeds?: Record<string, number>
29
- /** 선언된 공정 소요 — key + ISO 8601 원문(문자열은 그대로 비교한다: 선언이 바뀌면 다른 모델). */
30
- declared?: { key: string; duration?: string }[]
39
+ /**
40
+ * 선언된 공정 — key + ISO 8601 원문(문자열은 그대로 비교한다: 선언이 바뀌면 다른 모델).
41
+ *
42
+ * **변동폭도 지문에 든다.** 변동폭이 허용오차를 정하므로(§toleranceOf), 그것이 바뀌었는데 "같은 모델"
43
+ * 이라고 하면 판정의 잣대가 조용히 바뀐 채로 학습이 이어진다.
44
+ */
45
+ declared?: DeclaredOperation[]
46
+ }
47
+
48
+ /** 선언된 공정 하나 — 소요와 변동폭(모델의 `operations` 원형에서 필요한 만큼만). */
49
+ export interface DeclaredOperation {
50
+ key: string
51
+ duration?: string
52
+ variability?: { distribution?: string; min?: string; max?: string; mode?: string }
53
+ }
54
+
55
+ /** ISO 8601 기간 → 밀리초. 읽을 수 없으면 `undefined`(짐작해 고치지 않는다). */
56
+ function isoMs(iso?: string): number | undefined {
57
+ if (!iso) return undefined
58
+ const m = /^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:([\d.]+)S)?)?$/.exec(iso)
59
+ if (!m || !m.slice(1).some(Boolean)) return undefined
60
+ return ((Number(m[1] ?? 0) * 24 + Number(m[2] ?? 0)) * 60 + Number(m[3] ?? 0)) * 60_000 + Number(m[4] ?? 0) * 1000
61
+ }
62
+
63
+ /**
64
+ * 작업 종류별 **허용오차** — 그 공정이 선언한 변동폭에서 나온다.
65
+ *
66
+ * 실측 p50 은 정상 변동 안에서 흔들린다. 그 흔들림을 "다른 모델" 로 읽으면 보정이 자꾸 끊기고, 사용자는
67
+ * 학습이 왜 안 듣는지 모른다. 얼마나 흔들리는 것이 정상인지는 **현장이 이미 선언했다**(`variability`).
68
+ *
69
+ * 반폭을 쓴다(`(max − min) / 2 / 소요`) — 중앙에서 어느 쪽으로든 그만큼은 정상이라는 뜻이다.
70
+ * 선언이 없거나 읽을 수 없으면 기본값으로 떨어진다(짐작해 만들지 않는다).
71
+ */
72
+ export function toleranceOf(declared?: DeclaredOperation[]): Record<string, number> {
73
+ const out: Record<string, number> = {}
74
+ for (const o of declared ?? []) {
75
+ if (!o?.key) continue
76
+ const base = isoMs(o.duration)
77
+ const min = isoMs(o.variability?.min)
78
+ const max = isoMs(o.variability?.max)
79
+ if (!base || min === undefined || max === undefined || !(max > min)) continue
80
+ const half = (max - min) / 2 / base
81
+ if (Number.isFinite(half) && half > 0) out[o.key] = half
82
+ }
83
+ return out
31
84
  }
32
85
 
33
86
  /** 뜻 있는 수만 남긴다 — 0·음수·NaN 은 모델이 아니다. */
@@ -37,21 +90,27 @@ function clean(map?: Record<string, number>): Record<string, number> {
37
90
  return out
38
91
  }
39
92
 
40
- function declaredMap(list?: { key: string; duration?: string }[]): Record<string, string> {
93
+ function declaredMap(list?: DeclaredOperation[]): Record<string, string> {
41
94
  const out: Record<string, string> = {}
42
- for (const o of list ?? []) if (o?.key && o.duration) out[o.key] = o.duration
95
+ for (const o of list ?? []) {
96
+ if (!o?.key) continue
97
+ const v = o.variability
98
+ /* 변동폭까지 한 문장으로 — 순서를 고정해 같은 선언이 같은 문자열이 되게 한다. */
99
+ const spread = v ? `~${v.distribution ?? '?'}:${v.min ?? ''}..${v.max ?? ''}@${v.mode ?? ''}` : ''
100
+ if (o.duration || spread) out[o.key] = `${o.duration ?? ''}${spread}`
101
+ }
43
102
  return out
44
103
  }
45
104
 
46
105
  /** 두 수치 지도가 실질적으로 다른가 — 키 집합이 다르거나, 값이 허용오차를 넘어 다르면 다르다. */
47
- function numericChanged(a: Record<string, number>, b: Record<string, number>, tol: number): boolean {
106
+ function numericChanged(a: Record<string, number>, b: Record<string, number>, tolOf: (key: string) => number): boolean {
48
107
  const keys = new Set([...Object.keys(a), ...Object.keys(b)])
49
108
  for (const k of keys) {
50
109
  const x = a[k]
51
110
  const y = b[k]
52
111
  if (x === undefined || y === undefined) return true // 새로 배운 종류·사라진 종류 = 다른 모델
53
112
  const base = Math.max(Math.abs(x), Math.abs(y))
54
- if (base > 0 && Math.abs(x - y) / base > tol) return true
113
+ if (base > 0 && Math.abs(x - y) / base > tolOf(k)) return true
55
114
  }
56
115
  return false
57
116
  }
@@ -62,8 +121,17 @@ function numericChanged(a: Record<string, number>, b: Record<string, number>, to
62
121
  */
63
122
  export function modelBasisChanged(before?: ModelBasis, after?: ModelBasis, tol: number = BASIS_TOLERANCE): boolean {
64
123
  if (!before || !after) return false
65
- if (numericChanged(clean(before.measured), clean(after.measured), tol)) return true
66
- if (numericChanged(clean(before.speeds), clean(after.speeds), tol)) return true
124
+ /*
125
+ * 허용오차는 **선언이 정한다** 배운 시점과 지금의 선언이 다를 수 있으므로 둘 중 **넓은 쪽**을 쓴다.
126
+ * 좁은 쪽을 쓰면 선언을 넓힌 직후에 정상 변동이 "다른 모델" 로 잡힌다(그 변경 자체는 아래 선언 비교가
127
+ * 이미 잡으므로, 여기서 또 잡을 필요가 없다).
128
+ */
129
+ const tolBefore = toleranceOf(before.declared)
130
+ const tolAfter = toleranceOf(after.declared)
131
+ const tolOf = (key: string): number => Math.max(tolBefore[key] ?? tol, tolAfter[key] ?? tol)
132
+ if (numericChanged(clean(before.measured), clean(after.measured), tolOf)) return true
133
+ /* 속도는 자원 종류별이고 선언된 변동폭이 없다 — 기본값이 그대로 쓰인다. */
134
+ if (numericChanged(clean(before.speeds), clean(after.speeds), () => tol)) return true
67
135
  const da = declaredMap(before.declared)
68
136
  const db = declaredMap(after.declared)
69
137
  const keys = new Set([...Object.keys(da), ...Object.keys(db)])
@@ -1,52 +1,73 @@
1
1
  /*
2
- * 저장된 보드의 **어휘 세대를 흡수한다** — 읽는 순간 한 어휘로. 순수.
2
+ * 저장된 트윈 모델의 **어휘 세대를 흡수한다** — 읽는 순간 한 어휘로. 순수.
3
3
  *
4
4
  * ── 계기 (2026-08-06, 사용자 신고) ───────────────────────────────────────────
5
- * 트윈 모델러의 엔티티 목록이 **통째로 비어 있었다.** 오류도 없었다. 원인은 이 인스턴스의 저장된 보드가
5
+ * 트윈 모델러의 엔티티 목록이 **통째로 비어 있었다.** 오류도 없었다. 원인은 이 인스턴스의 저장된 모델가
6
6
  * 옛 어휘였고 화면과 호스트는 새 어휘(`locations`/`equipment`)를 읽었기 때문이다. 그 옛 이름들은
7
7
  * 아래 `RETIRED_KEYS` 에 있다 — 흡수할 세대의 이름을 적지 않고는 이 파일을 쓸 수 없다.
8
8
  *
9
9
  * 커널에는 이미 세대를 흡수하는 리더가 있다(`readBoardLocations`·`readBoardEquipment` — 세 세대
10
- * `movers`→`equipmentList`→`equipment`, 실측 주석에 "저장된 보드 23개 중 13개가 `movers`"). 그런데 **그
10
+ * `movers`→`equipmentList`→`equipment`, 실측 주석에 "저장된 모델 23개 중 13개가 `movers`"). 그런데 **그
11
11
  * 리더를 거치는 곳이 목록뿐**이었다. 상세·구조 서명·주행 추정기·설비 동기는 컬렉션을 직접 읽어
12
12
  * **오류 없이 빈 공장**으로 읽었다:
13
13
  *
14
14
  * · 모델러·팔레트·보드 자동생성 → 엔티티 0(사용자가 본 증상)
15
15
  * · 구조 서명 → `N[]M[]` — **서로 다른 두 공장이 구조적으로 같아 보인다**(변경 감지가 죽는다)
16
16
  * · 주행 추정기 → 설비 0이라 거리 기반 소요시간을 못 만든다
17
- * · 설비 동기 → 아는 설비가 0이라 중복 append + 한 보드에 두 세대가 섞인다
17
+ * · 설비 동기 → 아는 설비가 0이라 중복 append + 한 모델에 두 세대가 섞인다
18
18
  *
19
19
  * ── 왜 자리마다 고치지 않나 ─────────────────────────────────────────────────
20
- * 저장된 보드를 읽는 자리가 **27곳**이다. 자리마다 리더를 쓰게 하면 다음에 추가되는 자리가 또 빠진다 —
20
+ * 저장된 모델를 읽는 자리가 **27곳**이다. 자리마다 리더를 쓰게 하면 다음에 추가되는 자리가 또 빠진다 —
21
21
  * 그리고 빠진 것은 오류가 아니라 **빈 공장**이라 조용하다. 그래서 **DB에서 읽는 순간 한 번** 정규화한다
22
22
  * (엔티티 컬럼 변환기). 그 뒤로는 코드가 세대를 알 필요가 없다.
23
23
  *
24
24
  * ── 쓰기는 건드리지 않는다 ──────────────────────────────────────────────────
25
- * 저장은 주는 대로 한다(새 쓰기는 이미 새 어휘다). 읽기가 정규화되므로, 저장된 옛 보드는 **다음에
26
- * 저장될 때 자연히 새 어휘로 옮겨간다** — 마이그레이션을 따로 돌리지 않는다.
25
+ * 저장은 주는 대로 한다(새 쓰기는 이미 새 어휘다). 읽기가 정규화되므로, 저장된 옛 모델는 **다음에
26
+ * 저장될 때 자연히 새 어휘로 옮겨간다** — 마이그레이션을 따로 실행하지 않는다.
27
27
  */
28
28
  import { readBoardLocations, readBoardEquipment } from '@operato/twin-kernel'
29
29
 
30
30
  /** 흡수하고 **내보내지 않는** 옛 키 — 남겨 두면 소비처가 두 어휘를 다시 보게 된다. */
31
31
  const RETIRED_KEYS = ['nodes', 'movers', 'equipmentList'] as const // vocabulary-guard: allow — 옛 어휘를 흡수하는 것이 이 코드의 일이다(세대 이름을 적어야 흡수할 수 있다)
32
32
 
33
+ /** 등급 축들 — 넷이 같은 모양이라 같은 규칙을 쓴다(어느 자원이든 표시명은 `name`). */
34
+ const CLASS_KEYS = ['personnelClasses', 'equipmentClasses', 'assetClasses', 'materialClasses'] as const
35
+
33
36
  /**
34
- * 보드를 정본 어휘로 — `locations`·`equipment` 만 남는다. 새 어휘 보드에는 **아무 일도 하지 않는다**
37
+ * 보드를 정본 어휘로 — `locations`·`equipment` 만 남는다. 새 어휘 모델에는 **아무 일도 하지 않는다**
35
38
  * (같은 값을 다시 담을 뿐이라 멱등).
36
39
  *
37
- * 보드가 아닌 것(없음·배열·원시값)은 **그대로 통과**시킨다 — 모양을 짐작해 고치지 않는다.
40
+ * 모델이 아닌 것(없음·배열·원시값)은 **그대로 통과**시킨다 — 모양을 짐작해 고치지 않는다.
38
41
  */
39
- export function canonicalBoard<T = any>(board: T): T {
40
- if (!board || typeof board !== 'object' || Array.isArray(board)) return board
42
+ export function canonicalModel<T = any>(model: T): T {
43
+ if (!model || typeof model !== 'object' || Array.isArray(model)) return model
41
44
 
42
45
  const rest: Record<string, unknown> = {}
43
- for (const [k, v] of Object.entries(board as Record<string, unknown>)) {
46
+ for (const [k, v] of Object.entries(model as Record<string, unknown>)) {
44
47
  if ((RETIRED_KEYS as readonly string[]).includes(k)) continue
45
48
  rest[k] = v
46
49
  }
47
50
 
51
+ /*
52
+ * **등급의 표시명은 `name` 이다** — 옛 모델는 `description` 에 담았다.
53
+ *
54
+ * 표준 마스터가 등급 표시명을 `description` 으로 주기 때문에 그대로 저장돼 있었고, 화면은 그것을
55
+ * "설명" 열에 앉혔다 — 사용자에게는 이름인데 머리글은 설명이라, 같은 것을 축마다 다른 이름으로
56
+ * 부르게 된다. 인제스트 문에서 이미 `name` 으로 옮기고 있지만, **이미 저장된 모델**가 그대로 남아
57
+ * 새 트윈만 고쳐진다. 정규화의 집은 여기 하나다(위 머리말의 이유 그대로).
58
+ */
59
+ for (const key of CLASS_KEYS) {
60
+ const list = rest[key]
61
+ if (!Array.isArray(list)) continue
62
+ rest[key] = list.map((c: any) =>
63
+ c && typeof c === 'object' && !c.name && c.description
64
+ ? (({ description, ...keep }) => ({ ...keep, name: description }))(c)
65
+ : c
66
+ )
67
+ }
68
+
48
69
  /* 리더가 세대를 고른다 — 여기서 `??` 사슬을 다시 적으면 세대 규칙이 두 곳이 된다. */
49
- return { ...rest, locations: readBoardLocations(board as any), equipment: readBoardEquipment(board as any) } as T
70
+ return { ...rest, locations: readBoardLocations(model as any), equipment: readBoardEquipment(model as any) } as T
50
71
  }
51
72
 
52
73
  /**
@@ -55,7 +76,7 @@ export function canonicalBoard<T = any>(board: T): T {
55
76
  * `to`(쓰기)는 손대지 않는다: 저장은 호출부가 준 그대로가 진실이고, 읽기가 정규화되므로 다음 저장에서
56
77
  * 자연히 새 어휘가 된다.
57
78
  */
58
- export const boardColumnTransformer = {
59
- from: (value: any) => canonicalBoard(value),
79
+ export const modelColumnTransformer = {
80
+ from: (value: any) => canonicalModel(value),
60
81
  to: (value: any) => value
61
82
  }
@@ -23,6 +23,12 @@ interface Acc {
23
23
  scrapCount: number
24
24
  }
25
25
 
26
+ /** 누적기의 완전한 재개점 — 저장했다 되살릴 수 있어야 꼬리만 접을 수 있다. */
27
+ export interface OeeCheckpoint {
28
+ maxMs: number
29
+ acc: ({ moverId: string } & Acc)[]
30
+ }
31
+
26
32
  export class OeeAccumulator {
27
33
  private acc = new Map<string, Acc>()
28
34
  private maxMs = 0 // 최신 이벤트 시각 — nowMs 기본값(현재 계측 시점)
@@ -36,6 +42,27 @@ export class OeeAccumulator {
36
42
  return a
37
43
  }
38
44
 
45
+ /**
46
+ * **재개점** — 이 누적기의 상태 전부(이어 접기의 씨앗).
47
+ *
48
+ * ── 왜 필요한가 (2026-08-18) ────────────────────────────────────────────────
49
+ * 가동 이력은 원 시스템이 누적을 주지 않아 **우리가 이벤트를 적분해** 만든다. 그래서 저널을 꼬리만
50
+ * 접으면 이 누적기는 꼬리분만 갖게 되고, 화면의 가용률·수율이 조용히 작아진다(오류는 나지 않는다).
51
+ * 재개점을 함께 저장·복원해야 「이어 접은 값」이 「0부터 접은 값」과 같아진다.
52
+ */
53
+ serialize(): OeeCheckpoint {
54
+ return {
55
+ maxMs: this.maxMs,
56
+ acc: [...this.acc.entries()].map(([moverId, a]) => ({ moverId, ...a }))
57
+ }
58
+ }
59
+
60
+ /** 재개점에서 되세운다 — 모르는 것은 지어내지 않는다(없으면 빈 누적기다). */
61
+ restore(cp: OeeCheckpoint | undefined | null): void {
62
+ this.maxMs = cp?.maxMs ?? 0
63
+ this.acc = new Map((cp?.acc ?? []).map(({ moverId, ...a }) => [moverId, { ...a }]))
64
+ }
65
+
39
66
  /** equipment.status(시간-in-상태) + quality.output(양품/불량) 소비. 다른 eventType 무시. */
40
67
  apply(env: CanonicalEnvelope): void {
41
68
  const t = Date.parse(env.eventTime)
@@ -62,7 +89,13 @@ export class OeeAccumulator {
62
89
  /** 무버 OEE 카운터 — 마지막 상태 구간을 nowMs(기본=최신 이벤트 시각)까지 flush. busy→run 근사, setup=0. */
63
90
  counters(moverId: string, nowMs: number = this.maxMs): OeeCounters {
64
91
  const a = this.acc.get(moverId)
65
- if (!a) return { runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, scrapCount: 0, metricsSinceMs: 0 }
92
+ /*
93
+ * **이 설비의 이벤트를 하나도 못 봤다** — 계측 창이 없다는 뜻이고, 그것을 `0` 으로 메우면
94
+ * "1970년부터 재고 있었다" 가 된다. 시각이 밀리초 epoch 이므로 그 값이 곧 지금 시각이 되어,
95
+ * 화면에 `idleMs 1767226603000`(약 56,000년) 유휴가 섰다. 창을 **말하지 않는다** — 공식이
96
+ * 없음을 없음으로 다룬다(§computeOee).
97
+ */
98
+ if (!a) return { runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, scrapCount: 0 }
66
99
  const dt = Math.max(0, nowMs - a.lastMs)
67
100
  let busyMs = a.busyMs
68
101
  let downMs = a.downMs
@@ -0,0 +1,100 @@
1
+ /*
2
+ * 이 트윈이 **어떤 공정을 돌리고, 그 시간이 어디서 오나.**
3
+ *
4
+ * ── 왜 이 목록이 필요한가 (2026-08-18) ─────────────────────────────────────
5
+ * 「측정이 없을 때의 표준 시간」을 현장이 선언하려면 먼저 **무엇에 선언할지** 보여야 한다. 그런데 공정은
6
+ * 자리·설비와 달리 모델에 행이 없을 수 있다 — 실측: 운영 중인 트윈 35개 중 창고·야드는 공정 명세 행이
7
+ * 0개였고(MES만 4개), 그 트윈들의 시간은 전부 커널 상수에서 왔다.
8
+ *
9
+ * 그래서 목록을 **지어내지 않고 네 곳에서 모은다**:
10
+ * ① 원천 명세 행(`model.operations`) — 원 시스템이 말한 공정.
11
+ * ② 커널의 자기보고(`specCoverage()`) — **실제로 돌린** 공정과 그 시간의 출처.
12
+ * ③ 이력에서 배운 종류(`measuredOperationKinds`) — 재기동에도 남는 근거.
13
+ * ④ 우리가 이미 선언한 것(`model.localDurations`) — 선언해 둔 공정이 목록에서 사라지지 않게.
14
+ *
15
+ * ③이 필요한 이유는 실측으로 드러났다: ②는 **작업이 새로 생길 때** 채워지므로, 이력이 풍부한 창고
16
+ * 트윈도 재기동 직후에는 목록이 비었다 — 값을 채울 곳을 보여야 하는 화면이 정작 그때 아무 말도 못 한다.
17
+ *
18
+ * 그래도 셋 다 비는 트윈은 있다(한 번도 돌지 않고 명세 행도 없는 새 트윈). 그때 「공정이 없다」고 말하지
19
+ * 않는다(모르는 것이다) — 부르는 쪽이 그 사실을 그대로 화면에 옮긴다.
20
+ */
21
+ import { readSpecCoverage } from './spec-coverage.ts'
22
+
23
+ /** 공정 한 종류 — 이름과 「지금 무엇으로 도나」. */
24
+ export interface OperationBasisRow {
25
+ /** 작업 종류(`FlowTask.kind` = `OperationDef.key`) — 선언이 겨누는 id 다. */
26
+ kind: string
27
+ /** 사람이 읽는 이름 — 원천 명세 행이 있을 때만(없으면 종류 이름이 곧 이름이다). */
28
+ label?: string
29
+ /**
30
+ * 지금 이 종류의 시간이 어디서 오나 — 커널의 자기보고, 또는 **이력에서 배운 사실**. 없으면 모른다.
31
+ * · `measured` — 이 현장의 이력에서 배운 값(가장 강한 근거). **선언보다 이긴다.**
32
+ * · `declared` — 선언된 값(현장이 정한 것 또는 원천 명세 행).
33
+ * · `default` — 커널 소스의 상수. 이 목록의 존재 이유다.
34
+ */
35
+ basis?: 'measured' | 'declared' | 'default'
36
+ /** 원천 명세 행이 말한 시간(ISO 8601) — 있으면. 우리 선언이 이것을 이긴다. */
37
+ referenceDuration?: string
38
+ /** 우리가 선언해 둔 시간(ISO 8601) — 있으면. */
39
+ localDuration?: string
40
+ }
41
+
42
+ /**
43
+ * 공정 목록을 모은다 — **넷의 합집합**, 순서는 원천 행 → 돌린 것 → 이력에서 배운 것 → 선언한 것.
44
+ *
45
+ * `kernel` 이 없으면(정지 트윈) ②가 빠지고, `measuredKinds` 를 주지 않으면 ③이 빠진다: 그 사실은
46
+ * `basis` 가 비는 것으로 드러난다(빈칸이 곧 「모른다」다).
47
+ */
48
+ export function operationBasis(model: any, kernel?: any, measuredKinds?: readonly string[]): OperationBasisRow[] {
49
+ const rows = new Map<string, OperationBasisRow>()
50
+ const at = (kind: string): OperationBasisRow => {
51
+ const cur = rows.get(kind)
52
+ if (cur) return cur
53
+ const row: OperationBasisRow = { kind }
54
+ rows.set(kind, row)
55
+ return row
56
+ }
57
+
58
+ for (const o of (model?.operations ?? []) as any[]) {
59
+ if (!o?.key) continue
60
+ const row = at(String(o.key))
61
+ if (o.label) row.label = String(o.label)
62
+ if (o.duration) row.referenceDuration = String(o.duration)
63
+ }
64
+
65
+ const coverage = kernel ? readSpecCoverage(kernel) : undefined
66
+ for (const o of coverage?.operations ?? []) {
67
+ if (!o?.kind) continue
68
+ at(String(o.kind)).basis = o.duration
69
+ }
70
+
71
+ /*
72
+ * 이력에서 배운 종류 — 커널이 아직 이 종류를 돌리지 않았어도 **추정기가 답할 수 있다**. 그래서 근거를
73
+ * `measured` 로 채운다: 짐작이 아니라 설치된 추정기가 그 종류에 실측값을 줄 것이라는 사실이다.
74
+ * 커널이 이미 보고한 종류는 **덮지 않는다** — 실제로 돌린 것이 더 강한 사실이다.
75
+ */
76
+ for (const kind of measuredKinds ?? []) {
77
+ if (!kind) continue
78
+ const row = at(String(kind))
79
+ if (!row.basis) row.basis = 'measured'
80
+ }
81
+
82
+ const declared = (model?.localDurations ?? {}) as Record<string, string>
83
+ for (const [kind, iso] of Object.entries(declared)) {
84
+ if (!kind) continue
85
+ at(kind).localDuration = String(iso)
86
+ }
87
+
88
+ return [...rows.values()]
89
+ }
90
+
91
+ /**
92
+ * 선언을 받아도 되는 공정 이름들 — **하나도 확인할 수 없으면 `undefined`**(모른다).
93
+ *
94
+ * 빈 배열로 답하지 않는 이유: 빈 배열은 「어떤 이름도 안 된다」로 읽히고, 그러면 아직 돌지 않은 트윈에는
95
+ * 아무것도 선언할 수 없다. 모르는 것과 없는 것은 다르다 — 모를 때는 받아 주고 그 사실을 말한다.
96
+ */
97
+ export function knownOperationKinds(model: any, kernel?: any, measuredKinds?: readonly string[]): string[] | undefined {
98
+ const kinds = operationBasis(model, kernel, measuredKinds).map(r => r.kind)
99
+ return kinds.length ? kinds : undefined
100
+ }
@@ -0,0 +1,199 @@
1
+ /*
2
+ * 자원 속성의 **효과 등록부** — 이 속성이 무엇을 바꾸나.
3
+ *
4
+ * ── 왜 있나 ─────────────────────────────────────────────────────────────────
5
+ * 속성은 **열려 있다**: 템플릿·마스터가 아무 id 로나 사실을 실어 보낼 수 있고, 커널은 그것을 그대로
6
+ * 나르고 화면은 단위와 함께 보인다(표준이 속성 어휘를 정하지 않으므로 그것이 옳다).
7
+ *
8
+ * 그런데 **효과는 닫혀 있다**: 값이 계산·배정·판정을 바꾸려면 그 규칙을 코드가 알아야 한다. 지금
9
+ * 효과가 있는 것은 둘뿐이다. 문제는 그 사실이 **어디에도 드러나지 않는다**는 것이었다 — 사용자는
10
+ * 속성을 넣으면 무언가 달라질 것으로 기대하는데, 실제로는 표에 한 줄이 늘 뿐이다.
11
+ *
12
+ * 그래서 이 등록부는 **지어내지 않고 드러낸다**: 읽는 코드가 있는 것만 적고, 없는 것은 "표시만 된다"
13
+ * 고 화면이 말한다. 새 효과를 코드로 붙이는 사람은 **여기 한 줄을 더한다** — 그러지 않으면 사용자는
14
+ * 그 효과가 있는지 알 수 없다.
15
+ *
16
+ * ⚠️ 이 파일은 등록부이고 판정이 아니다. 값을 실제로 읽는 곳은 각자다(아래 `readBy` 가 그 자리를
17
+ * 가리킨다) — 여기에 계산을 옮겨 오면 규칙이 두 벌이 된다.
18
+ */
19
+
20
+ /** 효과가 있는 속성 하나. */
21
+ export interface PropertyEffect {
22
+ /** 속성 id — 선언에 적히는 그 이름(`ResourceProperty.id`). */
23
+ id: string
24
+ /** 어느 축의 속성인가 — 그 축의 항목에만 이 효과를 말한다. */
25
+ axes: string[]
26
+ /** 무엇을 바꾸나 — 화면이 읽는 i18n 키(언어중립: 서버는 사람 말을 만들지 않는다). */
27
+ effect: string
28
+ /** 실제로 읽는 코드 — 지어낸 효과를 등록하지 않기 위한 표식이다(없는 자리를 가리키면 리뷰가 잡는다). */
29
+ readBy: string
30
+ }
31
+
32
+ export const PROPERTY_EFFECTS: PropertyEffect[] = [
33
+ {
34
+ id: 'speed',
35
+ axes: ['equipment'],
36
+ effect: 'twin.propeffect.speed',
37
+ readBy: 'headless-twin/server/engine/travel-estimator.ts (buildTravelEstimator)'
38
+ },
39
+ {
40
+ id: 'perBaseUnit',
41
+ axes: ['materialDefinitions'],
42
+ effect: 'twin.propeffect.perBaseUnit',
43
+ readBy: '@operato/twin-kernel conversionFactorOf'
44
+ },
45
+ /*
46
+ * ── 에너지 속성 (2026-08-18) ──────────────────────────────────────────────
47
+ *
48
+ * 이 값들은 **오래전부터 읽히고 있었다** — 커널이 부하·피크·배터리를 그것으로 계산하고, 호스트가
49
+ * 금액을 그것으로 만든다. 그런데 등록부에 없어서 화면은 그 값들에 대해 「표시만 됩니다」라고 말했다.
50
+ * 조용한 거짓말이다: 사용자는 계약전력을 넣고도 「이 값은 아무것도 바꾸지 않는다」는 문장을 읽었다.
51
+ *
52
+ * 효과를 새로 만든 것이 아니라 **있는 효과를 드러낸다.** `readBy` 는 실제로 읽는 자리를 가리킨다.
53
+ */
54
+ {
55
+ id: 'contract.kW',
56
+ axes: ['locations'],
57
+ effect: 'twin.propeffect.contractKW',
58
+ readBy: '@operato/twin-kernel EmsKernel.declaredContractKW (계약 대비 판정·초과 주의)'
59
+ },
60
+ {
61
+ id: 'power.ratedKW',
62
+ axes: ['equipment'],
63
+ effect: 'twin.propeffect.ratedKW',
64
+ readBy: '@operato/twin-kernel EmsKernel.deriveEquipmentLoad (가동 중 부하)'
65
+ },
66
+ {
67
+ id: 'power.standbyKW',
68
+ axes: ['equipment'],
69
+ effect: 'twin.propeffect.standbyKW',
70
+ readBy: '@operato/twin-kernel EmsKernel.deriveEquipmentLoad (대기 중 부하)'
71
+ },
72
+ {
73
+ id: 'storage.capacityKWh',
74
+ axes: ['equipment'],
75
+ effect: 'twin.propeffect.capacityKWh',
76
+ readBy: '@operato/twin-kernel EmsKernel (축전지 방전 가능량)'
77
+ },
78
+ {
79
+ id: 'dispatch.dischargeAboveKW',
80
+ axes: ['equipment'],
81
+ effect: 'twin.propeffect.dischargeAboveKW',
82
+ readBy: '@operato/twin-kernel EmsKernel (방전 시작 문턱)'
83
+ },
84
+ {
85
+ id: 'dispatch.maxDischargeKW',
86
+ axes: ['equipment'],
87
+ effect: 'twin.propeffect.maxDischargeKW',
88
+ readBy: '@operato/twin-kernel EmsKernel (인버터 방전 상한)'
89
+ },
90
+ {
91
+ id: 'dispatch.reserveSoc',
92
+ axes: ['equipment'],
93
+ effect: 'twin.propeffect.reserveSoc',
94
+ readBy: '@operato/twin-kernel EmsKernel (이 아래로는 방전하지 않는다)'
95
+ },
96
+ {
97
+ id: 'storage.initialSoc',
98
+ axes: ['equipment'],
99
+ effect: 'twin.propeffect.initialSoc',
100
+ readBy: '@operato/twin-kernel EmsKernel (시작 충전율 — 예측·what-if 의 씨앗)'
101
+ },
102
+ {
103
+ id: 'generation.ratedKW',
104
+ axes: ['equipment'],
105
+ effect: 'twin.propeffect.genRatedKW',
106
+ readBy: '@operato/twin-kernel EmsKernel.deriveGeneration (정격 발전)'
107
+ },
108
+ {
109
+ id: 'generation.dailyProfile',
110
+ axes: ['equipment'],
111
+ effect: 'twin.propeffect.genDailyProfile',
112
+ readBy: '@operato/twin-kernel generationFractionAt (시각별 발전 비율)'
113
+ },
114
+ /* 요금은 커널이 읽지 않는다 — 금액은 저널을 접는 호스트가 만든다. readBy 가 그 사실을 말한다. */
115
+ {
116
+ id: 'tariff.demandChargePerKW',
117
+ axes: ['locations'],
118
+ effect: 'twin.propeffect.demandChargePerKW',
119
+ readBy: 'headless-twin/server/engine/kpi-query.ts (수요요금 계산)'
120
+ },
121
+ {
122
+ id: 'tariff.energyChargePerKWh',
123
+ axes: ['locations'],
124
+ effect: 'twin.propeffect.energyChargePerKWh',
125
+ readBy: 'headless-twin/server/engine/kpi-query.ts (전력량 요금 계산)'
126
+ },
127
+ /* 판정 기준 — 값이 경보의 유무를 바꾼다(가장 눈에 보이는 효과다). */
128
+ {
129
+ id: 'attention.congestionRatio',
130
+ axes: ['locations'],
131
+ effect: 'twin.propeffect.congestionRatio',
132
+ readBy: '@operato/twin-kernel deriveAttentions (병목 판정 기준)'
133
+ },
134
+ {
135
+ id: 'attention.scrapRate',
136
+ axes: ['equipment'],
137
+ effect: 'twin.propeffect.scrapRate',
138
+ readBy: '@operato/twin-kernel deriveAttentions (불량 과다 판정 기준)'
139
+ },
140
+ {
141
+ id: 'attention.scrapMinSamples',
142
+ axes: ['equipment'],
143
+ effect: 'twin.propeffect.scrapMinSamples',
144
+ readBy: '@operato/twin-kernel deriveAttentions (불량률을 재기 시작할 표본 수)'
145
+ },
146
+ /*
147
+ * ── 공정 소요시간 (2026-08-18) ────────────────────────────────────────────
148
+ *
149
+ * 실측: 운영 중인 트윈 35개 중 **창고·야드는 공정 명세 행이 0개**였다 — 그 트윈들의 시간은 전부 커널
150
+ * 상수에서 왔고, 예측은 「같은 조건에서의 상대 비교」에 머물렀다(절대 시각 주장 불가). 이 선언이
151
+ * 그 사다리를 한 칸 올린다: 실측이 없어도 **현장이 정한 시간**으로 굴러간다.
152
+ *
153
+ * 실측(이력에서 배운 값)은 여전히 이 선언을 이긴다 — 화면이 그 사실을 함께 말한다.
154
+ */
155
+ {
156
+ id: 'operation.duration',
157
+ axes: ['operations'],
158
+ effect: 'twin.propeffect.operationDuration',
159
+ readBy: '@operato/twin-kernel FlowEngine.durationOf ← declareDurations (engine/local-declarations.ts 가 실어 보낸다)'
160
+ },
161
+ {
162
+ id: 'tariff.currency',
163
+ axes: ['locations'],
164
+ effect: 'twin.propeffect.currency',
165
+ readBy: 'headless-twin/server/engine/kpi-query.ts (금액 표기 통화)'
166
+ }
167
+ ]
168
+
169
+ /**
170
+ * 이 축의 속성 효과 — `속성 id → i18n 키`.
171
+ *
172
+ * 없는 속성은 **여기 없다**(빈 값으로 채우지 않는다) — 화면이 "등록된 효과가 없다" 를 스스로 말한다.
173
+ */
174
+ export function propertyEffectsOf(axis: string): Record<string, string> {
175
+ const out: Record<string, string> = {}
176
+ for (const e of PROPERTY_EFFECTS) if (e.axes.includes(axis)) out[e.id] = e.effect
177
+ return out
178
+ }
179
+
180
+ /** 등록부의 축 이름 → 선언이 겨누는 대상 이름. 대상이 아닌 축(자재 정의 등)은 여기 없다. */
181
+ const AXIS_TO_TARGET: Record<string, string> = { locations: 'location', equipment: 'equipment', operations: 'operation' }
182
+
183
+ /**
184
+ * 이 속성을 **어느 대상에 얹을 수 있나** — 등록부의 축이 그대로 답한다.
185
+ *
186
+ * ── 왜 필요한가 (2026-08-18) ────────────────────────────────────────────────
187
+ * 지게차에 계약전력을 선언할 수 있었다. 문은 받아 주고 화면은 「넣었습니다」라고 말하는데, 그 값을 읽는
188
+ * 코드는 자리(수전)만 본다 — 아무 일도 일어나지 않고 그 사실이 어디에도 나오지 않았다. 효과가 어느 축의
189
+ * 것인지는 이미 등록부가 알고 있으므로, 그 지식을 검사에 쓴다(두 곳에 적지 않는다).
190
+ *
191
+ * 등록부에 없는 속성은 **비어 있는 목록이 아니라 `undefined`** 다: 「어디에도 못 얹는다」와 「우리가
192
+ * 제한을 모른다」는 다르다(후자는 받아 준다 — 속성은 열려 있다는 규약을 지킨다).
193
+ */
194
+ export function declarationTargetsOf(propertyId: string): string[] | undefined {
195
+ const e = PROPERTY_EFFECTS.find(x => x.id === propertyId)
196
+ if (!e) return undefined
197
+ const targets = e.axes.map(a => AXIS_TO_TARGET[a]).filter((t): t is string => !!t)
198
+ return targets.length ? targets : undefined
199
+ }
@@ -52,7 +52,7 @@ export function parseRuntimeKey(key: string): { domainId: string; instanceId: st
52
52
  return { domainId: key.slice(0, at), instanceId: key.slice(at + 1) }
53
53
  }
54
54
 
55
- /** 이 키가 이 테넌트의 것인가 — 레지스트리를 훑을 때 도메인으로 걸러낸다. */
55
+ /** 이 키가 이 테넌트의 것인가 — 레지스트리를 조회할 때 도메인으로 걸러낸다. */
56
56
  export function isOfDomain(key: string, domainId: string): boolean {
57
57
  return !!domainId && key.startsWith(`${domainId}${RUNTIME_KEY_SEP}`)
58
58
  }
@@ -2,9 +2,9 @@
2
2
  * 시뮬 명세 자기보고 — **"이 예측으로 무엇을 말할 수 있나" 를 예측과 함께 내보낸다.**
3
3
  *
4
4
  * 배경(operato-twin design/plans/simulation-spec.md): 공정 소요·수율이 커널 소스의 상수였다. 이제
5
- * 마스터가 명세를 실어 오면 커널이 그것으로 굴리고, `specCoverage()` 가 무엇을 선언값으로 무엇을
6
- * 기본값으로 썼는지 보고한다. 그 보고를 **예측 응답에 붙이지 않으면** 받는 쪽은 상수로 굴린 숫자와
7
- * 현장 명세로 굴린 숫자를 구별할 수 없다 — 똑같이 "p50 168" 로 보인다.
5
+ * 마스터가 명세를 실어 오면 커널이 그것으로 실행하고, `specCoverage()` 가 무엇을 선언값으로 무엇을
6
+ * 기본값으로 썼는지 보고한다. 그 보고를 **예측 응답에 붙이지 않으면** 받는 쪽은 상수로 계산한 숫자와
7
+ * 현장 명세로 계산한 숫자를 구별할 수 없다 — 똑같이 "p50 168" 로 보인다.
8
8
  *
9
9
  * 언어 중립: 여기서는 **코드와 수치만** 낸다(`qualification` + 카운트). 사람이 읽는 문장은 표현
10
10
  * 계층(클라 i18next / AI 답변)이 만든다 — attention 과 같은 규율.
@@ -18,12 +18,17 @@
18
18
  * 채로 뜨는 것이 가장 나쁘다.
19
19
  */
20
20
 
21
- /** 상태가 들고 있는 축 — 이 목록 밖의 이름은 고를 수 없다. */
22
- export const STATE_AXES = ['locations', 'items', 'equipment', 'persons', 'assets', 'tasks', 'orders', 'attentions', 'acked'] as const
21
+ /**
22
+ * 상태가 들고 있는 목록 밖의 이름은 고를 없다.
23
+ *
24
+ * `energy` 는 에너지 트윈만 채운다(계량 지점·수요 구간·피크). `ALWAYS` 에 두지 않는다: 마감 구간을
25
+ * 하루치 들고 있어 작지 않고, 그것이 필요한 화면이 스스로 이름을 말하는 편이 맞다.
26
+ */
27
+ export const STATE_AXES = ['locations', 'items', 'equipment', 'persons', 'assets', 'tasks', 'orders', 'attentions', 'acked', 'energy'] as const
23
28
  export type StateAxis = (typeof STATE_AXES)[number]
24
29
 
25
30
  /** 축과 무관하게 늘 남는 것 — 이것이 없으면 무엇을 받았는지 알 수 없다. */
26
- const ALWAYS = ['revision', 'nowTime', 'simClockMs', 'corrections'] as const
31
+ const ALWAYS = ['revision', 'nowTime', 'simClockMs', 'corrections', 'unhandled'] as const
27
32
 
28
33
  export interface PickedAxes {
29
34
  /** 고른 축만 남은 상태. `axes` 가 비었으면 원본 그대로(자르지 않는다). */
@@ -2,7 +2,7 @@
2
2
  * 구조 이력의 **차이** — "2번째에 도장 부스가 넷 늘었다" 를 말할 수 있게.
3
3
  *
4
4
  * 리비전은 그 시절 공장을 통째로 담는다(재생하려면 전체 위상이 필요하다). 그런데 사람이 보고 싶은
5
- * 것은 통째가 아니라 **무엇이 달라졌나** 다 — 62KB 짜리 보드 두 개를 나란히 놓고 눈으로 비교할 수는
5
+ * 것은 통째가 아니라 **무엇이 달라졌나** 다 — 62KB 짜리 모델 두 개를 나란히 놓고 눈으로 비교할 수는
6
6
  * 없다. 그 한 줄을 여기서 만든다.
7
7
  *
8
8
  * 순수 함수인 이유: 같은 문장을 화면과 AI 가 함께 써야 한다. 한쪽에서만 계산하면 둘이 다른 말을 한다.