@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
@@ -1 +1 @@
1
- {"version":3,"file":"oee-accumulator.js","sourceRoot":"","sources":["../../server/engine/oee-accumulator.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * OEE 이벤트 누적기 (순수·근사) — live-모드 OEE 입력 생산의 한 경로(face2-inbound-live §1.1, option ②).\n *\n * 문제: OEE 공식(computeOee)은 sim/live 공유되나, 입력 카운터(runMs/downMs/…)는 미러 State·이벤트에 없다\n * (emitEquipment 는 status/location/motion 만 방출). → live 는 카운터를 별도 생산해야 한다.\n * 이 누적기: equipment.status 전이 스트림으로 무버별 시간-in-상태를 누적해 카운터를 근사 생산.\n *\n * 정확: downMs(고장 시간 = down 구간 합) · quality(good/scrap = quality.output 델타). 근사·한계:\n * - busyMs 를 runMs 로(setup 분리 불가 — 이벤트에 setup 없음) → performance 낙관·availability 는 setup 손실 미반영.\n * - quality.output 이 없으면(예: WMS/YMS) quality=1(품질 개념 없음).\n * 시간축 = Date.parse(eventTime)(ms) — 구간 차분은 sim/실시간 무관하게 정확(고정 오프셋 상쇄).\n */\nimport { computeOee } from '@operato/twin-kernel'\nimport type { CanonicalEnvelope, OeeCounters, OeeMetrics } from '@operato/twin-kernel'\n\ninterface Acc {\n status?: string\n lastMs: number\n startMs: number\n busyMs: number\n downMs: number\n goodCount: number\n scrapCount: number\n}\n\nexport class OeeAccumulator {\n private acc = new Map<string, Acc>()\n private maxMs = 0 // 최신 이벤트 시각 — nowMs 기본값(현재 계측 시점)\n\n private ensure(moverId: string, t: number): Acc {\n let a = this.acc.get(moverId)\n if (!a) {\n a = { lastMs: t, startMs: t, busyMs: 0, downMs: 0, goodCount: 0, scrapCount: 0 }\n this.acc.set(moverId, a)\n }\n return a\n }\n\n /** equipment.status(시간-in-상태) + quality.output(양품/불량) 소비. 다른 eventType 무시. */\n apply(env: CanonicalEnvelope): void {\n const t = Date.parse(env.eventTime)\n if (t > this.maxMs) this.maxMs = t\n if (env.eventType === 'quality.output') {\n const q = env.data as { moverId?: string; goodCount?: number; scrapCount?: number }\n if (!q?.moverId) return\n const a = this.ensure(q.moverId, t)\n a.goodCount = q.goodCount ?? a.goodCount // 누적값 최신화(good/scrap 은 무버 누적)\n a.scrapCount = q.scrapCount ?? a.scrapCount\n return\n }\n if (env.eventType !== 'equipment.status') return\n const d = env.data as { moverId?: string; status?: string }\n if (!d?.moverId) return\n const a = this.ensure(d.moverId, t)\n const dt = Math.max(0, t - a.lastMs)\n if (a.status === 'busy') a.busyMs += dt\n else if (a.status === 'down') a.downMs += dt\n a.status = d.status\n a.lastMs = t\n }\n\n /** 무버 OEE 카운터 — 마지막 상태 구간을 nowMs(기본=최신 이벤트 시각)까지 flush. busy→run 근사, setup=0. */\n counters(moverId: string, nowMs: number = this.maxMs): OeeCounters {\n const a = this.acc.get(moverId)\n if (!a) return { runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, scrapCount: 0, metricsSinceMs: 0 }\n const dt = Math.max(0, nowMs - a.lastMs)\n let busyMs = a.busyMs\n let downMs = a.downMs\n if (a.status === 'busy') busyMs += dt\n else if (a.status === 'down') downMs += dt\n return { runMs: busyMs, setupMs: 0, downMs, goodCount: a.goodCount, scrapCount: a.scrapCount, metricsSinceMs: a.startMs }\n }\n\n /** 공유 공식으로 근사 OEE 산출 — 정확한 OEE 는 실 텔레메트리(option ①). nowMs 기본=최신 이벤트 시각. */\n oee(moverId: string, nowMs: number = this.maxMs): OeeMetrics {\n return computeOee(this.counters(moverId, nowMs), nowMs)\n }\n}\n\n/**\n * live State 를 OEE 로 보강 — projector 미러 State(관측)의 무버에 누적기 OEE(계산)를 채운다.\n * live-모드 publishEntityData 가 buildEntityDeltas 전에 적용 → equipment data(tag) payload 에 oee 포함\n * (sim 은 커널이 채움 — 이로써 live 도 동일 payload 형상). 순수(입력 불변, 얕은 복사).\n */\nexport function withLiveOee<S extends { equipment?: any[] }>(state: S, acc: OeeAccumulator, nowMs?: number): S {\n if (!state?.equipment) return state\n return { ...state, equipment: state.equipment.map(m => ({ ...m, oee: acc.oee(m.id, nowMs) })) }\n}\n"]}
1
+ {"version":3,"file":"oee-accumulator.js","sourceRoot":"","sources":["../../server/engine/oee-accumulator.ts"],"names":[],"mappings":";;;AAqHA,kCAGC;AAxHD;;;;;;;;;;;GAWG;AACH,sDAAiD;AAmBjD,MAAa,cAAc;IAA3B;QACU,QAAG,GAAG,IAAI,GAAG,EAAe,CAAA;QAC5B,UAAK,GAAG,CAAC,CAAA,CAAC,kCAAkC;IA6EtD,CAAC;IA3ES,MAAM,CAAC,OAAe,EAAE,CAAS;QACvC,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;QAC7B,IAAI,CAAC,CAAC,EAAE,CAAC;YACP,CAAC,GAAG,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,CAAA;YAChF,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;QAC1B,CAAC;QACD,OAAO,CAAC,CAAA;IACV,CAAC;IAED;;;;;;;OAOG;IACH,SAAS;QACP,OAAO;YACL,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;SACxE,CAAA;IACH,CAAC;IAED,gDAAgD;IAChD,OAAO,CAAC,EAAoC;QAC1C,IAAI,CAAC,KAAK,GAAG,EAAE,EAAE,KAAK,IAAI,CAAC,CAAA;QAC3B,IAAI,CAAC,GAAG,GAAG,IAAI,GAAG,CAAC,CAAC,EAAE,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;IACrF,CAAC;IAED,8EAA8E;IAC9E,KAAK,CAAC,GAAsB;QAC1B,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;QACnC,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK;YAAE,IAAI,CAAC,KAAK,GAAG,CAAC,CAAA;QAClC,IAAI,GAAG,CAAC,SAAS,KAAK,gBAAgB,EAAE,CAAC;YACvC,MAAM,CAAC,GAAG,GAAG,CAAC,IAAqE,CAAA;YACnF,IAAI,CAAC,CAAC,EAAE,OAAO;gBAAE,OAAM;YACvB,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;YACnC,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,SAAS,CAAA,CAAC,8BAA8B;YACvE,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,UAAU,CAAA;YAC3C,OAAM;QACR,CAAC;QACD,IAAI,GAAG,CAAC,SAAS,KAAK,kBAAkB;YAAE,OAAM;QAChD,MAAM,CAAC,GAAG,GAAG,CAAC,IAA6C,CAAA;QAC3D,IAAI,CAAC,CAAC,EAAE,OAAO;YAAE,OAAM;QACvB,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;QACnC,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAA;QACpC,IAAI,CAAC,CAAC,MAAM,KAAK,MAAM;YAAE,CAAC,CAAC,MAAM,IAAI,EAAE,CAAA;aAClC,IAAI,CAAC,CAAC,MAAM,KAAK,MAAM;YAAE,CAAC,CAAC,MAAM,IAAI,EAAE,CAAA;QAC5C,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAA;QACnB,CAAC,CAAC,MAAM,GAAG,CAAC,CAAA;IACd,CAAC;IAED,iFAAiF;IACjF,QAAQ,CAAC,OAAe,EAAE,QAAgB,IAAI,CAAC,KAAK;QAClD,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;QAC/B;;;;;WAKG;QACH,IAAI,CAAC,CAAC;YAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,CAAA;QAC/E,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,MAAM,CAAC,CAAA;QACxC,IAAI,MAAM,GAAG,CAAC,CAAC,MAAM,CAAA;QACrB,IAAI,MAAM,GAAG,CAAC,CAAC,MAAM,CAAA;QACrB,IAAI,CAAC,CAAC,MAAM,KAAK,MAAM;YAAE,MAAM,IAAI,EAAE,CAAA;aAChC,IAAI,CAAC,CAAC,MAAM,KAAK,MAAM;YAAE,MAAM,IAAI,EAAE,CAAA;QAC1C,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE,cAAc,EAAE,CAAC,CAAC,OAAO,EAAE,CAAA;IAC3H,CAAC;IAED,2EAA2E;IAC3E,GAAG,CAAC,OAAe,EAAE,QAAgB,IAAI,CAAC,KAAK;QAC7C,OAAO,IAAA,wBAAU,EAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,KAAK,CAAC,CAAA;IACzD,CAAC;CACF;AA/ED,wCA+EC;AAED;;;;GAIG;AACH,SAAgB,WAAW,CAAkC,KAAQ,EAAE,GAAmB,EAAE,KAAc;IACxG,IAAI,CAAC,KAAK,EAAE,SAAS;QAAE,OAAO,KAAK,CAAA;IACnC,OAAO,EAAE,GAAG,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,CAAA;AACjG,CAAC","sourcesContent":["/*\n * OEE 이벤트 누적기 (순수·근사) — live-모드 OEE 입력 생산의 한 경로(face2-inbound-live §1.1, option ②).\n *\n * 문제: OEE 공식(computeOee)은 sim/live 공유되나, 입력 카운터(runMs/downMs/…)는 미러 State·이벤트에 없다\n * (emitEquipment 는 status/location/motion 만 방출). → live 는 카운터를 별도 생산해야 한다.\n * 이 누적기: equipment.status 전이 스트림으로 무버별 시간-in-상태를 누적해 카운터를 근사 생산.\n *\n * 정확: downMs(고장 시간 = down 구간 합) · quality(good/scrap = quality.output 델타). 근사·한계:\n * - busyMs 를 runMs 로(setup 분리 불가 — 이벤트에 setup 없음) → performance 낙관·availability 는 setup 손실 미반영.\n * - quality.output 이 없으면(예: WMS/YMS) quality=1(품질 개념 없음).\n * 시간축 = Date.parse(eventTime)(ms) — 구간 차분은 sim/실시간 무관하게 정확(고정 오프셋 상쇄).\n */\nimport { computeOee } from '@operato/twin-kernel'\nimport type { CanonicalEnvelope, OeeCounters, OeeMetrics } from '@operato/twin-kernel'\n\ninterface Acc {\n status?: string\n lastMs: number\n startMs: number\n busyMs: number\n downMs: number\n goodCount: number\n scrapCount: number\n}\n\n/** 누적기의 완전한 재개점 — 저장했다 되살릴 수 있어야 꼬리만 접을 수 있다. */\nexport interface OeeCheckpoint {\n maxMs: number\n acc: ({ moverId: string } & Acc)[]\n}\n\nexport class OeeAccumulator {\n private acc = new Map<string, Acc>()\n private maxMs = 0 // 최신 이벤트 시각 — nowMs 기본값(현재 계측 시점)\n\n private ensure(moverId: string, t: number): Acc {\n let a = this.acc.get(moverId)\n if (!a) {\n a = { lastMs: t, startMs: t, busyMs: 0, downMs: 0, goodCount: 0, scrapCount: 0 }\n this.acc.set(moverId, a)\n }\n return a\n }\n\n /**\n * **재개점** — 이 누적기의 상태 전부(이어 접기의 씨앗).\n *\n * ── 왜 필요한가 (2026-08-18) ────────────────────────────────────────────────\n * 가동 이력은 원 시스템이 누적을 주지 않아 **우리가 이벤트를 적분해** 만든다. 그래서 저널을 꼬리만\n * 접으면 이 누적기는 꼬리분만 갖게 되고, 화면의 가용률·수율이 조용히 작아진다(오류는 나지 않는다).\n * 재개점을 함께 저장·복원해야 「이어 접은 값」이 「0부터 접은 값」과 같아진다.\n */\n serialize(): OeeCheckpoint {\n return {\n maxMs: this.maxMs,\n acc: [...this.acc.entries()].map(([moverId, a]) => ({ moverId, ...a }))\n }\n }\n\n /** 재개점에서 되세운다 — 모르는 것은 지어내지 않는다(없으면 빈 누적기다). */\n restore(cp: OeeCheckpoint | undefined | null): void {\n this.maxMs = cp?.maxMs ?? 0\n this.acc = new Map((cp?.acc ?? []).map(({ moverId, ...a }) => [moverId, { ...a }]))\n }\n\n /** equipment.status(시간-in-상태) + quality.output(양품/불량) 소비. 다른 eventType 무시. */\n apply(env: CanonicalEnvelope): void {\n const t = Date.parse(env.eventTime)\n if (t > this.maxMs) this.maxMs = t\n if (env.eventType === 'quality.output') {\n const q = env.data as { moverId?: string; goodCount?: number; scrapCount?: number }\n if (!q?.moverId) return\n const a = this.ensure(q.moverId, t)\n a.goodCount = q.goodCount ?? a.goodCount // 누적값 최신화(good/scrap 은 무버 누적)\n a.scrapCount = q.scrapCount ?? a.scrapCount\n return\n }\n if (env.eventType !== 'equipment.status') return\n const d = env.data as { moverId?: string; status?: string }\n if (!d?.moverId) return\n const a = this.ensure(d.moverId, t)\n const dt = Math.max(0, t - a.lastMs)\n if (a.status === 'busy') a.busyMs += dt\n else if (a.status === 'down') a.downMs += dt\n a.status = d.status\n a.lastMs = t\n }\n\n /** 무버 OEE 카운터 — 마지막 상태 구간을 nowMs(기본=최신 이벤트 시각)까지 flush. busy→run 근사, setup=0. */\n counters(moverId: string, nowMs: number = this.maxMs): OeeCounters {\n const a = this.acc.get(moverId)\n /*\n * **이 설비의 이벤트를 하나도 못 봤다** — 계측 창이 없다는 뜻이고, 그것을 `0` 으로 메우면\n * \"1970년부터 재고 있었다\" 가 된다. 시각이 밀리초 epoch 이므로 그 값이 곧 지금 시각이 되어,\n * 화면에 `idleMs 1767226603000`(약 56,000년) 유휴가 섰다. 창을 **말하지 않는다** — 공식이\n * 없음을 없음으로 다룬다(§computeOee).\n */\n if (!a) return { runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, scrapCount: 0 }\n const dt = Math.max(0, nowMs - a.lastMs)\n let busyMs = a.busyMs\n let downMs = a.downMs\n if (a.status === 'busy') busyMs += dt\n else if (a.status === 'down') downMs += dt\n return { runMs: busyMs, setupMs: 0, downMs, goodCount: a.goodCount, scrapCount: a.scrapCount, metricsSinceMs: a.startMs }\n }\n\n /** 공유 공식으로 근사 OEE 산출 — 정확한 OEE 는 실 텔레메트리(option ①). nowMs 기본=최신 이벤트 시각. */\n oee(moverId: string, nowMs: number = this.maxMs): OeeMetrics {\n return computeOee(this.counters(moverId, nowMs), nowMs)\n }\n}\n\n/**\n * live State 를 OEE 로 보강 — projector 미러 State(관측)의 무버에 누적기 OEE(계산)를 채운다.\n * live-모드 publishEntityData 가 buildEntityDeltas 전에 적용 → equipment data(tag) payload 에 oee 포함\n * (sim 은 커널이 채움 — 이로써 live 도 동일 payload 형상). 순수(입력 불변, 얕은 복사).\n */\nexport function withLiveOee<S extends { equipment?: any[] }>(state: S, acc: OeeAccumulator, nowMs?: number): S {\n if (!state?.equipment) return state\n return { ...state, equipment: state.equipment.map(m => ({ ...m, oee: acc.oee(m.id, nowMs) })) }\n}\n"]}
@@ -0,0 +1,32 @@
1
+ /** 공정 한 종류 — 이름과 「지금 무엇으로 도나」. */
2
+ export interface OperationBasisRow {
3
+ /** 작업 종류(`FlowTask.kind` = `OperationDef.key`) — 선언이 겨누는 id 다. */
4
+ kind: string;
5
+ /** 사람이 읽는 이름 — 원천 명세 행이 있을 때만(없으면 종류 이름이 곧 이름이다). */
6
+ label?: string;
7
+ /**
8
+ * 지금 이 종류의 시간이 어디서 오나 — 커널의 자기보고, 또는 **이력에서 배운 사실**. 없으면 모른다.
9
+ * · `measured` — 이 현장의 이력에서 배운 값(가장 강한 근거). **선언보다 이긴다.**
10
+ * · `declared` — 선언된 값(현장이 정한 것 또는 원천 명세 행).
11
+ * · `default` — 커널 소스의 상수. 이 목록의 존재 이유다.
12
+ */
13
+ basis?: 'measured' | 'declared' | 'default';
14
+ /** 원천 명세 행이 말한 시간(ISO 8601) — 있으면. 우리 선언이 이것을 이긴다. */
15
+ referenceDuration?: string;
16
+ /** 우리가 선언해 둔 시간(ISO 8601) — 있으면. */
17
+ localDuration?: string;
18
+ }
19
+ /**
20
+ * 공정 목록을 모은다 — **넷의 합집합**, 순서는 원천 행 → 돌린 것 → 이력에서 배운 것 → 선언한 것.
21
+ *
22
+ * `kernel` 이 없으면(정지 트윈) ②가 빠지고, `measuredKinds` 를 주지 않으면 ③이 빠진다: 그 사실은
23
+ * `basis` 가 비는 것으로 드러난다(빈칸이 곧 「모른다」다).
24
+ */
25
+ export declare function operationBasis(model: any, kernel?: any, measuredKinds?: readonly string[]): OperationBasisRow[];
26
+ /**
27
+ * 선언을 받아도 되는 공정 이름들 — **하나도 확인할 수 없으면 `undefined`**(모른다).
28
+ *
29
+ * 빈 배열로 답하지 않는 이유: 빈 배열은 「어떤 이름도 안 된다」로 읽히고, 그러면 아직 돌지 않은 트윈에는
30
+ * 아무것도 선언할 수 없다. 모르는 것과 없는 것은 다르다 — 모를 때는 받아 주고 그 사실을 말한다.
31
+ */
32
+ export declare function knownOperationKinds(model: any, kernel?: any, measuredKinds?: readonly string[]): string[] | undefined;
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.operationBasis = operationBasis;
4
+ exports.knownOperationKinds = knownOperationKinds;
5
+ /*
6
+ * 이 트윈이 **어떤 공정을 돌리고, 그 시간이 어디서 오나.**
7
+ *
8
+ * ── 왜 이 목록이 필요한가 (2026-08-18) ─────────────────────────────────────
9
+ * 「측정이 없을 때의 표준 시간」을 현장이 선언하려면 먼저 **무엇에 선언할지** 보여야 한다. 그런데 공정은
10
+ * 자리·설비와 달리 모델에 행이 없을 수 있다 — 실측: 운영 중인 트윈 35개 중 창고·야드는 공정 명세 행이
11
+ * 0개였고(MES만 4개), 그 트윈들의 시간은 전부 커널 상수에서 왔다.
12
+ *
13
+ * 그래서 목록을 **지어내지 않고 네 곳에서 모은다**:
14
+ * ① 원천 명세 행(`model.operations`) — 원 시스템이 말한 공정.
15
+ * ② 커널의 자기보고(`specCoverage()`) — **실제로 돌린** 공정과 그 시간의 출처.
16
+ * ③ 이력에서 배운 종류(`measuredOperationKinds`) — 재기동에도 남는 근거.
17
+ * ④ 우리가 이미 선언한 것(`model.localDurations`) — 선언해 둔 공정이 목록에서 사라지지 않게.
18
+ *
19
+ * ③이 필요한 이유는 실측으로 드러났다: ②는 **작업이 새로 생길 때** 채워지므로, 이력이 풍부한 창고
20
+ * 트윈도 재기동 직후에는 목록이 비었다 — 값을 채울 곳을 보여야 하는 화면이 정작 그때 아무 말도 못 한다.
21
+ *
22
+ * 그래도 셋 다 비는 트윈은 있다(한 번도 돌지 않고 명세 행도 없는 새 트윈). 그때 「공정이 없다」고 말하지
23
+ * 않는다(모르는 것이다) — 부르는 쪽이 그 사실을 그대로 화면에 옮긴다.
24
+ */
25
+ const spec_coverage_ts_1 = require("./spec-coverage.js");
26
+ /**
27
+ * 공정 목록을 모은다 — **넷의 합집합**, 순서는 원천 행 → 돌린 것 → 이력에서 배운 것 → 선언한 것.
28
+ *
29
+ * `kernel` 이 없으면(정지 트윈) ②가 빠지고, `measuredKinds` 를 주지 않으면 ③이 빠진다: 그 사실은
30
+ * `basis` 가 비는 것으로 드러난다(빈칸이 곧 「모른다」다).
31
+ */
32
+ function operationBasis(model, kernel, measuredKinds) {
33
+ const rows = new Map();
34
+ const at = (kind) => {
35
+ const cur = rows.get(kind);
36
+ if (cur)
37
+ return cur;
38
+ const row = { kind };
39
+ rows.set(kind, row);
40
+ return row;
41
+ };
42
+ for (const o of (model?.operations ?? [])) {
43
+ if (!o?.key)
44
+ continue;
45
+ const row = at(String(o.key));
46
+ if (o.label)
47
+ row.label = String(o.label);
48
+ if (o.duration)
49
+ row.referenceDuration = String(o.duration);
50
+ }
51
+ const coverage = kernel ? (0, spec_coverage_ts_1.readSpecCoverage)(kernel) : undefined;
52
+ for (const o of coverage?.operations ?? []) {
53
+ if (!o?.kind)
54
+ continue;
55
+ at(String(o.kind)).basis = o.duration;
56
+ }
57
+ /*
58
+ * 이력에서 배운 종류 — 커널이 아직 이 종류를 돌리지 않았어도 **추정기가 답할 수 있다**. 그래서 근거를
59
+ * `measured` 로 채운다: 짐작이 아니라 설치된 추정기가 그 종류에 실측값을 줄 것이라는 사실이다.
60
+ * 커널이 이미 보고한 종류는 **덮지 않는다** — 실제로 돌린 것이 더 강한 사실이다.
61
+ */
62
+ for (const kind of measuredKinds ?? []) {
63
+ if (!kind)
64
+ continue;
65
+ const row = at(String(kind));
66
+ if (!row.basis)
67
+ row.basis = 'measured';
68
+ }
69
+ const declared = (model?.localDurations ?? {});
70
+ for (const [kind, iso] of Object.entries(declared)) {
71
+ if (!kind)
72
+ continue;
73
+ at(kind).localDuration = String(iso);
74
+ }
75
+ return [...rows.values()];
76
+ }
77
+ /**
78
+ * 선언을 받아도 되는 공정 이름들 — **하나도 확인할 수 없으면 `undefined`**(모른다).
79
+ *
80
+ * 빈 배열로 답하지 않는 이유: 빈 배열은 「어떤 이름도 안 된다」로 읽히고, 그러면 아직 돌지 않은 트윈에는
81
+ * 아무것도 선언할 수 없다. 모르는 것과 없는 것은 다르다 — 모를 때는 받아 주고 그 사실을 말한다.
82
+ */
83
+ function knownOperationKinds(model, kernel, measuredKinds) {
84
+ const kinds = operationBasis(model, kernel, measuredKinds).map(r => r.kind);
85
+ return kinds.length ? kinds : undefined;
86
+ }
87
+ //# sourceMappingURL=operation-basis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operation-basis.js","sourceRoot":"","sources":["../../server/engine/operation-basis.ts"],"names":[],"mappings":";;AA+CA,wCAyCC;AAQD,kDAGC;AAnGD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,yDAAqD;AAqBrD;;;;;GAKG;AACH,SAAgB,cAAc,CAAC,KAAU,EAAE,MAAY,EAAE,aAAiC;IACxF,MAAM,IAAI,GAAG,IAAI,GAAG,EAA6B,CAAA;IACjD,MAAM,EAAE,GAAG,CAAC,IAAY,EAAqB,EAAE;QAC7C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QAC1B,IAAI,GAAG;YAAE,OAAO,GAAG,CAAA;QACnB,MAAM,GAAG,GAAsB,EAAE,IAAI,EAAE,CAAA;QACvC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;QACnB,OAAO,GAAG,CAAA;IACZ,CAAC,CAAA;IAED,KAAK,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,UAAU,IAAI,EAAE,CAAU,EAAE,CAAC;QACnD,IAAI,CAAC,CAAC,EAAE,GAAG;YAAE,SAAQ;QACrB,MAAM,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;QAC7B,IAAI,CAAC,CAAC,KAAK;YAAE,GAAG,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAA;QACxC,IAAI,CAAC,CAAC,QAAQ;YAAE,GAAG,CAAC,iBAAiB,GAAG,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAA;IAC5D,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,CAAC,IAAA,mCAAgB,EAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAC9D,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,UAAU,IAAI,EAAE,EAAE,CAAC;QAC3C,IAAI,CAAC,CAAC,EAAE,IAAI;YAAE,SAAQ;QACtB,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,QAAQ,CAAA;IACvC,CAAC;IAED;;;;OAIG;IACH,KAAK,MAAM,IAAI,IAAI,aAAa,IAAI,EAAE,EAAE,CAAC;QACvC,IAAI,CAAC,IAAI;YAAE,SAAQ;QACnB,MAAM,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;QAC5B,IAAI,CAAC,GAAG,CAAC,KAAK;YAAE,GAAG,CAAC,KAAK,GAAG,UAAU,CAAA;IACxC,CAAC;IAED,MAAM,QAAQ,GAAG,CAAC,KAAK,EAAE,cAAc,IAAI,EAAE,CAA2B,CAAA;IACxE,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QACnD,IAAI,CAAC,IAAI;YAAE,SAAQ;QACnB,EAAE,CAAC,IAAI,CAAC,CAAC,aAAa,GAAG,MAAM,CAAC,GAAG,CAAC,CAAA;IACtC,CAAC;IAED,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAA;AAC3B,CAAC;AAED;;;;;GAKG;AACH,SAAgB,mBAAmB,CAAC,KAAU,EAAE,MAAY,EAAE,aAAiC;IAC7F,MAAM,KAAK,GAAG,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;IAC3E,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAA;AACzC,CAAC","sourcesContent":["/*\n * 이 트윈이 **어떤 공정을 돌리고, 그 시간이 어디서 오나.**\n *\n * ── 왜 이 목록이 필요한가 (2026-08-18) ─────────────────────────────────────\n * 「측정이 없을 때의 표준 시간」을 현장이 선언하려면 먼저 **무엇에 선언할지** 보여야 한다. 그런데 공정은\n * 자리·설비와 달리 모델에 행이 없을 수 있다 — 실측: 운영 중인 트윈 35개 중 창고·야드는 공정 명세 행이\n * 0개였고(MES만 4개), 그 트윈들의 시간은 전부 커널 상수에서 왔다.\n *\n * 그래서 목록을 **지어내지 않고 네 곳에서 모은다**:\n * ① 원천 명세 행(`model.operations`) — 원 시스템이 말한 공정.\n * ② 커널의 자기보고(`specCoverage()`) — **실제로 돌린** 공정과 그 시간의 출처.\n * ③ 이력에서 배운 종류(`measuredOperationKinds`) — 재기동에도 남는 근거.\n * ④ 우리가 이미 선언한 것(`model.localDurations`) — 선언해 둔 공정이 목록에서 사라지지 않게.\n *\n * ③이 필요한 이유는 실측으로 드러났다: ②는 **작업이 새로 생길 때** 채워지므로, 이력이 풍부한 창고\n * 트윈도 재기동 직후에는 목록이 비었다 — 값을 채울 곳을 보여야 하는 화면이 정작 그때 아무 말도 못 한다.\n *\n * 그래도 셋 다 비는 트윈은 있다(한 번도 돌지 않고 명세 행도 없는 새 트윈). 그때 「공정이 없다」고 말하지\n * 않는다(모르는 것이다) — 부르는 쪽이 그 사실을 그대로 화면에 옮긴다.\n */\nimport { readSpecCoverage } from './spec-coverage.ts'\n\n/** 공정 한 종류 — 이름과 「지금 무엇으로 도나」. */\nexport interface OperationBasisRow {\n /** 작업 종류(`FlowTask.kind` = `OperationDef.key`) — 선언이 겨누는 id 다. */\n kind: string\n /** 사람이 읽는 이름 — 원천 명세 행이 있을 때만(없으면 종류 이름이 곧 이름이다). */\n label?: string\n /**\n * 지금 이 종류의 시간이 어디서 오나 — 커널의 자기보고, 또는 **이력에서 배운 사실**. 없으면 모른다.\n * · `measured` — 이 현장의 이력에서 배운 값(가장 강한 근거). **선언보다 이긴다.**\n * · `declared` — 선언된 값(현장이 정한 것 또는 원천 명세 행).\n * · `default` — 커널 소스의 상수. 이 목록의 존재 이유다.\n */\n basis?: 'measured' | 'declared' | 'default'\n /** 원천 명세 행이 말한 시간(ISO 8601) — 있으면. 우리 선언이 이것을 이긴다. */\n referenceDuration?: string\n /** 우리가 선언해 둔 시간(ISO 8601) — 있으면. */\n localDuration?: string\n}\n\n/**\n * 공정 목록을 모은다 — **넷의 합집합**, 순서는 원천 행 → 돌린 것 → 이력에서 배운 것 → 선언한 것.\n *\n * `kernel` 이 없으면(정지 트윈) ②가 빠지고, `measuredKinds` 를 주지 않으면 ③이 빠진다: 그 사실은\n * `basis` 가 비는 것으로 드러난다(빈칸이 곧 「모른다」다).\n */\nexport function operationBasis(model: any, kernel?: any, measuredKinds?: readonly string[]): OperationBasisRow[] {\n const rows = new Map<string, OperationBasisRow>()\n const at = (kind: string): OperationBasisRow => {\n const cur = rows.get(kind)\n if (cur) return cur\n const row: OperationBasisRow = { kind }\n rows.set(kind, row)\n return row\n }\n\n for (const o of (model?.operations ?? []) as any[]) {\n if (!o?.key) continue\n const row = at(String(o.key))\n if (o.label) row.label = String(o.label)\n if (o.duration) row.referenceDuration = String(o.duration)\n }\n\n const coverage = kernel ? readSpecCoverage(kernel) : undefined\n for (const o of coverage?.operations ?? []) {\n if (!o?.kind) continue\n at(String(o.kind)).basis = o.duration\n }\n\n /*\n * 이력에서 배운 종류 — 커널이 아직 이 종류를 돌리지 않았어도 **추정기가 답할 수 있다**. 그래서 근거를\n * `measured` 로 채운다: 짐작이 아니라 설치된 추정기가 그 종류에 실측값을 줄 것이라는 사실이다.\n * 커널이 이미 보고한 종류는 **덮지 않는다** — 실제로 돌린 것이 더 강한 사실이다.\n */\n for (const kind of measuredKinds ?? []) {\n if (!kind) continue\n const row = at(String(kind))\n if (!row.basis) row.basis = 'measured'\n }\n\n const declared = (model?.localDurations ?? {}) as Record<string, string>\n for (const [kind, iso] of Object.entries(declared)) {\n if (!kind) continue\n at(kind).localDuration = String(iso)\n }\n\n return [...rows.values()]\n}\n\n/**\n * 선언을 받아도 되는 공정 이름들 — **하나도 확인할 수 없으면 `undefined`**(모른다).\n *\n * 빈 배열로 답하지 않는 이유: 빈 배열은 「어떤 이름도 안 된다」로 읽히고, 그러면 아직 돌지 않은 트윈에는\n * 아무것도 선언할 수 없다. 모르는 것과 없는 것은 다르다 — 모를 때는 받아 주고 그 사실을 말한다.\n */\nexport function knownOperationKinds(model: any, kernel?: any, measuredKinds?: readonly string[]): string[] | undefined {\n const kinds = operationBasis(model, kernel, measuredKinds).map(r => r.kind)\n return kinds.length ? kinds : undefined\n}\n"]}
@@ -0,0 +1,30 @@
1
+ /** 효과가 있는 속성 하나. */
2
+ export interface PropertyEffect {
3
+ /** 속성 id — 선언에 적히는 그 이름(`ResourceProperty.id`). */
4
+ id: string;
5
+ /** 어느 축의 속성인가 — 그 축의 항목에만 이 효과를 말한다. */
6
+ axes: string[];
7
+ /** 무엇을 바꾸나 — 화면이 읽는 i18n 키(언어중립: 서버는 사람 말을 만들지 않는다). */
8
+ effect: string;
9
+ /** 실제로 읽는 코드 — 지어낸 효과를 등록하지 않기 위한 표식이다(없는 자리를 가리키면 리뷰가 잡는다). */
10
+ readBy: string;
11
+ }
12
+ export declare const PROPERTY_EFFECTS: PropertyEffect[];
13
+ /**
14
+ * 이 축의 속성 효과 — `속성 id → i18n 키`.
15
+ *
16
+ * 없는 속성은 **여기 없다**(빈 값으로 채우지 않는다) — 화면이 "등록된 효과가 없다" 를 스스로 말한다.
17
+ */
18
+ export declare function propertyEffectsOf(axis: string): Record<string, string>;
19
+ /**
20
+ * 이 속성을 **어느 대상에 얹을 수 있나** — 등록부의 축이 그대로 답한다.
21
+ *
22
+ * ── 왜 필요한가 (2026-08-18) ────────────────────────────────────────────────
23
+ * 지게차에 계약전력을 선언할 수 있었다. 문은 받아 주고 화면은 「넣었습니다」라고 말하는데, 그 값을 읽는
24
+ * 코드는 자리(수전)만 본다 — 아무 일도 일어나지 않고 그 사실이 어디에도 나오지 않았다. 효과가 어느 축의
25
+ * 것인지는 이미 등록부가 알고 있으므로, 그 지식을 검사에 쓴다(두 곳에 적지 않는다).
26
+ *
27
+ * 등록부에 없는 속성은 **비어 있는 목록이 아니라 `undefined`** 다: 「어디에도 못 얹는다」와 「우리가
28
+ * 제한을 모른다」는 다르다(후자는 받아 준다 — 속성은 열려 있다는 규약을 지킨다).
29
+ */
30
+ export declare function declarationTargetsOf(propertyId: string): string[] | undefined;
@@ -0,0 +1,192 @@
1
+ "use strict";
2
+ /*
3
+ * 자원 속성의 **효과 등록부** — 이 속성이 무엇을 바꾸나.
4
+ *
5
+ * ── 왜 있나 ─────────────────────────────────────────────────────────────────
6
+ * 속성은 **열려 있다**: 템플릿·마스터가 아무 id 로나 사실을 실어 보낼 수 있고, 커널은 그것을 그대로
7
+ * 나르고 화면은 단위와 함께 보인다(표준이 속성 어휘를 정하지 않으므로 그것이 옳다).
8
+ *
9
+ * 그런데 **효과는 닫혀 있다**: 값이 계산·배정·판정을 바꾸려면 그 규칙을 코드가 알아야 한다. 지금
10
+ * 효과가 있는 것은 둘뿐이다. 문제는 그 사실이 **어디에도 드러나지 않는다**는 것이었다 — 사용자는
11
+ * 속성을 넣으면 무언가 달라질 것으로 기대하는데, 실제로는 표에 한 줄이 늘 뿐이다.
12
+ *
13
+ * 그래서 이 등록부는 **지어내지 않고 드러낸다**: 읽는 코드가 있는 것만 적고, 없는 것은 "표시만 된다"
14
+ * 고 화면이 말한다. 새 효과를 코드로 붙이는 사람은 **여기 한 줄을 더한다** — 그러지 않으면 사용자는
15
+ * 그 효과가 있는지 알 수 없다.
16
+ *
17
+ * ⚠️ 이 파일은 등록부이고 판정이 아니다. 값을 실제로 읽는 곳은 각자다(아래 `readBy` 가 그 자리를
18
+ * 가리킨다) — 여기에 계산을 옮겨 오면 규칙이 두 벌이 된다.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.PROPERTY_EFFECTS = void 0;
22
+ exports.propertyEffectsOf = propertyEffectsOf;
23
+ exports.declarationTargetsOf = declarationTargetsOf;
24
+ exports.PROPERTY_EFFECTS = [
25
+ {
26
+ id: 'speed',
27
+ axes: ['equipment'],
28
+ effect: 'twin.propeffect.speed',
29
+ readBy: 'headless-twin/server/engine/travel-estimator.ts (buildTravelEstimator)'
30
+ },
31
+ {
32
+ id: 'perBaseUnit',
33
+ axes: ['materialDefinitions'],
34
+ effect: 'twin.propeffect.perBaseUnit',
35
+ readBy: '@operato/twin-kernel conversionFactorOf'
36
+ },
37
+ /*
38
+ * ── 에너지 속성 (2026-08-18) ──────────────────────────────────────────────
39
+ *
40
+ * 이 값들은 **오래전부터 읽히고 있었다** — 커널이 부하·피크·배터리를 그것으로 계산하고, 호스트가
41
+ * 금액을 그것으로 만든다. 그런데 등록부에 없어서 화면은 그 값들에 대해 「표시만 됩니다」라고 말했다.
42
+ * 조용한 거짓말이다: 사용자는 계약전력을 넣고도 「이 값은 아무것도 바꾸지 않는다」는 문장을 읽었다.
43
+ *
44
+ * 효과를 새로 만든 것이 아니라 **있는 효과를 드러낸다.** `readBy` 는 실제로 읽는 자리를 가리킨다.
45
+ */
46
+ {
47
+ id: 'contract.kW',
48
+ axes: ['locations'],
49
+ effect: 'twin.propeffect.contractKW',
50
+ readBy: '@operato/twin-kernel EmsKernel.declaredContractKW (계약 대비 판정·초과 주의)'
51
+ },
52
+ {
53
+ id: 'power.ratedKW',
54
+ axes: ['equipment'],
55
+ effect: 'twin.propeffect.ratedKW',
56
+ readBy: '@operato/twin-kernel EmsKernel.deriveEquipmentLoad (가동 중 부하)'
57
+ },
58
+ {
59
+ id: 'power.standbyKW',
60
+ axes: ['equipment'],
61
+ effect: 'twin.propeffect.standbyKW',
62
+ readBy: '@operato/twin-kernel EmsKernel.deriveEquipmentLoad (대기 중 부하)'
63
+ },
64
+ {
65
+ id: 'storage.capacityKWh',
66
+ axes: ['equipment'],
67
+ effect: 'twin.propeffect.capacityKWh',
68
+ readBy: '@operato/twin-kernel EmsKernel (축전지 방전 가능량)'
69
+ },
70
+ {
71
+ id: 'dispatch.dischargeAboveKW',
72
+ axes: ['equipment'],
73
+ effect: 'twin.propeffect.dischargeAboveKW',
74
+ readBy: '@operato/twin-kernel EmsKernel (방전 시작 문턱)'
75
+ },
76
+ {
77
+ id: 'dispatch.maxDischargeKW',
78
+ axes: ['equipment'],
79
+ effect: 'twin.propeffect.maxDischargeKW',
80
+ readBy: '@operato/twin-kernel EmsKernel (인버터 방전 상한)'
81
+ },
82
+ {
83
+ id: 'dispatch.reserveSoc',
84
+ axes: ['equipment'],
85
+ effect: 'twin.propeffect.reserveSoc',
86
+ readBy: '@operato/twin-kernel EmsKernel (이 아래로는 방전하지 않는다)'
87
+ },
88
+ {
89
+ id: 'storage.initialSoc',
90
+ axes: ['equipment'],
91
+ effect: 'twin.propeffect.initialSoc',
92
+ readBy: '@operato/twin-kernel EmsKernel (시작 충전율 — 예측·what-if 의 씨앗)'
93
+ },
94
+ {
95
+ id: 'generation.ratedKW',
96
+ axes: ['equipment'],
97
+ effect: 'twin.propeffect.genRatedKW',
98
+ readBy: '@operato/twin-kernel EmsKernel.deriveGeneration (정격 발전)'
99
+ },
100
+ {
101
+ id: 'generation.dailyProfile',
102
+ axes: ['equipment'],
103
+ effect: 'twin.propeffect.genDailyProfile',
104
+ readBy: '@operato/twin-kernel generationFractionAt (시각별 발전 비율)'
105
+ },
106
+ /* 요금은 커널이 읽지 않는다 — 금액은 저널을 접는 호스트가 만든다. readBy 가 그 사실을 말한다. */
107
+ {
108
+ id: 'tariff.demandChargePerKW',
109
+ axes: ['locations'],
110
+ effect: 'twin.propeffect.demandChargePerKW',
111
+ readBy: 'headless-twin/server/engine/kpi-query.ts (수요요금 계산)'
112
+ },
113
+ {
114
+ id: 'tariff.energyChargePerKWh',
115
+ axes: ['locations'],
116
+ effect: 'twin.propeffect.energyChargePerKWh',
117
+ readBy: 'headless-twin/server/engine/kpi-query.ts (전력량 요금 계산)'
118
+ },
119
+ /* 판정 기준 — 값이 경보의 유무를 바꾼다(가장 눈에 보이는 효과다). */
120
+ {
121
+ id: 'attention.congestionRatio',
122
+ axes: ['locations'],
123
+ effect: 'twin.propeffect.congestionRatio',
124
+ readBy: '@operato/twin-kernel deriveAttentions (병목 판정 기준)'
125
+ },
126
+ {
127
+ id: 'attention.scrapRate',
128
+ axes: ['equipment'],
129
+ effect: 'twin.propeffect.scrapRate',
130
+ readBy: '@operato/twin-kernel deriveAttentions (불량 과다 판정 기준)'
131
+ },
132
+ {
133
+ id: 'attention.scrapMinSamples',
134
+ axes: ['equipment'],
135
+ effect: 'twin.propeffect.scrapMinSamples',
136
+ readBy: '@operato/twin-kernel deriveAttentions (불량률을 재기 시작할 표본 수)'
137
+ },
138
+ /*
139
+ * ── 공정 소요시간 (2026-08-18) ────────────────────────────────────────────
140
+ *
141
+ * 실측: 운영 중인 트윈 35개 중 **창고·야드는 공정 명세 행이 0개**였다 — 그 트윈들의 시간은 전부 커널
142
+ * 상수에서 왔고, 예측은 「같은 조건에서의 상대 비교」에 머물렀다(절대 시각 주장 불가). 이 선언이
143
+ * 그 사다리를 한 칸 올린다: 실측이 없어도 **현장이 정한 시간**으로 굴러간다.
144
+ *
145
+ * 실측(이력에서 배운 값)은 여전히 이 선언을 이긴다 — 화면이 그 사실을 함께 말한다.
146
+ */
147
+ {
148
+ id: 'operation.duration',
149
+ axes: ['operations'],
150
+ effect: 'twin.propeffect.operationDuration',
151
+ readBy: '@operato/twin-kernel FlowEngine.durationOf ← declareDurations (engine/local-declarations.ts 가 실어 보낸다)'
152
+ },
153
+ {
154
+ id: 'tariff.currency',
155
+ axes: ['locations'],
156
+ effect: 'twin.propeffect.currency',
157
+ readBy: 'headless-twin/server/engine/kpi-query.ts (금액 표기 통화)'
158
+ }
159
+ ];
160
+ /**
161
+ * 이 축의 속성 효과 — `속성 id → i18n 키`.
162
+ *
163
+ * 없는 속성은 **여기 없다**(빈 값으로 채우지 않는다) — 화면이 "등록된 효과가 없다" 를 스스로 말한다.
164
+ */
165
+ function propertyEffectsOf(axis) {
166
+ const out = {};
167
+ for (const e of exports.PROPERTY_EFFECTS)
168
+ if (e.axes.includes(axis))
169
+ out[e.id] = e.effect;
170
+ return out;
171
+ }
172
+ /** 등록부의 축 이름 → 선언이 겨누는 대상 이름. 대상이 아닌 축(자재 정의 등)은 여기 없다. */
173
+ const AXIS_TO_TARGET = { locations: 'location', equipment: 'equipment', operations: 'operation' };
174
+ /**
175
+ * 이 속성을 **어느 대상에 얹을 수 있나** — 등록부의 축이 그대로 답한다.
176
+ *
177
+ * ── 왜 필요한가 (2026-08-18) ────────────────────────────────────────────────
178
+ * 지게차에 계약전력을 선언할 수 있었다. 문은 받아 주고 화면은 「넣었습니다」라고 말하는데, 그 값을 읽는
179
+ * 코드는 자리(수전)만 본다 — 아무 일도 일어나지 않고 그 사실이 어디에도 나오지 않았다. 효과가 어느 축의
180
+ * 것인지는 이미 등록부가 알고 있으므로, 그 지식을 검사에 쓴다(두 곳에 적지 않는다).
181
+ *
182
+ * 등록부에 없는 속성은 **비어 있는 목록이 아니라 `undefined`** 다: 「어디에도 못 얹는다」와 「우리가
183
+ * 제한을 모른다」는 다르다(후자는 받아 준다 — 속성은 열려 있다는 규약을 지킨다).
184
+ */
185
+ function declarationTargetsOf(propertyId) {
186
+ const e = exports.PROPERTY_EFFECTS.find(x => x.id === propertyId);
187
+ if (!e)
188
+ return undefined;
189
+ const targets = e.axes.map(a => AXIS_TO_TARGET[a]).filter((t) => !!t);
190
+ return targets.length ? targets : undefined;
191
+ }
192
+ //# sourceMappingURL=property-effects.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"property-effects.js","sourceRoot":"","sources":["../../server/engine/property-effects.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 자원 속성의 **효과 등록부** — 이 속성이 무엇을 바꾸나.\n *\n * ── 왜 있나 ─────────────────────────────────────────────────────────────────\n * 속성은 **열려 있다**: 템플릿·마스터가 아무 id 로나 사실을 실어 보낼 수 있고, 커널은 그것을 그대로\n * 나르고 화면은 단위와 함께 보인다(표준이 속성 어휘를 정하지 않으므로 그것이 옳다).\n *\n * 그런데 **효과는 닫혀 있다**: 값이 계산·배정·판정을 바꾸려면 그 규칙을 코드가 알아야 한다. 지금\n * 효과가 있는 것은 둘뿐이다. 문제는 그 사실이 **어디에도 드러나지 않는다**는 것이었다 — 사용자는\n * 속성을 넣으면 무언가 달라질 것으로 기대하는데, 실제로는 표에 한 줄이 늘 뿐이다.\n *\n * 그래서 이 등록부는 **지어내지 않고 드러낸다**: 읽는 코드가 있는 것만 적고, 없는 것은 \"표시만 된다\"\n * 고 화면이 말한다. 새 효과를 코드로 붙이는 사람은 **여기 한 줄을 더한다** — 그러지 않으면 사용자는\n * 그 효과가 있는지 알 수 없다.\n *\n * ⚠️ 이 파일은 등록부이고 판정이 아니다. 값을 실제로 읽는 곳은 각자다(아래 `readBy` 가 그 자리를\n * 가리킨다) — 여기에 계산을 옮겨 오면 규칙이 두 벌이 된다.\n */\n\n/** 효과가 있는 속성 하나. */\nexport interface PropertyEffect {\n /** 속성 id — 선언에 적히는 그 이름(`ResourceProperty.id`). */\n id: string\n /** 어느 축의 속성인가 — 그 축의 항목에만 이 효과를 말한다. */\n axes: string[]\n /** 무엇을 바꾸나 — 화면이 읽는 i18n 키(언어중립: 서버는 사람 말을 만들지 않는다). */\n effect: string\n /** 실제로 읽는 코드 — 지어낸 효과를 등록하지 않기 위한 표식이다(없는 자리를 가리키면 리뷰가 잡는다). */\n readBy: string\n}\n\nexport const PROPERTY_EFFECTS: PropertyEffect[] = [\n {\n id: 'speed',\n axes: ['equipment'],\n effect: 'twin.propeffect.speed',\n readBy: 'headless-twin/server/engine/travel-estimator.ts (buildTravelEstimator)'\n },\n {\n id: 'perBaseUnit',\n axes: ['materialDefinitions'],\n effect: 'twin.propeffect.perBaseUnit',\n readBy: '@operato/twin-kernel conversionFactorOf'\n },\n /*\n * ── 에너지 속성 (2026-08-18) ──────────────────────────────────────────────\n *\n * 이 값들은 **오래전부터 읽히고 있었다** — 커널이 부하·피크·배터리를 그것으로 계산하고, 호스트가\n * 금액을 그것으로 만든다. 그런데 등록부에 없어서 화면은 그 값들에 대해 「표시만 됩니다」라고 말했다.\n * 조용한 거짓말이다: 사용자는 계약전력을 넣고도 「이 값은 아무것도 바꾸지 않는다」는 문장을 읽었다.\n *\n * 효과를 새로 만든 것이 아니라 **있는 효과를 드러낸다.** `readBy` 는 실제로 읽는 자리를 가리킨다.\n */\n {\n id: 'contract.kW',\n axes: ['locations'],\n effect: 'twin.propeffect.contractKW',\n readBy: '@operato/twin-kernel EmsKernel.declaredContractKW (계약 대비 판정·초과 주의)'\n },\n {\n id: 'power.ratedKW',\n axes: ['equipment'],\n effect: 'twin.propeffect.ratedKW',\n readBy: '@operato/twin-kernel EmsKernel.deriveEquipmentLoad (가동 중 부하)'\n },\n {\n id: 'power.standbyKW',\n axes: ['equipment'],\n effect: 'twin.propeffect.standbyKW',\n readBy: '@operato/twin-kernel EmsKernel.deriveEquipmentLoad (대기 중 부하)'\n },\n {\n id: 'storage.capacityKWh',\n axes: ['equipment'],\n effect: 'twin.propeffect.capacityKWh',\n readBy: '@operato/twin-kernel EmsKernel (축전지 방전 가능량)'\n },\n {\n id: 'dispatch.dischargeAboveKW',\n axes: ['equipment'],\n effect: 'twin.propeffect.dischargeAboveKW',\n readBy: '@operato/twin-kernel EmsKernel (방전 시작 문턱)'\n },\n {\n id: 'dispatch.maxDischargeKW',\n axes: ['equipment'],\n effect: 'twin.propeffect.maxDischargeKW',\n readBy: '@operato/twin-kernel EmsKernel (인버터 방전 상한)'\n },\n {\n id: 'dispatch.reserveSoc',\n axes: ['equipment'],\n effect: 'twin.propeffect.reserveSoc',\n readBy: '@operato/twin-kernel EmsKernel (이 아래로는 방전하지 않는다)'\n },\n {\n id: 'storage.initialSoc',\n axes: ['equipment'],\n effect: 'twin.propeffect.initialSoc',\n readBy: '@operato/twin-kernel EmsKernel (시작 충전율 — 예측·what-if 의 씨앗)'\n },\n {\n id: 'generation.ratedKW',\n axes: ['equipment'],\n effect: 'twin.propeffect.genRatedKW',\n readBy: '@operato/twin-kernel EmsKernel.deriveGeneration (정격 발전)'\n },\n {\n id: 'generation.dailyProfile',\n axes: ['equipment'],\n effect: 'twin.propeffect.genDailyProfile',\n readBy: '@operato/twin-kernel generationFractionAt (시각별 발전 비율)'\n },\n /* 요금은 커널이 읽지 않는다 — 금액은 저널을 접는 호스트가 만든다. readBy 가 그 사실을 말한다. */\n {\n id: 'tariff.demandChargePerKW',\n axes: ['locations'],\n effect: 'twin.propeffect.demandChargePerKW',\n readBy: 'headless-twin/server/engine/kpi-query.ts (수요요금 계산)'\n },\n {\n id: 'tariff.energyChargePerKWh',\n axes: ['locations'],\n effect: 'twin.propeffect.energyChargePerKWh',\n readBy: 'headless-twin/server/engine/kpi-query.ts (전력량 요금 계산)'\n },\n /* 판정 기준 — 값이 경보의 유무를 바꾼다(가장 눈에 보이는 효과다). */\n {\n id: 'attention.congestionRatio',\n axes: ['locations'],\n effect: 'twin.propeffect.congestionRatio',\n readBy: '@operato/twin-kernel deriveAttentions (병목 판정 기준)'\n },\n {\n id: 'attention.scrapRate',\n axes: ['equipment'],\n effect: 'twin.propeffect.scrapRate',\n readBy: '@operato/twin-kernel deriveAttentions (불량 과다 판정 기준)'\n },\n {\n id: 'attention.scrapMinSamples',\n axes: ['equipment'],\n effect: 'twin.propeffect.scrapMinSamples',\n readBy: '@operato/twin-kernel deriveAttentions (불량률을 재기 시작할 표본 수)'\n },\n /*\n * ── 공정 소요시간 (2026-08-18) ────────────────────────────────────────────\n *\n * 실측: 운영 중인 트윈 35개 중 **창고·야드는 공정 명세 행이 0개**였다 — 그 트윈들의 시간은 전부 커널\n * 상수에서 왔고, 예측은 「같은 조건에서의 상대 비교」에 머물렀다(절대 시각 주장 불가). 이 선언이\n * 그 사다리를 한 칸 올린다: 실측이 없어도 **현장이 정한 시간**으로 굴러간다.\n *\n * 실측(이력에서 배운 값)은 여전히 이 선언을 이긴다 — 화면이 그 사실을 함께 말한다.\n */\n {\n id: 'operation.duration',\n axes: ['operations'],\n effect: 'twin.propeffect.operationDuration',\n readBy: '@operato/twin-kernel FlowEngine.durationOf ← declareDurations (engine/local-declarations.ts 가 실어 보낸다)'\n },\n {\n id: 'tariff.currency',\n axes: ['locations'],\n effect: 'twin.propeffect.currency',\n readBy: 'headless-twin/server/engine/kpi-query.ts (금액 표기 통화)'\n }\n]\n\n/**\n * 이 축의 속성 효과 — `속성 id → i18n 키`.\n *\n * 없는 속성은 **여기 없다**(빈 값으로 채우지 않는다) — 화면이 \"등록된 효과가 없다\" 를 스스로 말한다.\n */\nexport function propertyEffectsOf(axis: string): Record<string, string> {\n const out: Record<string, string> = {}\n for (const e of PROPERTY_EFFECTS) if (e.axes.includes(axis)) out[e.id] = e.effect\n return out\n}\n\n/** 등록부의 축 이름 → 선언이 겨누는 대상 이름. 대상이 아닌 축(자재 정의 등)은 여기 없다. */\nconst AXIS_TO_TARGET: Record<string, string> = { locations: 'location', equipment: 'equipment', operations: 'operation' }\n\n/**\n * 이 속성을 **어느 대상에 얹을 수 있나** — 등록부의 축이 그대로 답한다.\n *\n * ── 왜 필요한가 (2026-08-18) ────────────────────────────────────────────────\n * 지게차에 계약전력을 선언할 수 있었다. 문은 받아 주고 화면은 「넣었습니다」라고 말하는데, 그 값을 읽는\n * 코드는 자리(수전)만 본다 — 아무 일도 일어나지 않고 그 사실이 어디에도 나오지 않았다. 효과가 어느 축의\n * 것인지는 이미 등록부가 알고 있으므로, 그 지식을 검사에 쓴다(두 곳에 적지 않는다).\n *\n * 등록부에 없는 속성은 **비어 있는 목록이 아니라 `undefined`** 다: 「어디에도 못 얹는다」와 「우리가\n * 제한을 모른다」는 다르다(후자는 받아 준다 — 속성은 열려 있다는 규약을 지킨다).\n */\nexport function declarationTargetsOf(propertyId: string): string[] | undefined {\n const e = PROPERTY_EFFECTS.find(x => x.id === propertyId)\n if (!e) return undefined\n const targets = e.axes.map(a => AXIS_TO_TARGET[a]).filter((t): t is string => !!t)\n return targets.length ? targets : undefined\n}\n"]}
@@ -11,5 +11,5 @@ export declare function parseRuntimeKey(key: string): {
11
11
  domainId: string;
12
12
  instanceId: string;
13
13
  };
14
- /** 이 키가 이 테넌트의 것인가 — 레지스트리를 훑을 때 도메인으로 걸러낸다. */
14
+ /** 이 키가 이 테넌트의 것인가 — 레지스트리를 조회할 때 도메인으로 걸러낸다. */
15
15
  export declare function isOfDomain(key: string, domainId: string): boolean;
@@ -57,7 +57,7 @@ function parseRuntimeKey(key) {
57
57
  throw new Error(`parseRuntimeKey: "${key}" is not a runtime key`);
58
58
  return { domainId: key.slice(0, at), instanceId: key.slice(at + 1) };
59
59
  }
60
- /** 이 키가 이 테넌트의 것인가 — 레지스트리를 훑을 때 도메인으로 걸러낸다. */
60
+ /** 이 키가 이 테넌트의 것인가 — 레지스트리를 조회할 때 도메인으로 걸러낸다. */
61
61
  function isOfDomain(key, domainId) {
62
62
  return !!domainId && key.startsWith(`${domainId}${exports.RUNTIME_KEY_SEP}`);
63
63
  }
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-key.js","sourceRoot":"","sources":["../../server/engine/runtime-key.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;;;AAUH,gCAOC;AAGD,0CAIC;AAGD,gCAEC;AA3BD,kDAAkD;AACrC,QAAA,eAAe,GAAG,GAAG,CAAA;AAElC;;;;GAIG;AACH,SAAgB,UAAU,CAAC,QAAgB,EAAE,UAAkB;IAC7D,IAAI,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,8EAA8E,CAAC,CAAA;IAC9G,IAAI,CAAC,UAAU;QAAE,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAA;IACtE,IAAI,QAAQ,CAAC,QAAQ,CAAC,uBAAe,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CAAC,0CAA0C,uBAAe,kCAAkC,CAAC,CAAA;IAC9G,CAAC;IACD,OAAO,GAAG,QAAQ,GAAG,uBAAe,GAAG,UAAU,EAAE,CAAA;AACrD,CAAC;AAED,iDAAiD;AACjD,SAAgB,eAAe,CAAC,GAAW;IACzC,MAAM,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC,uBAAe,CAAC,CAAA;IACvC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,GAAG,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,qBAAqB,GAAG,wBAAwB,CAAC,CAAA;IACvG,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAA;AACtE,CAAC;AAED,gDAAgD;AAChD,SAAgB,UAAU,CAAC,GAAW,EAAE,QAAgB;IACtD,OAAO,CAAC,CAAC,QAAQ,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,QAAQ,GAAG,uBAAe,EAAE,CAAC,CAAA;AACtE,CAAC","sourcesContent":["/*\n * 런타임 레지스트리 키 — **트윈의 정체성은 `(도메인, 인스턴스 id)` 다.** 순수.\n *\n * ── 왜 있나 (2026-08-06 테넌트 격리 전수 확인) ───────────────────────────────\n * `TwinEngine.instances` 와 `recovered` 가 **`instanceId` 하나로** 키를 잡고 있었다. 그런데 DB 유일성은\n * `[domain, instanceId]` 이고(`TwinInstance` 인덱스) 트윈의 정체성도 그쪽이다. 즉 런타임 맵만 **전역\n * 이름 공간**이었다.\n *\n * **충돌은 이론이 아니다.** 레퍼런스 경로가 `instanceId` 로 **레퍼런스 소스 이름**을 쓴다\n * (`sap-ewm-1710`). 벤더 시스템 이름은 테넌트가 **공유하는 어휘**라, 두 테넌트가 같은 시스템에\n * 연결하면 같은 id 가 된다 — 드문 사고가 아니라 예상되는 경우다.\n *\n * 그래서 무엇이 어긋났나:\n * · `startLive` 의 조기 반환이 A 의 런타임을 B 에게 돌려주고 `ingestLive` 는 도메인을 확인하지 않아\n * **B 의 라이브 이벤트가 A 의 커널에 적용**될 수 있었다.\n * · `recovered` 가 겹치면 웜스타트 씨앗이 **남의 스냅샷**이 되어 재고가 섞인다.\n * · `start`/`stop` 이 남의 런타임을 집으므로 B 는 \"기동됨\" 을 받고도 이후 전부 거부됐다.\n *\n * 이 부류는 **이미 한 번 물었다** — `twin-lifecycle-mutation.ts` 주석이 증언한다: \"과거엔\n * `Object.keys(TwinEngine.instances)` 로 전역 반환 → 다른 도메인 트윈이 새던 버그.\" 그 자리만 고쳤다.\n * 그래서 이번에는 가드를 더 붙이지 않고 **키를 고친다** — 저수준 메서드가 `domainId` 를 받게 되는 것이\n * 요점이다. \"누구의 트윈인가\" 를 말하지 않고는 만질 수 없다.\n *\n * ── 구분자 규약 ─────────────────────────────────────────────────────────────\n * `${domainId}:${instanceId}` — 이 저장소가 이미 쓰는 모양이다(`structureRevCache`·`measuredCache`).\n * 새 모양을 만들지 않는다. 파싱은 **첫 구분자**에서 자르므로 `instanceId` 에 `:` 가 들어 있어도\n * 안전하다(레퍼런스 소스 이름에 들어갈 수 있다). 대신 `domainId` 에 구분자가 있으면 키가 모호해지므로\n * **그때는 던진다** — 조용히 잘못된 키를 만들면 그게 곧 이 결함의 재발이다.\n */\n\n/** 도메인과 인스턴스 id 를 잇는 문자 — 저장소의 기존 캐시 키와 같은 모양. */\nexport const RUNTIME_KEY_SEP = ':'\n\n/**\n * 런타임 레지스트리 키.\n *\n * @throws 도메인 id 가 비었거나 구분자를 담고 있으면(키가 모호해진다). 인스턴스 id 가 비었으면.\n */\nexport function runtimeKey(domainId: string, instanceId: string): string {\n if (!domainId) throw new Error('runtimeKey: domainId is required — a twin runtime always belongs to a tenant')\n if (!instanceId) throw new Error('runtimeKey: instanceId is required')\n if (domainId.includes(RUNTIME_KEY_SEP)) {\n throw new Error(`runtimeKey: domainId must not contain \"${RUNTIME_KEY_SEP}\" (would make the key ambiguous)`)\n }\n return `${domainId}${RUNTIME_KEY_SEP}${instanceId}`\n}\n\n/** 키를 되돌린다 — 첫 구분자에서 자른다(인스턴스 id 안의 구분자는 보존). */\nexport function parseRuntimeKey(key: string): { domainId: string; instanceId: string } {\n const at = key.indexOf(RUNTIME_KEY_SEP)\n if (at <= 0 || at === key.length - 1) throw new Error(`parseRuntimeKey: \"${key}\" is not a runtime key`)\n return { domainId: key.slice(0, at), instanceId: key.slice(at + 1) }\n}\n\n/** 이 키가 이 테넌트의 것인가 — 레지스트리를 훑을 때 도메인으로 걸러낸다. */\nexport function isOfDomain(key: string, domainId: string): boolean {\n return !!domainId && key.startsWith(`${domainId}${RUNTIME_KEY_SEP}`)\n}\n"]}
1
+ {"version":3,"file":"runtime-key.js","sourceRoot":"","sources":["../../server/engine/runtime-key.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;;;AAUH,gCAOC;AAGD,0CAIC;AAGD,gCAEC;AA3BD,kDAAkD;AACrC,QAAA,eAAe,GAAG,GAAG,CAAA;AAElC;;;;GAIG;AACH,SAAgB,UAAU,CAAC,QAAgB,EAAE,UAAkB;IAC7D,IAAI,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,8EAA8E,CAAC,CAAA;IAC9G,IAAI,CAAC,UAAU;QAAE,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAA;IACtE,IAAI,QAAQ,CAAC,QAAQ,CAAC,uBAAe,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CAAC,0CAA0C,uBAAe,kCAAkC,CAAC,CAAA;IAC9G,CAAC;IACD,OAAO,GAAG,QAAQ,GAAG,uBAAe,GAAG,UAAU,EAAE,CAAA;AACrD,CAAC;AAED,iDAAiD;AACjD,SAAgB,eAAe,CAAC,GAAW;IACzC,MAAM,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC,uBAAe,CAAC,CAAA;IACvC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,GAAG,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,qBAAqB,GAAG,wBAAwB,CAAC,CAAA;IACvG,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAA;AACtE,CAAC;AAED,iDAAiD;AACjD,SAAgB,UAAU,CAAC,GAAW,EAAE,QAAgB;IACtD,OAAO,CAAC,CAAC,QAAQ,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,QAAQ,GAAG,uBAAe,EAAE,CAAC,CAAA;AACtE,CAAC","sourcesContent":["/*\n * 런타임 레지스트리 키 — **트윈의 정체성은 `(도메인, 인스턴스 id)` 다.** 순수.\n *\n * ── 왜 있나 (2026-08-06 테넌트 격리 전수 확인) ───────────────────────────────\n * `TwinEngine.instances` 와 `recovered` 가 **`instanceId` 하나로** 키를 잡고 있었다. 그런데 DB 유일성은\n * `[domain, instanceId]` 이고(`TwinInstance` 인덱스) 트윈의 정체성도 그쪽이다. 즉 런타임 맵만 **전역\n * 이름 공간**이었다.\n *\n * **충돌은 이론이 아니다.** 레퍼런스 경로가 `instanceId` 로 **레퍼런스 소스 이름**을 쓴다\n * (`sap-ewm-1710`). 벤더 시스템 이름은 테넌트가 **공유하는 어휘**라, 두 테넌트가 같은 시스템에\n * 연결하면 같은 id 가 된다 — 드문 사고가 아니라 예상되는 경우다.\n *\n * 그래서 무엇이 어긋났나:\n * · `startLive` 의 조기 반환이 A 의 런타임을 B 에게 돌려주고 `ingestLive` 는 도메인을 확인하지 않아\n * **B 의 라이브 이벤트가 A 의 커널에 적용**될 수 있었다.\n * · `recovered` 가 겹치면 웜스타트 씨앗이 **남의 스냅샷**이 되어 재고가 섞인다.\n * · `start`/`stop` 이 남의 런타임을 집으므로 B 는 \"기동됨\" 을 받고도 이후 전부 거부됐다.\n *\n * 이 부류는 **이미 한 번 물었다** — `twin-lifecycle-mutation.ts` 주석이 증언한다: \"과거엔\n * `Object.keys(TwinEngine.instances)` 로 전역 반환 → 다른 도메인 트윈이 새던 버그.\" 그 자리만 고쳤다.\n * 그래서 이번에는 가드를 더 붙이지 않고 **키를 고친다** — 저수준 메서드가 `domainId` 를 받게 되는 것이\n * 요점이다. \"누구의 트윈인가\" 를 말하지 않고는 만질 수 없다.\n *\n * ── 구분자 규약 ─────────────────────────────────────────────────────────────\n * `${domainId}:${instanceId}` — 이 저장소가 이미 쓰는 모양이다(`structureRevCache`·`measuredCache`).\n * 새 모양을 만들지 않는다. 파싱은 **첫 구분자**에서 자르므로 `instanceId` 에 `:` 가 들어 있어도\n * 안전하다(레퍼런스 소스 이름에 들어갈 수 있다). 대신 `domainId` 에 구분자가 있으면 키가 모호해지므로\n * **그때는 던진다** — 조용히 잘못된 키를 만들면 그게 곧 이 결함의 재발이다.\n */\n\n/** 도메인과 인스턴스 id 를 잇는 문자 — 저장소의 기존 캐시 키와 같은 모양. */\nexport const RUNTIME_KEY_SEP = ':'\n\n/**\n * 런타임 레지스트리 키.\n *\n * @throws 도메인 id 가 비었거나 구분자를 담고 있으면(키가 모호해진다). 인스턴스 id 가 비었으면.\n */\nexport function runtimeKey(domainId: string, instanceId: string): string {\n if (!domainId) throw new Error('runtimeKey: domainId is required — a twin runtime always belongs to a tenant')\n if (!instanceId) throw new Error('runtimeKey: instanceId is required')\n if (domainId.includes(RUNTIME_KEY_SEP)) {\n throw new Error(`runtimeKey: domainId must not contain \"${RUNTIME_KEY_SEP}\" (would make the key ambiguous)`)\n }\n return `${domainId}${RUNTIME_KEY_SEP}${instanceId}`\n}\n\n/** 키를 되돌린다 — 첫 구분자에서 자른다(인스턴스 id 안의 구분자는 보존). */\nexport function parseRuntimeKey(key: string): { domainId: string; instanceId: string } {\n const at = key.indexOf(RUNTIME_KEY_SEP)\n if (at <= 0 || at === key.length - 1) throw new Error(`parseRuntimeKey: \"${key}\" is not a runtime key`)\n return { domainId: key.slice(0, at), instanceId: key.slice(at + 1) }\n}\n\n/** 이 키가 이 테넌트의 것인가 — 레지스트리를 조회할 때 도메인으로 걸러낸다. */\nexport function isOfDomain(key: string, domainId: string): boolean {\n return !!domainId && key.startsWith(`${domainId}${RUNTIME_KEY_SEP}`)\n}\n"]}
@@ -3,9 +3,9 @@
3
3
  * 시뮬 명세 자기보고 — **"이 예측으로 무엇을 말할 수 있나" 를 예측과 함께 내보낸다.**
4
4
  *
5
5
  * 배경(operato-twin design/plans/simulation-spec.md): 공정 소요·수율이 커널 소스의 상수였다. 이제
6
- * 마스터가 명세를 실어 오면 커널이 그것으로 굴리고, `specCoverage()` 가 무엇을 선언값으로 무엇을
7
- * 기본값으로 썼는지 보고한다. 그 보고를 **예측 응답에 붙이지 않으면** 받는 쪽은 상수로 굴린 숫자와
8
- * 현장 명세로 굴린 숫자를 구별할 수 없다 — 똑같이 "p50 168" 로 보인다.
6
+ * 마스터가 명세를 실어 오면 커널이 그것으로 실행하고, `specCoverage()` 가 무엇을 선언값으로 무엇을
7
+ * 기본값으로 썼는지 보고한다. 그 보고를 **예측 응답에 붙이지 않으면** 받는 쪽은 상수로 계산한 숫자와
8
+ * 현장 명세로 계산한 숫자를 구별할 수 없다 — 똑같이 "p50 168" 로 보인다.
9
9
  *
10
10
  * 언어 중립: 여기서는 **코드와 수치만** 낸다(`qualification` + 카운트). 사람이 읽는 문장은 표현
11
11
  * 계층(클라 i18next / AI 답변)이 만든다 — attention 과 같은 규율.
@@ -1 +1 @@
1
- {"version":3,"file":"spec-coverage.js","sourceRoot":"","sources":["../../server/engine/spec-coverage.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;AA+BH,oDA6BC;AAOD,4CAOC;AA/CD;;;GAGG;AACH,SAAgB,oBAAoB,CAAC,GAKpC;IACC,MAAM,GAAG,GAAG,GAAG,EAAE,UAAU,CAAA;IAC3B,IAAI,CAAC,GAAG,EAAE,MAAM;QAAE,OAAO,SAAS,CAAA;IAClC,MAAM,UAAU,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC/B,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACxD,UAAU,EAAE,CAAC,CAAC,UAAU,IAAI,EAAE;KAC/B,CAAC,CAAC,CAAA;IACH,MAAM,iBAAiB,GAAG,GAAG,CAAC,iBAAiB,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,UAAU,CAAC,CAAC,MAAM,CAAA;IAC3G,MAAM,iBAAiB,GAAG,GAAG,CAAC,iBAAiB,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,UAAU,CAAC,CAAC,MAAM,CAAA;IAC3G,MAAM,gBAAgB,GAAG,GAAG,CAAC,gBAAgB,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,MAAM,CAAA;IACxG,MAAM,gBAAgB,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC,WAAW,KAAK,UAAU,CAAC,CAAC,MAAM,CAAA;IACrG,iGAAiG;IACjG,MAAM,QAAQ,GAAG,iBAAiB,GAAG,iBAAiB,CAAA;IACtD,MAAM,aAAa,GACjB,QAAQ,KAAK,CAAC;QACZ,CAAC,CAAC,UAAU;QACZ,CAAC,CAAC,gBAAgB,GAAG,CAAC;YACpB,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,iBAAiB,KAAK,CAAC;gBACvB,CAAC,CAAC,YAAY;gBACd,CAAC,CAAC,kBAAkB,CAAA;IAC5B,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,aAAa,EAAE,CAAA;AAChH,CAAC;AAED;;;;GAIG;AACH,SAAgB,gBAAgB,CAAC,MAAW;IAC1C,IAAI,OAAO,MAAM,EAAE,YAAY,KAAK,UAAU;QAAE,OAAO,SAAS,CAAA;IAChE,IAAI,CAAC;QACH,OAAO,oBAAoB,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC,CAAA;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC","sourcesContent":["/*\n * 시뮬 명세 자기보고 — **\"이 예측으로 무엇을 말할 수 있나\" 를 예측과 함께 내보낸다.**\n *\n * 배경(operato-twin design/plans/simulation-spec.md): 공정 소요·수율이 커널 소스의 상수였다. 이제\n * 마스터가 명세를 실어 오면 커널이 그것으로 굴리고, `specCoverage()` 가 무엇을 선언값으로 무엇을\n * 기본값으로 썼는지 보고한다. 그 보고를 **예측 응답에 붙이지 않으면** 받는 쪽은 상수로 굴린 숫자와\n * 현장 명세로 굴린 숫자를 구별할 수 없다 — 똑같이 \"p50 168\" 로 보인다.\n *\n * 언어 중립: 여기서는 **코드와 수치만** 낸다(`qualification` + 카운트). 사람이 읽는 문장은 표현\n * 계층(클라 i18next / AI 답변)이 만든다 — attention 과 같은 규율.\n */\n\n/** 예측 자격 — 선언된 명세가 얼마나 덮는지에 따라 이 예측으로 정당하게 말할 수 있는 범위가 달라진다. */\nexport type ForecastQualification =\n /** 모든 소요가 커널 기본 상수 → **상대 비교만**(\"자원 2대 더 넣으면 나아지나\"). 절대 시각 주장 불가. */\n | 'relative'\n /** 일부만 선언·실측 → 그 구간은 접지, 나머지는 상수. 혼합. */\n | 'partial'\n /** 소요 전부 선언 → 절대 예측 시도 가능. 단 **정확도는 이력이 확인한다**(선언 != 검증). */\n | 'absolute-capable'\n /**\n * 소요 전부가 **이 현장의 이력에서 온 값**(추정기) → 가장 강한 근거. 선언은 사람이 적은 값이고\n * 이것은 실제로 일어난 값이다. 그래도 \"정확하다\" 가 아니라 \"관측에 접지됐다\" 는 뜻이다.\n */\n | 'calibrated'\n\nexport interface SpecCoverageReport {\n operations: { kind: string; duration: 'measured' | 'declared' | 'default'; variability?: string; parameters: string[] }[]\n /** 이력·계산에서 온 소요(추정기) — 선언값보다 강한 근거. */\n measuredDurations: number\n declaredDurations: number\n defaultDurations: number\n /** 변동(분포)을 가진 작업 종류 수. **0 이면 모든 소요가 고정값**이라 대기·병목이 과소평가된다. */\n varyingDurations: number\n qualification: ForecastQualification\n}\n\n/**\n * 명세 자격 판정(순수) — 커널 보고를 받아 분류한다. 커널을 모르므로 테스트가 쉽다.\n * 관측된 작업이 하나도 없으면(`operations` 빈 배열) 판정할 근거가 없으므로 **undefined**(모른다).\n */\nexport function classifySpecCoverage(cov: {\n operations?: { kind: string; duration: 'measured' | 'declared' | 'default'; variability?: string; parameters?: string[] }[]\n measuredDurations?: number\n declaredDurations?: number\n defaultDurations?: number\n}): SpecCoverageReport | undefined {\n const ops = cov?.operations\n if (!ops?.length) return undefined\n const operations = ops.map(o => ({\n kind: o.kind,\n duration: o.duration,\n ...(o.variability ? { variability: o.variability } : {}),\n parameters: o.parameters ?? []\n }))\n const measuredDurations = cov.measuredDurations ?? operations.filter(o => o.duration === 'measured').length\n const declaredDurations = cov.declaredDurations ?? operations.filter(o => o.duration === 'declared').length\n const defaultDurations = cov.defaultDurations ?? operations.filter(o => o.duration === 'default').length\n const varyingDurations = operations.filter(o => o.variability && o.variability !== 'constant').length\n /* 사다리: 전부 이력 → calibrated · 상수 없음 → absolute-capable · 접지가 하나도 없음 → relative · 나머지 → partial. */\n const grounded = measuredDurations + declaredDurations\n const qualification: ForecastQualification =\n grounded === 0\n ? 'relative'\n : defaultDurations > 0\n ? 'partial'\n : declaredDurations === 0\n ? 'calibrated'\n : 'absolute-capable'\n return { operations, measuredDurations, declaredDurations, defaultDurations, varyingDurations, qualification }\n}\n\n/**\n * 커널에서 읽는다 — **아직 이 API 가 없는 커널(발행 이전)이면 undefined.**\n * 그때 `undefined` 는 \"명세가 없다\" 가 아니라 \"모른다\" 다. 소비처가 그 둘을 섞지 않도록 주석을 남긴다:\n * 없는 것을 \"전부 기본값\" 이라고 단정하지 않는다(단정하려면 커널이 말해 줘야 한다).\n */\nexport function readSpecCoverage(kernel: any): SpecCoverageReport | undefined {\n if (typeof kernel?.specCoverage !== 'function') return undefined\n try {\n return classifySpecCoverage(kernel.specCoverage())\n } catch {\n return undefined\n }\n}\n"]}
1
+ {"version":3,"file":"spec-coverage.js","sourceRoot":"","sources":["../../server/engine/spec-coverage.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;AA+BH,oDA6BC;AAOD,4CAOC;AA/CD;;;GAGG;AACH,SAAgB,oBAAoB,CAAC,GAKpC;IACC,MAAM,GAAG,GAAG,GAAG,EAAE,UAAU,CAAA;IAC3B,IAAI,CAAC,GAAG,EAAE,MAAM;QAAE,OAAO,SAAS,CAAA;IAClC,MAAM,UAAU,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC/B,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACxD,UAAU,EAAE,CAAC,CAAC,UAAU,IAAI,EAAE;KAC/B,CAAC,CAAC,CAAA;IACH,MAAM,iBAAiB,GAAG,GAAG,CAAC,iBAAiB,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,UAAU,CAAC,CAAC,MAAM,CAAA;IAC3G,MAAM,iBAAiB,GAAG,GAAG,CAAC,iBAAiB,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,UAAU,CAAC,CAAC,MAAM,CAAA;IAC3G,MAAM,gBAAgB,GAAG,GAAG,CAAC,gBAAgB,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,MAAM,CAAA;IACxG,MAAM,gBAAgB,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC,WAAW,KAAK,UAAU,CAAC,CAAC,MAAM,CAAA;IACrG,iGAAiG;IACjG,MAAM,QAAQ,GAAG,iBAAiB,GAAG,iBAAiB,CAAA;IACtD,MAAM,aAAa,GACjB,QAAQ,KAAK,CAAC;QACZ,CAAC,CAAC,UAAU;QACZ,CAAC,CAAC,gBAAgB,GAAG,CAAC;YACpB,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,iBAAiB,KAAK,CAAC;gBACvB,CAAC,CAAC,YAAY;gBACd,CAAC,CAAC,kBAAkB,CAAA;IAC5B,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,aAAa,EAAE,CAAA;AAChH,CAAC;AAED;;;;GAIG;AACH,SAAgB,gBAAgB,CAAC,MAAW;IAC1C,IAAI,OAAO,MAAM,EAAE,YAAY,KAAK,UAAU;QAAE,OAAO,SAAS,CAAA;IAChE,IAAI,CAAC;QACH,OAAO,oBAAoB,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC,CAAA;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC","sourcesContent":["/*\n * 시뮬 명세 자기보고 — **\"이 예측으로 무엇을 말할 수 있나\" 를 예측과 함께 내보낸다.**\n *\n * 배경(operato-twin design/plans/simulation-spec.md): 공정 소요·수율이 커널 소스의 상수였다. 이제\n * 마스터가 명세를 실어 오면 커널이 그것으로 실행하고, `specCoverage()` 가 무엇을 선언값으로 무엇을\n * 기본값으로 썼는지 보고한다. 그 보고를 **예측 응답에 붙이지 않으면** 받는 쪽은 상수로 계산한 숫자와\n * 현장 명세로 계산한 숫자를 구별할 수 없다 — 똑같이 \"p50 168\" 로 보인다.\n *\n * 언어 중립: 여기서는 **코드와 수치만** 낸다(`qualification` + 카운트). 사람이 읽는 문장은 표현\n * 계층(클라 i18next / AI 답변)이 만든다 — attention 과 같은 규율.\n */\n\n/** 예측 자격 — 선언된 명세가 얼마나 덮는지에 따라 이 예측으로 정당하게 말할 수 있는 범위가 달라진다. */\nexport type ForecastQualification =\n /** 모든 소요가 커널 기본 상수 → **상대 비교만**(\"자원 2대 더 넣으면 나아지나\"). 절대 시각 주장 불가. */\n | 'relative'\n /** 일부만 선언·실측 → 그 구간은 접지, 나머지는 상수. 혼합. */\n | 'partial'\n /** 소요 전부 선언 → 절대 예측 시도 가능. 단 **정확도는 이력이 확인한다**(선언 != 검증). */\n | 'absolute-capable'\n /**\n * 소요 전부가 **이 현장의 이력에서 온 값**(추정기) → 가장 강한 근거. 선언은 사람이 적은 값이고\n * 이것은 실제로 일어난 값이다. 그래도 \"정확하다\" 가 아니라 \"관측에 접지됐다\" 는 뜻이다.\n */\n | 'calibrated'\n\nexport interface SpecCoverageReport {\n operations: { kind: string; duration: 'measured' | 'declared' | 'default'; variability?: string; parameters: string[] }[]\n /** 이력·계산에서 온 소요(추정기) — 선언값보다 강한 근거. */\n measuredDurations: number\n declaredDurations: number\n defaultDurations: number\n /** 변동(분포)을 가진 작업 종류 수. **0 이면 모든 소요가 고정값**이라 대기·병목이 과소평가된다. */\n varyingDurations: number\n qualification: ForecastQualification\n}\n\n/**\n * 명세 자격 판정(순수) — 커널 보고를 받아 분류한다. 커널을 모르므로 테스트가 쉽다.\n * 관측된 작업이 하나도 없으면(`operations` 빈 배열) 판정할 근거가 없으므로 **undefined**(모른다).\n */\nexport function classifySpecCoverage(cov: {\n operations?: { kind: string; duration: 'measured' | 'declared' | 'default'; variability?: string; parameters?: string[] }[]\n measuredDurations?: number\n declaredDurations?: number\n defaultDurations?: number\n}): SpecCoverageReport | undefined {\n const ops = cov?.operations\n if (!ops?.length) return undefined\n const operations = ops.map(o => ({\n kind: o.kind,\n duration: o.duration,\n ...(o.variability ? { variability: o.variability } : {}),\n parameters: o.parameters ?? []\n }))\n const measuredDurations = cov.measuredDurations ?? operations.filter(o => o.duration === 'measured').length\n const declaredDurations = cov.declaredDurations ?? operations.filter(o => o.duration === 'declared').length\n const defaultDurations = cov.defaultDurations ?? operations.filter(o => o.duration === 'default').length\n const varyingDurations = operations.filter(o => o.variability && o.variability !== 'constant').length\n /* 사다리: 전부 이력 → calibrated · 상수 없음 → absolute-capable · 접지가 하나도 없음 → relative · 나머지 → partial. */\n const grounded = measuredDurations + declaredDurations\n const qualification: ForecastQualification =\n grounded === 0\n ? 'relative'\n : defaultDurations > 0\n ? 'partial'\n : declaredDurations === 0\n ? 'calibrated'\n : 'absolute-capable'\n return { operations, measuredDurations, declaredDurations, defaultDurations, varyingDurations, qualification }\n}\n\n/**\n * 커널에서 읽는다 — **아직 이 API 가 없는 커널(발행 이전)이면 undefined.**\n * 그때 `undefined` 는 \"명세가 없다\" 가 아니라 \"모른다\" 다. 소비처가 그 둘을 섞지 않도록 주석을 남긴다:\n * 없는 것을 \"전부 기본값\" 이라고 단정하지 않는다(단정하려면 커널이 말해 줘야 한다).\n */\nexport function readSpecCoverage(kernel: any): SpecCoverageReport | undefined {\n if (typeof kernel?.specCoverage !== 'function') return undefined\n try {\n return classifySpecCoverage(kernel.specCoverage())\n } catch {\n return undefined\n }\n}\n"]}
@@ -1,5 +1,10 @@
1
- /** 상태가 들고 있는 축 — 이 목록 밖의 이름은 고를 수 없다. */
2
- export declare const STATE_AXES: readonly ["locations", "items", "equipment", "persons", "assets", "tasks", "orders", "attentions", "acked"];
1
+ /**
2
+ * 상태가 들고 있는 목록 밖의 이름은 고를 없다.
3
+ *
4
+ * `energy` 는 에너지 트윈만 채운다(계량 지점·수요 구간·피크). `ALWAYS` 에 두지 않는다: 마감 구간을
5
+ * 하루치 들고 있어 작지 않고, 그것이 필요한 화면이 스스로 이름을 말하는 편이 맞다.
6
+ */
7
+ export declare const STATE_AXES: readonly ["locations", "items", "equipment", "persons", "assets", "tasks", "orders", "attentions", "acked", "energy"];
3
8
  export type StateAxis = (typeof STATE_AXES)[number];
4
9
  export interface PickedAxes {
5
10
  /** 고른 축만 남은 상태. `axes` 가 비었으면 원본 그대로(자르지 않는다). */
@@ -21,10 +21,15 @@
21
21
  Object.defineProperty(exports, "__esModule", { value: true });
22
22
  exports.STATE_AXES = void 0;
23
23
  exports.pickAxes = pickAxes;
24
- /** 상태가 들고 있는 축 — 이 목록 밖의 이름은 고를 수 없다. */
25
- exports.STATE_AXES = ['locations', 'items', 'equipment', 'persons', 'assets', 'tasks', 'orders', 'attentions', 'acked'];
24
+ /**
25
+ * 상태가 들고 있는 목록 밖의 이름은 고를 수 없다.
26
+ *
27
+ * `energy` 는 에너지 트윈만 채운다(계량 지점·수요 구간·피크). `ALWAYS` 에 두지 않는다: 마감 구간을
28
+ * 하루치 들고 있어 작지 않고, 그것이 필요한 화면이 스스로 이름을 말하는 편이 맞다.
29
+ */
30
+ exports.STATE_AXES = ['locations', 'items', 'equipment', 'persons', 'assets', 'tasks', 'orders', 'attentions', 'acked', 'energy'];
26
31
  /** 축과 무관하게 늘 남는 것 — 이것이 없으면 무엇을 받았는지 알 수 없다. */
27
- const ALWAYS = ['revision', 'nowTime', 'simClockMs', 'corrections'];
32
+ const ALWAYS = ['revision', 'nowTime', 'simClockMs', 'corrections', 'unhandled'];
28
33
  /**
29
34
  * 요청한 축만 남긴다.
30
35
  *
@@ -1 +1 @@
1
- {"version":3,"file":"state-axes.js","sourceRoot":"","sources":["../../server/engine/state-axes.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;GAkBG;;;AAsBH,4BAcC;AAlCD,yCAAyC;AAC5B,QAAA,UAAU,GAAG,CAAC,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,CAAU,CAAA;AAGrI,gDAAgD;AAChD,MAAM,MAAM,GAAG,CAAC,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,CAAU,CAAA;AAS5E;;;;;GAKG;AACH,SAAgB,QAAQ,CAAC,KAAU,EAAE,IAA0C;IAC7E,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,CAAA;IACtE,IAAI,CAAC,IAAI,EAAE,MAAM;QAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,CAAA;IAEhD,MAAM,KAAK,GAAG,IAAI,GAAG,CAAS,kBAA+B,CAAC,CAAA;IAC9D,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;IAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;IAC/C,uDAAuD;IACvD,IAAI,CAAC,MAAM,CAAC,MAAM;QAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,CAAA;IAE7C,MAAM,GAAG,GAAQ,EAAE,CAAA;IACnB,KAAK,MAAM,CAAC,IAAI,MAAM;QAAE,IAAI,CAAC,IAAI,KAAK;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;IACzD,KAAK,MAAM,CAAC,IAAI,MAAM;QAAE,IAAI,CAAC,IAAI,KAAK;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;IACzD,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,CAAA;AAChC,CAAC","sourcesContent":["/*\n * 상태 축 고르기 — **순수**. \"요청한 축만 남긴다.\"\n *\n * ── 왜 있나 (2026-08-05) ────────────────────────────────────────────────────\n * 지도는 구독으로 \"바뀌었다\" 신호를 받고 상태를 다시 읽는다. 그 신호는 의도적으로 가볍고(`{kind,\n * revision}`) 리프레시도 250ms 로 합쳐진다 — 거기까지는 설계대로다. 문제는 **리프레시가 통째로**\n * 라는 것이다: 실 저널로 재 보니 트윈 하나가 최대 710KB 이고, 같은 공간에 셋이면 한 번에 약 2MB 다.\n *\n * 그런데 지도가 실제로 쓰는 것은 `locations`·`attentions`·`nowTime` **셋뿐**이다(코드 확인). 물품\n * 2,278건·작업·오더를 받아 놓고 쓰지 않는다. 축을 골라 받으면 트윈에 따라 1.7~15배 줄어든다\n * (물품이 많은 트윈은 15배, 신호가 많은 트윈은 2배 — 신호가 큰 쪽은 그것이 다음 과제다).\n *\n * ── 화면 이름을 API 에 넣지 않는다 ──────────────────────────────────────────\n * `twinMapState` 같은 화면 전용 질의를 만들지 않는다. 호출부가 **필요한 축을 말하고** 서버가 자른다 —\n * 새 화면이 생길 때마다 질의가 늘지 않고, 어휘도 화면 이름이 아니라 상태의 축이다(무방언).\n *\n * 모르는 축 이름은 **조용히 버리지 않고 무시하되 그 사실을 돌려준다** — 오타 하나로 화면이 빈\n * 채로 뜨는 것이 가장 나쁘다.\n */\n\n/** 상태가 들고 있는 축 — 이 목록 밖의 이름은 고를 수 없다. */\nexport const STATE_AXES = ['locations', 'items', 'equipment', 'persons', 'assets', 'tasks', 'orders', 'attentions', 'acked'] as const\nexport type StateAxis = (typeof STATE_AXES)[number]\n\n/** 축과 무관하게 늘 남는 것 — 이것이 없으면 무엇을 받았는지 알 수 없다. */\nconst ALWAYS = ['revision', 'nowTime', 'simClockMs', 'corrections'] as const\n\nexport interface PickedAxes {\n /** 고른 축만 남은 상태. `axes` 가 비었으면 원본 그대로(자르지 않는다). */\n state: any\n /** 상태 축이 아닌 이름 — 호출부가 오타를 알 수 있게 돌려준다. */\n unknown: string[]\n}\n\n/**\n * 요청한 축만 남긴다.\n *\n * `axes` 가 비어 있으면 **자르지 않는다** — 기본값이 \"전부\" 여야 기존 호출부가 그대로 동작한다\n * (자르는 쪽이 기본이면 축을 말하지 않은 화면이 조용히 비어 버린다).\n */\nexport function pickAxes(state: any, axes: readonly string[] | undefined | null): PickedAxes {\n if (!state || typeof state !== 'object') return { state, unknown: [] }\n if (!axes?.length) return { state, unknown: [] }\n\n const known = new Set<string>(STATE_AXES as readonly string[])\n const wanted = axes.filter(a => known.has(a))\n const unknown = axes.filter(a => !known.has(a))\n /* 아는 축이 하나도 없으면 자르지 않는다 — 전부 오타인 경우에 빈 화면을 만들지 않는다. */\n if (!wanted.length) return { state, unknown }\n\n const out: any = {}\n for (const k of ALWAYS) if (k in state) out[k] = state[k]\n for (const a of wanted) if (a in state) out[a] = state[a]\n return { state: out, unknown }\n}\n"]}
1
+ {"version":3,"file":"state-axes.js","sourceRoot":"","sources":["../../server/engine/state-axes.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;GAkBG;;;AA2BH,4BAcC;AAvCD;;;;;GAKG;AACU,QAAA,UAAU,GAAG,CAAC,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,CAAU,CAAA;AAG/I,gDAAgD;AAChD,MAAM,MAAM,GAAG,CAAC,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,WAAW,CAAU,CAAA;AASzF;;;;;GAKG;AACH,SAAgB,QAAQ,CAAC,KAAU,EAAE,IAA0C;IAC7E,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,CAAA;IACtE,IAAI,CAAC,IAAI,EAAE,MAAM;QAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,CAAA;IAEhD,MAAM,KAAK,GAAG,IAAI,GAAG,CAAS,kBAA+B,CAAC,CAAA;IAC9D,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;IAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;IAC/C,uDAAuD;IACvD,IAAI,CAAC,MAAM,CAAC,MAAM;QAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,CAAA;IAE7C,MAAM,GAAG,GAAQ,EAAE,CAAA;IACnB,KAAK,MAAM,CAAC,IAAI,MAAM;QAAE,IAAI,CAAC,IAAI,KAAK;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;IACzD,KAAK,MAAM,CAAC,IAAI,MAAM;QAAE,IAAI,CAAC,IAAI,KAAK;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;IACzD,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,CAAA;AAChC,CAAC","sourcesContent":["/*\n * 상태 축 고르기 — **순수**. \"요청한 축만 남긴다.\"\n *\n * ── 왜 있나 (2026-08-05) ────────────────────────────────────────────────────\n * 지도는 구독으로 \"바뀌었다\" 신호를 받고 상태를 다시 읽는다. 그 신호는 의도적으로 가볍고(`{kind,\n * revision}`) 리프레시도 250ms 로 합쳐진다 — 거기까지는 설계대로다. 문제는 **리프레시가 통째로**\n * 라는 것이다: 실 저널로 재 보니 트윈 하나가 최대 710KB 이고, 같은 공간에 셋이면 한 번에 약 2MB 다.\n *\n * 그런데 지도가 실제로 쓰는 것은 `locations`·`attentions`·`nowTime` **셋뿐**이다(코드 확인). 물품\n * 2,278건·작업·오더를 받아 놓고 쓰지 않는다. 축을 골라 받으면 트윈에 따라 1.7~15배 줄어든다\n * (물품이 많은 트윈은 15배, 신호가 많은 트윈은 2배 — 신호가 큰 쪽은 그것이 다음 과제다).\n *\n * ── 화면 이름을 API 에 넣지 않는다 ──────────────────────────────────────────\n * `twinMapState` 같은 화면 전용 질의를 만들지 않는다. 호출부가 **필요한 축을 말하고** 서버가 자른다 —\n * 새 화면이 생길 때마다 질의가 늘지 않고, 어휘도 화면 이름이 아니라 상태의 축이다(무방언).\n *\n * 모르는 축 이름은 **조용히 버리지 않고 무시하되 그 사실을 돌려준다** — 오타 하나로 화면이 빈\n * 채로 뜨는 것이 가장 나쁘다.\n */\n\n/**\n * 상태가 들고 있는 축 — 이 목록 밖의 이름은 고를 수 없다.\n *\n * `energy` 는 에너지 트윈만 채운다(계량 지점·수요 구간·피크). `ALWAYS` 에 두지 않는다: 마감 구간을\n * 하루치 들고 있어 작지 않고, 그것이 필요한 화면이 스스로 이름을 말하는 편이 맞다.\n */\nexport const STATE_AXES = ['locations', 'items', 'equipment', 'persons', 'assets', 'tasks', 'orders', 'attentions', 'acked', 'energy'] as const\nexport type StateAxis = (typeof STATE_AXES)[number]\n\n/** 축과 무관하게 늘 남는 것 — 이것이 없으면 무엇을 받았는지 알 수 없다. */\nconst ALWAYS = ['revision', 'nowTime', 'simClockMs', 'corrections', 'unhandled'] as const\n\nexport interface PickedAxes {\n /** 고른 축만 남은 상태. `axes` 가 비었으면 원본 그대로(자르지 않는다). */\n state: any\n /** 상태 축이 아닌 이름 — 호출부가 오타를 알 수 있게 돌려준다. */\n unknown: string[]\n}\n\n/**\n * 요청한 축만 남긴다.\n *\n * `axes` 가 비어 있으면 **자르지 않는다** — 기본값이 \"전부\" 여야 기존 호출부가 그대로 동작한다\n * (자르는 쪽이 기본이면 축을 말하지 않은 화면이 조용히 비어 버린다).\n */\nexport function pickAxes(state: any, axes: readonly string[] | undefined | null): PickedAxes {\n if (!state || typeof state !== 'object') return { state, unknown: [] }\n if (!axes?.length) return { state, unknown: [] }\n\n const known = new Set<string>(STATE_AXES as readonly string[])\n const wanted = axes.filter(a => known.has(a))\n const unknown = axes.filter(a => !known.has(a))\n /* 아는 축이 하나도 없으면 자르지 않는다 — 전부 오타인 경우에 빈 화면을 만들지 않는다. */\n if (!wanted.length) return { state, unknown }\n\n const out: any = {}\n for (const k of ALWAYS) if (k in state) out[k] = state[k]\n for (const a of wanted) if (a in state) out[a] = state[a]\n return { state: out, unknown }\n}\n"]}
@@ -3,7 +3,7 @@
3
3
  * 구조 이력의 **차이** — "2번째에 도장 부스가 넷 늘었다" 를 말할 수 있게.
4
4
  *
5
5
  * 리비전은 그 시절 공장을 통째로 담는다(재생하려면 전체 위상이 필요하다). 그런데 사람이 보고 싶은
6
- * 것은 통째가 아니라 **무엇이 달라졌나** 다 — 62KB 짜리 보드 두 개를 나란히 놓고 눈으로 비교할 수는
6
+ * 것은 통째가 아니라 **무엇이 달라졌나** 다 — 62KB 짜리 모델 두 개를 나란히 놓고 눈으로 비교할 수는
7
7
  * 없다. 그 한 줄을 여기서 만든다.
8
8
  *
9
9
  * 순수 함수인 이유: 같은 문장을 화면과 AI 가 함께 써야 한다. 한쪽에서만 계산하면 둘이 다른 말을 한다.