@things-factory/headless-twin 10.0.14 → 10.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (478) hide show
  1. package/dist-server/engine/attention-digest.d.ts +4 -4
  2. package/dist-server/engine/attention-digest.js +7 -7
  3. package/dist-server/engine/attention-digest.js.map +1 -1
  4. package/dist-server/engine/canonical-ingest.d.ts +11 -2
  5. package/dist-server/engine/canonical-ingest.js +137 -8
  6. package/dist-server/engine/canonical-ingest.js.map +1 -1
  7. package/dist-server/engine/command-routing.js +1 -1
  8. package/dist-server/engine/command-routing.js.map +1 -1
  9. package/dist-server/engine/declared-stimulus.js +1 -1
  10. package/dist-server/engine/declared-stimulus.js.map +1 -1
  11. package/dist-server/engine/energy-topology.js +1 -1
  12. package/dist-server/engine/energy-topology.js.map +1 -1
  13. package/dist-server/engine/ingest-dedupe.d.ts +38 -0
  14. package/dist-server/engine/ingest-dedupe.js +145 -0
  15. package/dist-server/engine/ingest-dedupe.js.map +1 -0
  16. package/dist-server/engine/ingest-health.d.ts +124 -9
  17. package/dist-server/engine/ingest-health.js +169 -15
  18. package/dist-server/engine/ingest-health.js.map +1 -1
  19. package/dist-server/engine/integration-probes.d.ts +3 -3
  20. package/dist-server/engine/integration-probes.js +6 -6
  21. package/dist-server/engine/integration-probes.js.map +1 -1
  22. package/dist-server/engine/kpi-baseline.js +4 -4
  23. package/dist-server/engine/kpi-baseline.js.map +1 -1
  24. package/dist-server/engine/kpi-fold.d.ts +122 -12
  25. package/dist-server/engine/kpi-fold.js +247 -18
  26. package/dist-server/engine/kpi-fold.js.map +1 -1
  27. package/dist-server/engine/kpi-query.d.ts +11 -11
  28. package/dist-server/engine/kpi-query.js +185 -28
  29. package/dist-server/engine/kpi-query.js.map +1 -1
  30. package/dist-server/engine/live-feed-registry.d.ts +1 -1
  31. package/dist-server/engine/live-feed-registry.js +1 -1
  32. package/dist-server/engine/live-feed-registry.js.map +1 -1
  33. package/dist-server/engine/load-meter.d.ts +1 -1
  34. package/dist-server/engine/load-meter.js.map +1 -1
  35. package/dist-server/engine/local-declarations.d.ts +7 -7
  36. package/dist-server/engine/local-declarations.js +9 -9
  37. package/dist-server/engine/local-declarations.js.map +1 -1
  38. package/dist-server/engine/log.d.ts +1 -1
  39. package/dist-server/engine/log.js +1 -1
  40. package/dist-server/engine/log.js.map +1 -1
  41. package/dist-server/engine/measured-estimator.js +1 -1
  42. package/dist-server/engine/measured-estimator.js.map +1 -1
  43. package/dist-server/engine/measured-yield.js +1 -1
  44. package/dist-server/engine/measured-yield.js.map +1 -1
  45. package/dist-server/engine/model-basis.d.ts +1 -1
  46. package/dist-server/engine/model-basis.js +1 -1
  47. package/dist-server/engine/model-basis.js.map +1 -1
  48. package/dist-server/engine/model-gap.d.ts +3 -3
  49. package/dist-server/engine/model-gap.js +1 -1
  50. package/dist-server/engine/model-gap.js.map +1 -1
  51. package/dist-server/engine/model-vocabulary.js +1 -1
  52. package/dist-server/engine/model-vocabulary.js.map +1 -1
  53. package/dist-server/engine/oee-accumulator.d.ts +4 -4
  54. package/dist-server/engine/oee-accumulator.js +3 -3
  55. package/dist-server/engine/oee-accumulator.js.map +1 -1
  56. package/dist-server/engine/property-effects.js +2 -2
  57. package/dist-server/engine/property-effects.js.map +1 -1
  58. package/dist-server/engine/read-time.d.ts +22 -0
  59. package/dist-server/engine/read-time.js +81 -0
  60. package/dist-server/engine/read-time.js.map +1 -0
  61. package/dist-server/engine/restart-policy.js +1 -1
  62. package/dist-server/engine/restart-policy.js.map +1 -1
  63. package/dist-server/engine/runtime-key.js +1 -1
  64. package/dist-server/engine/runtime-key.js.map +1 -1
  65. package/dist-server/engine/spec-coverage.d.ts +2 -2
  66. package/dist-server/engine/spec-coverage.js +1 -1
  67. package/dist-server/engine/spec-coverage.js.map +1 -1
  68. package/dist-server/engine/stage-path.d.ts +4 -4
  69. package/dist-server/engine/stage-path.js +1 -1
  70. package/dist-server/engine/stage-path.js.map +1 -1
  71. package/dist-server/engine/state-axes.d.ts +1 -1
  72. package/dist-server/engine/state-axes.js +2 -2
  73. package/dist-server/engine/state-axes.js.map +1 -1
  74. package/dist-server/engine/travel-estimator.d.ts +2 -2
  75. package/dist-server/engine/travel-estimator.js +1 -1
  76. package/dist-server/engine/travel-estimator.js.map +1 -1
  77. package/dist-server/engine/twin-engine.d.ts +243 -53
  78. package/dist-server/engine/twin-engine.js +690 -191
  79. package/dist-server/engine/twin-engine.js.map +1 -1
  80. package/dist-server/engine/warm-start.d.ts +2 -2
  81. package/dist-server/engine/warm-start.js +5 -5
  82. package/dist-server/engine/warm-start.js.map +1 -1
  83. package/dist-server/index.js +1 -1
  84. package/dist-server/index.js.map +1 -1
  85. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js +1 -1
  86. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js.map +1 -1
  87. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js +3 -3
  88. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js.map +1 -1
  89. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js +2 -2
  90. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js.map +1 -1
  91. package/dist-server/migrations/1786300000000-IndexEventTypeByTime.d.ts +5 -0
  92. package/dist-server/migrations/1786300000000-IndexEventTypeByTime.js +54 -0
  93. package/dist-server/migrations/1786300000000-IndexEventTypeByTime.js.map +1 -0
  94. package/dist-server/migrations/1786400000000-IndexEventCreatedAt.d.ts +5 -0
  95. package/dist-server/migrations/1786400000000-IndexEventCreatedAt.js +44 -0
  96. package/dist-server/migrations/1786400000000-IndexEventCreatedAt.js.map +1 -0
  97. package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.d.ts +6 -0
  98. package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.js +84 -0
  99. package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.js.map +1 -0
  100. package/dist-server/migrations/index.js +7 -1
  101. package/dist-server/migrations/index.js.map +1 -1
  102. package/dist-server/routes.d.ts +1 -0
  103. package/dist-server/routes.js +69 -5
  104. package/dist-server/routes.js.map +1 -1
  105. package/dist-server/service/index.d.ts +2 -2
  106. package/dist-server/service/index.js +27 -24
  107. package/dist-server/service/index.js.map +1 -1
  108. package/dist-server/service/reference/control-routing.d.ts +1 -1
  109. package/dist-server/service/reference/control-routing.js +1 -1
  110. package/dist-server/service/reference/control-routing.js.map +1 -1
  111. package/dist-server/service/reference/discovery-result.d.ts +2 -2
  112. package/dist-server/service/reference/discovery-result.js +3 -3
  113. package/dist-server/service/reference/discovery-result.js.map +1 -1
  114. package/dist-server/service/reference/hook-contract.d.ts +43 -0
  115. package/dist-server/service/reference/hook-contract.js +59 -0
  116. package/dist-server/service/reference/hook-contract.js.map +1 -0
  117. package/dist-server/service/reference/ingest-space.d.ts +1 -1
  118. package/dist-server/service/reference/ingest-space.js +4 -4
  119. package/dist-server/service/reference/ingest-space.js.map +1 -1
  120. package/dist-server/service/reference/knob-defaults.d.ts +1 -1
  121. package/dist-server/service/reference/knob-defaults.js +4 -4
  122. package/dist-server/service/reference/knob-defaults.js.map +1 -1
  123. package/dist-server/service/reference/reference-adapter.d.ts +140 -8
  124. package/dist-server/service/reference/reference-adapter.js +27 -0
  125. package/dist-server/service/reference/reference-adapter.js.map +1 -1
  126. package/dist-server/service/reference/reference-hook.d.ts +20 -0
  127. package/dist-server/service/reference/reference-hook.js +133 -0
  128. package/dist-server/service/reference/reference-hook.js.map +1 -0
  129. package/dist-server/service/reference/reference-live.d.ts +2 -2
  130. package/dist-server/service/reference/reference-live.js +88 -13
  131. package/dist-server/service/reference/reference-live.js.map +1 -1
  132. package/dist-server/service/reference/reference-master.d.ts +14 -14
  133. package/dist-server/service/reference/reference-master.js +34 -12
  134. package/dist-server/service/reference/reference-master.js.map +1 -1
  135. package/dist-server/service/reference/reference-resolver.d.ts +1 -1
  136. package/dist-server/service/reference/reference-resolver.js +10 -10
  137. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  138. package/dist-server/service/reference/twin-reference.d.ts +3 -3
  139. package/dist-server/service/reference/twin-reference.js.map +1 -1
  140. package/dist-server/service/twin-attention/twin-attention-query.d.ts +1 -1
  141. package/dist-server/service/twin-attention/twin-attention-query.js +1 -1
  142. package/dist-server/service/twin-attention/twin-attention-query.js.map +1 -1
  143. package/dist-server/service/twin-audit/command-audit.d.ts +1 -1
  144. package/dist-server/service/twin-audit/command-audit.js.map +1 -1
  145. package/dist-server/service/twin-audit/twin-audit-event.d.ts +1 -1
  146. package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -1
  147. package/dist-server/service/twin-audit/twin-audit-query.js +1 -1
  148. package/dist-server/service/twin-audit/twin-audit-query.js.map +1 -1
  149. package/dist-server/service/twin-control/twin-control-mutation.js +1 -1
  150. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  151. package/dist-server/service/twin-event/twin-event-keys.d.ts +22 -2
  152. package/dist-server/service/twin-event/twin-event-keys.js +37 -5
  153. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  154. package/dist-server/service/twin-event/twin-event-type.d.ts +1 -0
  155. package/dist-server/service/twin-event/twin-event-type.js +8 -1
  156. package/dist-server/service/twin-event/twin-event-type.js.map +1 -1
  157. package/dist-server/service/twin-event/twin-event.d.ts +2 -2
  158. package/dist-server/service/twin-event/twin-event.js +32 -4
  159. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  160. package/dist-server/service/twin-forecast/forecast-metrics.js +1 -1
  161. package/dist-server/service/twin-forecast/forecast-metrics.js.map +1 -1
  162. package/dist-server/service/twin-forecast/gap-analytics.d.ts +1 -1
  163. package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
  164. package/dist-server/service/twin-forecast/twin-forecast-query.js +29 -11
  165. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  166. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.d.ts +1 -1
  167. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js +1 -1
  168. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js.map +1 -1
  169. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js +1 -1
  170. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js.map +1 -1
  171. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  172. package/dist-server/service/twin-journal/event-type-split-shape.d.ts +50 -0
  173. package/dist-server/service/twin-journal/event-type-split-shape.js +111 -0
  174. package/dist-server/service/twin-journal/event-type-split-shape.js.map +1 -0
  175. package/dist-server/service/twin-journal/event-type-split.d.ts +15 -0
  176. package/dist-server/service/twin-journal/event-type-split.js +102 -0
  177. package/dist-server/service/twin-journal/event-type-split.js.map +1 -0
  178. package/dist-server/service/twin-journal/journal-order.d.ts +15 -0
  179. package/dist-server/service/twin-journal/journal-order.js +17 -0
  180. package/dist-server/service/twin-journal/journal-order.js.map +1 -0
  181. package/dist-server/service/twin-journal/twin-journal-query.d.ts +5 -5
  182. package/dist-server/service/twin-journal/twin-journal-query.js +232 -21
  183. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  184. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +27 -1
  185. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +59 -3
  186. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  187. package/dist-server/service/twin-metrics/twin-metrics-query.js +14 -1
  188. package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
  189. package/dist-server/service/twin-model/axis-journal-evidence.js +1 -1
  190. package/dist-server/service/twin-model/axis-journal-evidence.js.map +1 -1
  191. package/dist-server/service/twin-model/epcis-coverage.js +36 -1
  192. package/dist-server/service/twin-model/epcis-coverage.js.map +1 -1
  193. package/dist-server/service/twin-model/iec61850-coverage.js +1 -1
  194. package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -1
  195. package/dist-server/service/twin-model/isa95-coverage.d.ts +15 -2
  196. package/dist-server/service/twin-model/isa95-coverage.js +32 -8
  197. package/dist-server/service/twin-model/isa95-coverage.js.map +1 -1
  198. package/dist-server/service/twin-model/item-ref.js +2 -2
  199. package/dist-server/service/twin-model/item-ref.js.map +1 -1
  200. package/dist-server/service/twin-model/name-index.d.ts +1 -1
  201. package/dist-server/service/twin-model/name-index.js +1 -1
  202. package/dist-server/service/twin-model/name-index.js.map +1 -1
  203. package/dist-server/service/twin-model/project-structure.d.ts +1 -1
  204. package/dist-server/service/twin-model/project-structure.js +3 -3
  205. package/dist-server/service/twin-model/project-structure.js.map +1 -1
  206. package/dist-server/service/twin-model/twin-equipment.d.ts +1 -1
  207. package/dist-server/service/twin-model/twin-equipment.js +1 -1
  208. package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
  209. package/dist-server/service/twin-model/twin-lineage-query.js +16 -5
  210. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
  211. package/dist-server/service/twin-model/twin-location.d.ts +1 -1
  212. package/dist-server/service/twin-model/twin-location.js.map +1 -1
  213. package/dist-server/service/twin-model/twin-model-item-query.d.ts +3 -3
  214. package/dist-server/service/twin-model/twin-model-item-query.js +14 -14
  215. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
  216. package/dist-server/service/twin-model/twin-model-mutation.d.ts +2 -2
  217. package/dist-server/service/twin-model/twin-model-mutation.js +4 -4
  218. package/dist-server/service/twin-model/twin-model-mutation.js.map +1 -1
  219. package/dist-server/service/twin-model/twin-model-query.js +117 -28
  220. package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
  221. package/dist-server/service/twin-model/twin-model-tree-query.js +22 -0
  222. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
  223. package/dist-server/service/twin-model/twin-operation.d.ts +1 -1
  224. package/dist-server/service/twin-model/twin-operation.js +1 -1
  225. package/dist-server/service/twin-model/twin-operation.js.map +1 -1
  226. package/dist-server/service/twin-space/move-space.js +2 -2
  227. package/dist-server/service/twin-space/move-space.js.map +1 -1
  228. package/dist-server/service/twin-space/representation-role.d.ts +2 -0
  229. package/dist-server/service/twin-space/representation-role.js +18 -0
  230. package/dist-server/service/twin-space/representation-role.js.map +1 -0
  231. package/dist-server/service/twin-space/space-integrity.d.ts +1 -1
  232. package/dist-server/service/twin-space/space-integrity.js.map +1 -1
  233. package/dist-server/service/twin-space/twin-space-representation.d.ts +2 -0
  234. package/dist-server/service/twin-space/twin-space-representation.js +16 -0
  235. package/dist-server/service/twin-space/twin-space-representation.js.map +1 -1
  236. package/dist-server/service/twin-space/twin-space-resolver.d.ts +2 -2
  237. package/dist-server/service/twin-space/twin-space-resolver.js +36 -7
  238. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  239. package/dist-server/service/twin-structure/twin-structure.js +3 -3
  240. package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
  241. package/dist-server/service/twin-subject/event-subjects.d.ts +43 -0
  242. package/dist-server/service/twin-subject/event-subjects.js +137 -0
  243. package/dist-server/service/twin-subject/event-subjects.js.map +1 -0
  244. package/dist-server/service/twin-subject/index.d.ts +9 -0
  245. package/dist-server/service/twin-subject/index.js +16 -0
  246. package/dist-server/service/twin-subject/index.js.map +1 -0
  247. package/dist-server/service/twin-subject/item-resolver.d.ts +10 -0
  248. package/dist-server/service/twin-subject/item-resolver.js +93 -0
  249. package/dist-server/service/twin-subject/item-resolver.js.map +1 -0
  250. package/dist-server/service/twin-subject/subject-query-shape.d.ts +22 -0
  251. package/dist-server/service/twin-subject/subject-query-shape.js +88 -0
  252. package/dist-server/service/twin-subject/subject-query-shape.js.map +1 -0
  253. package/dist-server/service/twin-subject/twin-subject-backfill.d.ts +23 -0
  254. package/dist-server/service/twin-subject/twin-subject-backfill.js +159 -0
  255. package/dist-server/service/twin-subject/twin-subject-backfill.js.map +1 -0
  256. package/dist-server/service/twin-subject/twin-subject-event.d.ts +14 -0
  257. package/dist-server/service/twin-subject/twin-subject-event.js +111 -0
  258. package/dist-server/service/twin-subject/twin-subject-event.js.map +1 -0
  259. package/dist-server/service/twin-subject/twin-subject-fast-path.d.ts +39 -0
  260. package/dist-server/service/twin-subject/twin-subject-fast-path.js +295 -0
  261. package/dist-server/service/twin-subject/twin-subject-fast-path.js.map +1 -0
  262. package/dist-server/service/twin-subject/twin-subject-query.d.ts +27 -0
  263. package/dist-server/service/twin-subject/twin-subject-query.js +232 -0
  264. package/dist-server/service/twin-subject/twin-subject-query.js.map +1 -0
  265. package/dist-server/service/twin-subject/twin-subject-writer.d.ts +27 -0
  266. package/dist-server/service/twin-subject/twin-subject-writer.js +166 -0
  267. package/dist-server/service/twin-subject/twin-subject-writer.js.map +1 -0
  268. package/dist-server/service/twin-target/twin-target-resolver.js +1 -1
  269. package/dist-server/service/twin-target/twin-target-resolver.js.map +1 -1
  270. package/dist-shared/entity-delta.d.ts +1 -1
  271. package/dist-shared/entity-delta.js +3 -3
  272. package/dist-shared/entity-delta.js.map +1 -1
  273. package/dist-shared/touched-items.js +2 -2
  274. package/dist-shared/touched-items.js.map +1 -1
  275. package/dist-shared/twin-level.js +1 -1
  276. package/dist-shared/twin-level.js.map +1 -1
  277. package/package.json +3 -3
  278. package/server/engine/attention-digest.ts +9 -9
  279. package/server/engine/canonical-ingest.ts +170 -13
  280. package/server/engine/command-routing.ts +1 -1
  281. package/server/engine/declared-stimulus.ts +1 -1
  282. package/server/engine/energy-topology.ts +1 -1
  283. package/server/engine/ingest-dedupe.ts +149 -0
  284. package/server/engine/ingest-health.ts +234 -20
  285. package/server/engine/integration-probes.ts +7 -7
  286. package/server/engine/kpi-baseline.ts +4 -4
  287. package/server/engine/kpi-fold.ts +395 -27
  288. package/server/engine/kpi-query.ts +212 -46
  289. package/server/engine/live-feed-registry.ts +1 -1
  290. package/server/engine/load-meter.ts +1 -1
  291. package/server/engine/local-declarations.ts +13 -13
  292. package/server/engine/log.ts +1 -1
  293. package/server/engine/measured-estimator.ts +1 -1
  294. package/server/engine/measured-yield.ts +1 -1
  295. package/server/engine/model-basis.ts +2 -2
  296. package/server/engine/model-gap.ts +3 -3
  297. package/server/engine/model-vocabulary.ts +1 -1
  298. package/server/engine/oee-accumulator.ts +4 -4
  299. package/server/engine/property-effects.ts +2 -2
  300. package/server/engine/read-time.ts +77 -0
  301. package/server/engine/restart-policy.ts +1 -1
  302. package/server/engine/runtime-key.ts +1 -1
  303. package/server/engine/spec-coverage.ts +3 -3
  304. package/server/engine/stage-path.ts +5 -5
  305. package/server/engine/state-axes.ts +2 -2
  306. package/server/engine/travel-estimator.ts +2 -2
  307. package/server/engine/twin-engine.ts +764 -201
  308. package/server/engine/warm-start.ts +6 -6
  309. package/server/index.ts +1 -1
  310. package/server/migrations/1786000000000-RenameTwinInstanceBoardToModel.ts +1 -1
  311. package/server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.ts +3 -3
  312. package/server/migrations/1786200000000-CarryLiveFeedCursor.ts +2 -2
  313. package/server/migrations/1786300000000-IndexEventTypeByTime.ts +48 -0
  314. package/server/migrations/1786400000000-IndexEventCreatedAt.ts +38 -0
  315. package/server/migrations/1786500000000-CreateTwinSubjectEvents.ts +93 -0
  316. package/server/migrations/index.ts +7 -1
  317. package/server/routes.ts +68 -5
  318. package/server/service/index.ts +4 -1
  319. package/server/service/reference/control-routing.ts +1 -1
  320. package/server/service/reference/discovery-result.ts +3 -3
  321. package/server/service/reference/hook-contract.ts +103 -0
  322. package/server/service/reference/ingest-space.ts +4 -4
  323. package/server/service/reference/knob-defaults.ts +4 -4
  324. package/server/service/reference/reference-adapter.ts +166 -9
  325. package/server/service/reference/reference-hook.ts +145 -0
  326. package/server/service/reference/reference-live.ts +92 -14
  327. package/server/service/reference/reference-master.ts +39 -17
  328. package/server/service/reference/reference-resolver.ts +10 -10
  329. package/server/service/reference/twin-reference.ts +3 -3
  330. package/server/service/twin-attention/twin-attention-query.ts +1 -1
  331. package/server/service/twin-audit/command-audit.ts +1 -1
  332. package/server/service/twin-audit/twin-audit-event.ts +2 -2
  333. package/server/service/twin-audit/twin-audit-query.ts +1 -1
  334. package/server/service/twin-control/twin-control-mutation.ts +1 -1
  335. package/server/service/twin-event/twin-event-keys.ts +37 -5
  336. package/server/service/twin-event/twin-event-type.ts +18 -1
  337. package/server/service/twin-event/twin-event.ts +32 -6
  338. package/server/service/twin-forecast/forecast-metrics.ts +1 -1
  339. package/server/service/twin-forecast/gap-analytics.ts +1 -1
  340. package/server/service/twin-forecast/twin-forecast-query.ts +30 -11
  341. package/server/service/twin-ingest-window/twin-ingest-window-query.ts +1 -1
  342. package/server/service/twin-ingest-window/twin-ingest-window-writer.ts +1 -1
  343. package/server/service/twin-instance/twin-instance.ts +4 -4
  344. package/server/service/twin-journal/event-type-split-shape.ts +112 -0
  345. package/server/service/twin-journal/event-type-split.ts +144 -0
  346. package/server/service/twin-journal/journal-order.ts +20 -0
  347. package/server/service/twin-journal/twin-journal-query.ts +224 -20
  348. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +57 -3
  349. package/server/service/twin-metrics/twin-metrics-query.ts +14 -1
  350. package/server/service/twin-model/axis-journal-evidence.ts +1 -1
  351. package/server/service/twin-model/epcis-coverage.ts +36 -1
  352. package/server/service/twin-model/iec61850-coverage.ts +1 -1
  353. package/server/service/twin-model/isa95-coverage.ts +45 -8
  354. package/server/service/twin-model/item-ref.ts +2 -2
  355. package/server/service/twin-model/name-index.ts +1 -1
  356. package/server/service/twin-model/project-structure.ts +4 -4
  357. package/server/service/twin-model/twin-equipment.ts +2 -2
  358. package/server/service/twin-model/twin-lineage-query.ts +16 -5
  359. package/server/service/twin-model/twin-location.ts +1 -1
  360. package/server/service/twin-model/twin-model-item-query.ts +14 -14
  361. package/server/service/twin-model/twin-model-mutation.ts +4 -4
  362. package/server/service/twin-model/twin-model-query.ts +117 -28
  363. package/server/service/twin-model/twin-model-tree-query.ts +25 -1
  364. package/server/service/twin-model/twin-operation.ts +2 -2
  365. package/server/service/twin-space/move-space.ts +3 -3
  366. package/server/service/twin-space/representation-role.ts +15 -0
  367. package/server/service/twin-space/space-integrity.ts +1 -1
  368. package/server/service/twin-space/twin-space-representation.ts +37 -0
  369. package/server/service/twin-space/twin-space-resolver.ts +39 -7
  370. package/server/service/twin-structure/twin-structure.ts +3 -3
  371. package/server/service/twin-subject/event-subjects.ts +155 -0
  372. package/server/service/twin-subject/index.ts +10 -0
  373. package/server/service/twin-subject/item-resolver.ts +92 -0
  374. package/server/service/twin-subject/subject-query-shape.ts +84 -0
  375. package/server/service/twin-subject/twin-subject-backfill.ts +196 -0
  376. package/server/service/twin-subject/twin-subject-event.ts +101 -0
  377. package/server/service/twin-subject/twin-subject-fast-path.ts +346 -0
  378. package/server/service/twin-subject/twin-subject-query.ts +210 -0
  379. package/server/service/twin-subject/twin-subject-writer.ts +189 -0
  380. package/server/service/twin-target/twin-target-resolver.ts +1 -1
  381. package/shared/entity-delta.ts +3 -3
  382. package/shared/touched-items.ts +2 -2
  383. package/shared/twin-level.ts +1 -1
  384. package/test/adopt-structure-live.test.ts +3 -3
  385. package/test/aggregate-not-findone.test.ts +112 -0
  386. package/test/attention-digest.test.ts +6 -6
  387. package/test/axis-read.test.ts +2 -2
  388. package/test/boot-resume.test.ts +20 -7
  389. package/test/broadcast-period.test.ts +2 -2
  390. package/test/broadcast-queue-overflow.test.ts +244 -0
  391. package/test/canonical-ingest-vocabularies.test.ts +179 -7
  392. package/test/canonical-quantity-door.test.ts +2 -2
  393. package/test/checkpoint-refuses-empty.test.ts +2 -2
  394. package/test/connector-capability-declaration.test.ts +88 -0
  395. package/test/control-capability.test.ts +1 -1
  396. package/test/cursor-stall-not-read-failure.test.ts +2 -2
  397. package/test/declaration-reaches-model.test.ts +5 -5
  398. package/test/declared-stimulus.test.ts +4 -4
  399. package/test/discovery-result.test.ts +2 -2
  400. package/test/entity-delta.test.ts +4 -4
  401. package/test/event-subjects.test.ts +147 -0
  402. package/test/event-time-column.test.ts +3 -3
  403. package/test/event-type-split.test.ts +155 -0
  404. package/test/fold-gap-speaks.test.ts +53 -0
  405. package/test/forecast-metrics.test.ts +1 -1
  406. package/test/forecast-tuning.test.ts +1 -1
  407. package/test/ingest-health-engine.test.ts +2 -2
  408. package/test/ingest-health-wiring.test.ts +7 -5
  409. package/test/ingest-health.test.ts +6 -6
  410. package/test/ingest-history.test.ts +2 -2
  411. package/test/ingest-idempotent.test.ts +132 -0
  412. package/test/ingest-space.test.ts +3 -3
  413. package/test/integration-probes.test.ts +3 -3
  414. package/test/integration-runner.test.ts +1 -1
  415. package/test/item-ref.test.ts +1 -1
  416. package/test/journal-order-axis.test.ts +80 -0
  417. package/test/journal-read-discipline.test.ts +16 -6
  418. package/test/journal-retention.test.ts +171 -29
  419. package/test/journal-sort-axis.test.ts +1 -1
  420. package/test/journal-write-door.test.ts +3 -3
  421. package/test/journal-write-trend.test.ts +1 -1
  422. package/test/kernel-kind-guard.test.ts +3 -3
  423. package/test/knob-defaults.test.ts +1 -1
  424. package/test/kpi-baseline-db.test.ts +2 -2
  425. package/test/kpi-fold.test.ts +185 -14
  426. package/test/kpi-query-bench.test.ts +3 -3
  427. package/test/live-cursor-wiring.test.ts +6 -6
  428. package/test/live-kernel-facts.test.ts +11 -11
  429. package/test/live-mirror-parity.test.ts +3 -3
  430. package/test/load-meter.test.ts +1 -1
  431. package/test/local-declarations.test.ts +10 -10
  432. package/test/log-stamp.test.ts +1 -1
  433. package/test/measured-yield.test.ts +2 -2
  434. package/test/merge-streams.test.ts +70 -0
  435. package/test/mirror-resumes-from-checkpoint.test.ts +13 -13
  436. package/test/model-basis.test.ts +2 -2
  437. package/test/model-gap.test.ts +4 -4
  438. package/test/model-vocabulary.test.ts +2 -2
  439. package/test/move-space.test.ts +1 -1
  440. package/test/oee-accumulator.test.ts +2 -2
  441. package/test/operational-vocabulary.test.ts +2 -2
  442. package/test/project-structure-db.test.ts +2 -2
  443. package/test/projection-reaches-screen.test.ts +2 -2
  444. package/test/property-effects.test.ts +3 -3
  445. package/test/read-failure-ledger.test.ts +106 -0
  446. package/test/read-failure-visible.test.ts +7 -7
  447. package/test/read-time.test.ts +118 -0
  448. package/test/reference-grounding.test.ts +1 -1
  449. package/test/reference-hook.test.ts +86 -0
  450. package/test/registry-key-guard.test.ts +1 -1
  451. package/test/rename-twin.test.ts +81 -0
  452. package/test/restart-policy.test.ts +3 -3
  453. package/test/resync-origin-site.test.ts +3 -3
  454. package/test/revision-axis.test.ts +14 -8
  455. package/test/runtime-key.test.ts +3 -3
  456. package/test/snapshot-freshness.test.ts +4 -4
  457. package/test/source-outcome-audit.test.ts +1 -1
  458. package/test/space-integrity.test.ts +1 -1
  459. package/test/space-representation.test.ts +66 -0
  460. package/test/space-scope-includes-stopped.test.ts +55 -0
  461. package/test/spec-coverage.test.ts +1 -1
  462. package/test/standard-coverage.test.ts +43 -1
  463. package/test/status-tally.test.ts +1 -1
  464. package/test/structure-revision-db.test.ts +7 -7
  465. package/test/subject-fast-path.test.ts +148 -0
  466. package/test/subject-read-model.test.ts +175 -0
  467. package/test/touched-items.test.ts +2 -2
  468. package/test/twin-audit.test.ts +1 -1
  469. package/test/twin-event-keys.test.ts +49 -3
  470. package/test/twin-model-item-db.test.ts +5 -5
  471. package/test/twin-model-tree-db.test.ts +35 -2
  472. package/test/twin-origin-resync.test.ts +1 -1
  473. package/test/warm-start-seam.test.ts +1 -1
  474. package/test/warm-start.test.ts +6 -6
  475. package/test/withheld-door.test.ts +95 -0
  476. package/test/yield-loop.test.ts +4 -4
  477. package/tsconfig.shared.tsbuildinfo +1 -1
  478. package/tsconfig.tsbuildinfo +1 -1
@@ -19,7 +19,7 @@ export interface ModelBasis {
19
19
  * 선언된 공정 — key + ISO 8601 원문(문자열은 그대로 비교한다: 선언이 바뀌면 다른 모델).
20
20
  *
21
21
  * **변동폭도 지문에 든다.** 변동폭이 허용오차를 정하므로(§toleranceOf), 그것이 바뀌었는데 "같은 모델"
22
- * 이라고 하면 판정의 잣대가 조용히 바뀐 채로 학습이 이어진다.
22
+ * 이라고 하면 판정의 잣대가 알리지 않고 바뀐 채로 학습이 이어진다.
23
23
  */
24
24
  declared?: DeclaredOperation[];
25
25
  }
@@ -8,7 +8,7 @@
8
8
  * 주면 **이중 보정**이 되어 정확도가 나빠진다. 학습 시점의 accAfter 도 더는 유효하지 않다.
9
9
  *
10
10
  * 그래서 보정 기록에 **그때 모델의 근거**를 함께 남기고, 적용 전에 지금 근거와 비교한다. 달라졌으면
11
- * 적용하지 않고 **그 사실을 응답에 싣는다**(조용히 사라지면 사용자는 학습이 왜 안 듣는지 모른다).
11
+ * 적용하지 않고 **그 사실을 응답에 싣는다**(오류 없이 사라지면 사용자는 학습이 왜 안 듣는지 모른다).
12
12
  *
13
13
  * 비교는 **상대 허용오차**로 한다(양자화 지문이 아니라). 실측 p50 은 새 기록이 쌓일 때마다 1~2초씩
14
14
  * 흔들리는데, 값을 버킷으로 합쳐 비교하면 버킷 경계에 걸린 값이 1초 드리프트로도 달라져 버린다 —
@@ -1 +1 @@
1
- {"version":3,"file":"model-basis.js","sourceRoot":"","sources":["../../server/engine/model-basis.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 모델 근거 — **배운 모델에만 보정을 적용하기 위한 비교.**\n *\n * 예측 출력 보정(P3.2 `twinLearnCalibration`)은 \"우리 모델이 늘 이만큼 적게 본다\" 를 배워 결과를\n * 밀어 준다. 그 학습은 **그때의 모델**을 전제로 한다. 그런데 소요시간 추정기(실측·거리계산)와 공정\n * 명세가 들어오면서 모델 자체가 바뀔 수 있게 됐다 — 모델이 이미 그 편향을 잃었는데 보정이 또 밀어\n * 주면 **이중 보정**이 되어 정확도가 나빠진다. 학습 시점의 accAfter 도 더는 유효하지 않다.\n *\n * 그래서 보정 기록에 **그때 모델의 근거**를 함께 남기고, 적용 전에 지금 근거와 비교한다. 달라졌으면\n * 적용하지 않고 **그 사실을 응답에 싣는다**(조용히 사라지면 사용자는 학습이 왜 안 듣는지 모른다).\n *\n * 비교는 **상대 허용오차**로 한다(양자화 지문이 아니라). 실측 p50 은 새 기록이 쌓일 때마다 1~2초씩\n * 흔들리는데, 값을 버킷으로 합쳐 비교하면 버킷 경계에 걸린 값이 1초 드리프트로도 달라져 버린다 —\n * \"작은 드리프트로 학습을 무효화하지 않는다\" 는 목적을 경계에서 못 지킨다. 비율로 비교하면 어디서든\n * 같은 규칙이 성립한다: 10% 이내는 같은 모델, 30초→47초(57%)는 다른 모델.\n *\n * 재료는 호스트가 아는 것들이다(커널 API 에 의존하지 않아 발행 상태와 무관하게 동작한다):\n * ① 실측으로 배운 작업 종류별 소요 ② 자원 종류별 속도 ③ 선언된 공정 소요.\n */\n\n/**\n * 허용오차의 **기본값** — 선언이 아무 말도 없을 때만 쓴다.\n *\n * 10% 는 근거에서 나온 수가 아니라 **판단값**이다(그 사실을 적어 두는 것이 이 주석의 목적이다). 그리고\n * 단일 잣대로 쓰면 틀린다 — 우리 트레일러 템플릿이 그것을 보여 준다: 용접은 변동폭을 `PT55M~PT1H35M`\n * (선언 소요 `PT1H10M` 기준 −21%~+36%)로 **스스로 선언한다.** 10% 관문은 그 공정의 **정상 변동조차**\n * \"다른 모델\" 로 판정해 보정을 끊는다. 반대로 1% 변동도 치명적인 공정에는 10% 가 너무 느슨하다.\n *\n * 그래서 허용오차는 **그 공정이 선언한 변동폭에서 나온다**(§toleranceOf). 선언이 있으면 선언이 이기고,\n * 없을 때만 이 값으로 떨어진다 — 도메인 설정을 새로 만들지 않는다(현장이 이미 말한 것을 읽는다).\n */\nexport const BASIS_TOLERANCE = 0.1\n\nexport interface ModelBasis {\n /** 작업 종류 → 실측 소요(ms). `buildMeasuredEstimator().learned`. */\n measured?: Record<string, number>\n /** 자원 종류 → 속도(m/s). `buildTravelEstimator().speedsByKind`. */\n speeds?: Record<string, number>\n /**\n * 선언된 공정 — key + ISO 8601 원문(문자열은 그대로 비교한다: 선언이 바뀌면 다른 모델).\n *\n * **변동폭도 지문에 든다.** 변동폭이 허용오차를 정하므로(§toleranceOf), 그것이 바뀌었는데 \"같은 모델\"\n * 이라고 하면 판정의 잣대가 조용히 바뀐 채로 학습이 이어진다.\n */\n declared?: DeclaredOperation[]\n}\n\n/** 선언된 공정 하나 — 소요와 변동폭(모델의 `operations` 원형에서 필요한 만큼만). */\nexport interface DeclaredOperation {\n key: string\n duration?: string\n variability?: { distribution?: string; min?: string; max?: string; mode?: string }\n}\n\n/** ISO 8601 기간 → 밀리초. 읽을 수 없으면 `undefined`(짐작해 고치지 않는다). */\nfunction isoMs(iso?: string): number | undefined {\n if (!iso) return undefined\n const m = /^P(?:(\\d+)D)?(?:T(?:(\\d+)H)?(?:(\\d+)M)?(?:([\\d.]+)S)?)?$/.exec(iso)\n if (!m || !m.slice(1).some(Boolean)) return undefined\n return ((Number(m[1] ?? 0) * 24 + Number(m[2] ?? 0)) * 60 + Number(m[3] ?? 0)) * 60_000 + Number(m[4] ?? 0) * 1000\n}\n\n/**\n * 작업 종류별 **허용오차** — 그 공정이 선언한 변동폭에서 나온다.\n *\n * 실측 p50 은 정상 변동 안에서 흔들린다. 그 흔들림을 \"다른 모델\" 로 읽으면 보정이 자꾸 끊기고, 사용자는\n * 학습이 왜 안 듣는지 모른다. 얼마나 흔들리는 것이 정상인지는 **현장이 이미 선언했다**(`variability`).\n *\n * 반폭을 쓴다(`(max − min) / 2 / 소요`) — 중앙에서 어느 쪽으로든 그만큼은 정상이라는 뜻이다.\n * 선언이 없거나 읽을 수 없으면 기본값으로 떨어진다(짐작해 만들지 않는다).\n */\nexport function toleranceOf(declared?: DeclaredOperation[]): Record<string, number> {\n const out: Record<string, number> = {}\n for (const o of declared ?? []) {\n if (!o?.key) continue\n const base = isoMs(o.duration)\n const min = isoMs(o.variability?.min)\n const max = isoMs(o.variability?.max)\n if (!base || min === undefined || max === undefined || !(max > min)) continue\n const half = (max - min) / 2 / base\n if (Number.isFinite(half) && half > 0) out[o.key] = half\n }\n return out\n}\n\n/** 뜻 있는 수만 남긴다 — 0·음수·NaN 은 모델이 아니다. */\nfunction clean(map?: Record<string, number>): Record<string, number> {\n const out: Record<string, number> = {}\n for (const [k, v] of Object.entries(map ?? {})) if (Number.isFinite(v) && v > 0) out[k] = v\n return out\n}\n\nfunction declaredMap(list?: DeclaredOperation[]): Record<string, string> {\n const out: Record<string, string> = {}\n for (const o of list ?? []) {\n if (!o?.key) continue\n const v = o.variability\n /* 변동폭까지 한 문장으로 — 순서를 고정해 같은 선언이 같은 문자열이 되게 한다. */\n const spread = v ? `~${v.distribution ?? '?'}:${v.min ?? ''}..${v.max ?? ''}@${v.mode ?? ''}` : ''\n if (o.duration || spread) out[o.key] = `${o.duration ?? ''}${spread}`\n }\n return out\n}\n\n/** 두 수치 지도가 실질적으로 다른가 — 키 집합이 다르거나, 값이 허용오차를 넘어 다르면 다르다. */\nfunction numericChanged(a: Record<string, number>, b: Record<string, number>, tolOf: (key: string) => number): boolean {\n const keys = new Set([...Object.keys(a), ...Object.keys(b)])\n for (const k of keys) {\n const x = a[k]\n const y = b[k]\n if (x === undefined || y === undefined) return true // 새로 배운 종류·사라진 종류 = 다른 모델\n const base = Math.max(Math.abs(x), Math.abs(y))\n if (base > 0 && Math.abs(x - y) / base > tolOf(k)) return true\n }\n return false\n}\n\n/**\n * 모델이 달라졌는가 — 보정을 적용하기 전의 관문.\n * 한쪽 근거를 모르면 **판정하지 않는다**(false) — 모르는 것을 근거로 학습을 끊지 않는다.\n */\nexport function modelBasisChanged(before?: ModelBasis, after?: ModelBasis, tol: number = BASIS_TOLERANCE): boolean {\n if (!before || !after) return false\n /*\n * 허용오차는 **선언이 정한다** — 배운 시점과 지금의 선언이 다를 수 있으므로 둘 중 **넓은 쪽**을 쓴다.\n * 좁은 쪽을 쓰면 선언을 넓힌 직후에 정상 변동이 \"다른 모델\" 로 잡힌다(그 변경 자체는 아래 선언 비교가\n * 이미 잡으므로, 여기서 또 잡을 필요가 없다).\n */\n const tolBefore = toleranceOf(before.declared)\n const tolAfter = toleranceOf(after.declared)\n const tolOf = (key: string): number => Math.max(tolBefore[key] ?? tol, tolAfter[key] ?? tol)\n if (numericChanged(clean(before.measured), clean(after.measured), tolOf)) return true\n /* 속도는 자원 종류별이고 선언된 변동폭이 없다 — 기본값이 그대로 쓰인다. */\n if (numericChanged(clean(before.speeds), clean(after.speeds), () => tol)) return true\n const da = declaredMap(before.declared)\n const db = declaredMap(after.declared)\n const keys = new Set([...Object.keys(da), ...Object.keys(db)])\n for (const k of keys) if (da[k] !== db[k]) return true\n return false\n}\n\n/**\n * 사람이 읽는 근거 요약 — **로그·진단용**(비교의 근거가 아니다).\n * 지문으로 같음을 판정하지 않는 이유는 위 주석 참조. 두 근거가 왜 달라졌는지 눈으로 보려면 이것을 찍는다.\n */\nexport function describeModelBasis(basis?: ModelBasis): string {\n if (!basis) return 'unknown'\n const seg: string[] = []\n const m = Object.entries(clean(basis.measured))\n .map(([k, v]) => `${k}=${Math.round(v / 1000)}s`)\n .sort()\n if (m.length) seg.push(`measured(${m.join(',')})`)\n const s = Object.entries(clean(basis.speeds))\n .map(([k, v]) => `${k}=${Math.round(v * 10) / 10}m/s`)\n .sort()\n if (s.length) seg.push(`speed(${s.join(',')})`)\n const d = Object.entries(declaredMap(basis.declared))\n .map(([k, v]) => `${k}=${v}`)\n .sort()\n if (d.length) seg.push(`declared(${d.join(',')})`)\n return seg.length ? seg.join(' ') : 'kernel defaults only'\n}\n"]}
1
+ {"version":3,"file":"model-basis.js","sourceRoot":"","sources":["../../server/engine/model-basis.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 모델 근거 — **배운 모델에만 보정을 적용하기 위한 비교.**\n *\n * 예측 출력 보정(P3.2 `twinLearnCalibration`)은 \"우리 모델이 늘 이만큼 적게 본다\" 를 배워 결과를\n * 밀어 준다. 그 학습은 **그때의 모델**을 전제로 한다. 그런데 소요시간 추정기(실측·거리계산)와 공정\n * 명세가 들어오면서 모델 자체가 바뀔 수 있게 됐다 — 모델이 이미 그 편향을 잃었는데 보정이 또 밀어\n * 주면 **이중 보정**이 되어 정확도가 나빠진다. 학습 시점의 accAfter 도 더는 유효하지 않다.\n *\n * 그래서 보정 기록에 **그때 모델의 근거**를 함께 남기고, 적용 전에 지금 근거와 비교한다. 달라졌으면\n * 적용하지 않고 **그 사실을 응답에 싣는다**(오류 없이 사라지면 사용자는 학습이 왜 안 듣는지 모른다).\n *\n * 비교는 **상대 허용오차**로 한다(양자화 지문이 아니라). 실측 p50 은 새 기록이 쌓일 때마다 1~2초씩\n * 흔들리는데, 값을 버킷으로 합쳐 비교하면 버킷 경계에 걸린 값이 1초 드리프트로도 달라져 버린다 —\n * \"작은 드리프트로 학습을 무효화하지 않는다\" 는 목적을 경계에서 못 지킨다. 비율로 비교하면 어디서든\n * 같은 규칙이 성립한다: 10% 이내는 같은 모델, 30초→47초(57%)는 다른 모델.\n *\n * 재료는 호스트가 아는 것들이다(커널 API 에 의존하지 않아 발행 상태와 무관하게 동작한다):\n * ① 실측으로 배운 작업 종류별 소요 ② 자원 종류별 속도 ③ 선언된 공정 소요.\n */\n\n/**\n * 허용오차의 **기본값** — 선언이 아무 말도 없을 때만 쓴다.\n *\n * 10% 는 근거에서 나온 수가 아니라 **판단값**이다(그 사실을 적어 두는 것이 이 주석의 목적이다). 그리고\n * 단일 잣대로 쓰면 틀린다 — 우리 트레일러 템플릿이 그것을 보여 준다: 용접은 변동폭을 `PT55M~PT1H35M`\n * (선언 소요 `PT1H10M` 기준 −21%~+36%)로 **스스로 선언한다.** 10% 관문은 그 공정의 **정상 변동조차**\n * \"다른 모델\" 로 판정해 보정을 끊는다. 반대로 1% 변동도 치명적인 공정에는 10% 가 너무 느슨하다.\n *\n * 그래서 허용오차는 **그 공정이 선언한 변동폭에서 나온다**(§toleranceOf). 선언이 있으면 선언이 이기고,\n * 없을 때만 이 값으로 떨어진다 — 도메인 설정을 새로 만들지 않는다(현장이 이미 말한 것을 읽는다).\n */\nexport const BASIS_TOLERANCE = 0.1\n\nexport interface ModelBasis {\n /** 작업 종류 → 실측 소요(ms). `buildMeasuredEstimator().learned`. */\n measured?: Record<string, number>\n /** 자원 종류 → 속도(m/s). `buildTravelEstimator().speedsByKind`. */\n speeds?: Record<string, number>\n /**\n * 선언된 공정 — key + ISO 8601 원문(문자열은 그대로 비교한다: 선언이 바뀌면 다른 모델).\n *\n * **변동폭도 지문에 든다.** 변동폭이 허용오차를 정하므로(§toleranceOf), 그것이 바뀌었는데 \"같은 모델\"\n * 이라고 하면 판정의 잣대가 알리지 않고 바뀐 채로 학습이 이어진다.\n */\n declared?: DeclaredOperation[]\n}\n\n/** 선언된 공정 하나 — 소요와 변동폭(모델의 `operations` 원형에서 필요한 만큼만). */\nexport interface DeclaredOperation {\n key: string\n duration?: string\n variability?: { distribution?: string; min?: string; max?: string; mode?: string }\n}\n\n/** ISO 8601 기간 → 밀리초. 읽을 수 없으면 `undefined`(짐작해 고치지 않는다). */\nfunction isoMs(iso?: string): number | undefined {\n if (!iso) return undefined\n const m = /^P(?:(\\d+)D)?(?:T(?:(\\d+)H)?(?:(\\d+)M)?(?:([\\d.]+)S)?)?$/.exec(iso)\n if (!m || !m.slice(1).some(Boolean)) return undefined\n return ((Number(m[1] ?? 0) * 24 + Number(m[2] ?? 0)) * 60 + Number(m[3] ?? 0)) * 60_000 + Number(m[4] ?? 0) * 1000\n}\n\n/**\n * 작업 종류별 **허용오차** — 그 공정이 선언한 변동폭에서 나온다.\n *\n * 실측 p50 은 정상 변동 안에서 흔들린다. 그 흔들림을 \"다른 모델\" 로 읽으면 보정이 자꾸 끊기고, 사용자는\n * 학습이 왜 안 듣는지 모른다. 얼마나 흔들리는 것이 정상인지는 **현장이 이미 선언했다**(`variability`).\n *\n * 반폭을 쓴다(`(max − min) / 2 / 소요`) — 중앙에서 어느 쪽으로든 그만큼은 정상이라는 뜻이다.\n * 선언이 없거나 읽을 수 없으면 기본값으로 떨어진다(짐작해 만들지 않는다).\n */\nexport function toleranceOf(declared?: DeclaredOperation[]): Record<string, number> {\n const out: Record<string, number> = {}\n for (const o of declared ?? []) {\n if (!o?.key) continue\n const base = isoMs(o.duration)\n const min = isoMs(o.variability?.min)\n const max = isoMs(o.variability?.max)\n if (!base || min === undefined || max === undefined || !(max > min)) continue\n const half = (max - min) / 2 / base\n if (Number.isFinite(half) && half > 0) out[o.key] = half\n }\n return out\n}\n\n/** 뜻 있는 수만 남긴다 — 0·음수·NaN 은 모델이 아니다. */\nfunction clean(map?: Record<string, number>): Record<string, number> {\n const out: Record<string, number> = {}\n for (const [k, v] of Object.entries(map ?? {})) if (Number.isFinite(v) && v > 0) out[k] = v\n return out\n}\n\nfunction declaredMap(list?: DeclaredOperation[]): Record<string, string> {\n const out: Record<string, string> = {}\n for (const o of list ?? []) {\n if (!o?.key) continue\n const v = o.variability\n /* 변동폭까지 한 문장으로 — 순서를 고정해 같은 선언이 같은 문자열이 되게 한다. */\n const spread = v ? `~${v.distribution ?? '?'}:${v.min ?? ''}..${v.max ?? ''}@${v.mode ?? ''}` : ''\n if (o.duration || spread) out[o.key] = `${o.duration ?? ''}${spread}`\n }\n return out\n}\n\n/** 두 수치 지도가 실질적으로 다른가 — 키 집합이 다르거나, 값이 허용오차를 넘어 다르면 다르다. */\nfunction numericChanged(a: Record<string, number>, b: Record<string, number>, tolOf: (key: string) => number): boolean {\n const keys = new Set([...Object.keys(a), ...Object.keys(b)])\n for (const k of keys) {\n const x = a[k]\n const y = b[k]\n if (x === undefined || y === undefined) return true // 새로 배운 종류·사라진 종류 = 다른 모델\n const base = Math.max(Math.abs(x), Math.abs(y))\n if (base > 0 && Math.abs(x - y) / base > tolOf(k)) return true\n }\n return false\n}\n\n/**\n * 모델이 달라졌는가 — 보정을 적용하기 전의 관문.\n * 한쪽 근거를 모르면 **판정하지 않는다**(false) — 모르는 것을 근거로 학습을 끊지 않는다.\n */\nexport function modelBasisChanged(before?: ModelBasis, after?: ModelBasis, tol: number = BASIS_TOLERANCE): boolean {\n if (!before || !after) return false\n /*\n * 허용오차는 **선언이 정한다** — 배운 시점과 지금의 선언이 다를 수 있으므로 둘 중 **넓은 쪽**을 쓴다.\n * 좁은 쪽을 쓰면 선언을 넓힌 직후에 정상 변동이 \"다른 모델\" 로 잡힌다(그 변경 자체는 아래 선언 비교가\n * 이미 잡으므로, 여기서 또 잡을 필요가 없다).\n */\n const tolBefore = toleranceOf(before.declared)\n const tolAfter = toleranceOf(after.declared)\n const tolOf = (key: string): number => Math.max(tolBefore[key] ?? tol, tolAfter[key] ?? tol)\n if (numericChanged(clean(before.measured), clean(after.measured), tolOf)) return true\n /* 속도는 자원 종류별이고 선언된 변동폭이 없다 — 기본값이 그대로 쓰인다. */\n if (numericChanged(clean(before.speeds), clean(after.speeds), () => tol)) return true\n const da = declaredMap(before.declared)\n const db = declaredMap(after.declared)\n const keys = new Set([...Object.keys(da), ...Object.keys(db)])\n for (const k of keys) if (da[k] !== db[k]) return true\n return false\n}\n\n/**\n * 사람이 읽는 근거 요약 — **로그·진단용**(비교의 근거가 아니다).\n * 지문으로 같음을 판정하지 않는 이유는 위 주석 참조. 두 근거가 왜 달라졌는지 눈으로 보려면 이것을 찍는다.\n */\nexport function describeModelBasis(basis?: ModelBasis): string {\n if (!basis) return 'unknown'\n const seg: string[] = []\n const m = Object.entries(clean(basis.measured))\n .map(([k, v]) => `${k}=${Math.round(v / 1000)}s`)\n .sort()\n if (m.length) seg.push(`measured(${m.join(',')})`)\n const s = Object.entries(clean(basis.speeds))\n .map(([k, v]) => `${k}=${Math.round(v * 10) / 10}m/s`)\n .sort()\n if (s.length) seg.push(`speed(${s.join(',')})`)\n const d = Object.entries(declaredMap(basis.declared))\n .map(([k, v]) => `${k}=${v}`)\n .sort()\n if (d.length) seg.push(`declared(${d.join(',')})`)\n return seg.length ? seg.join(' ') : 'kernel defaults only'\n}\n"]}
@@ -24,7 +24,7 @@ export interface ModelGapSource {
24
24
  *
25
25
  * `unknownEquipment` 를 루트에서 읽고 있었다(2026-08-20에 잡음). 커널은 그것을 `EnergyState` 안에
26
26
  * 담으므로, 루트에서 읽으면 **영원히 `null`** 이다 — 오류도 없이 「에너지 트윈이 아니다」로 읽힌다.
27
- * 나만 읽는 필드를 잘못된 경로에서 읽으면 이렇게 조용히 사라진다.
27
+ * 나만 읽는 필드를 잘못된 경로에서 읽으면 이렇게 오류 없이 사라진다.
28
28
  *
29
29
  * 계량 지점의 `origin` 은 **없을 수 있다**(옛 스냅샷에서 이어받은 지점). 그때는 「선언되었다」가
30
30
  * 아니라 **모른다**다.
@@ -42,7 +42,7 @@ export interface ModelGapSource {
42
42
  * 화면에 내는 id 수 상한.
43
43
  *
44
44
  * 오타 하나를 찾는 데 스무 개면 충분하고, 원천이 수천 개를 보내는 경우(모델을 아예 안 맞춘 경우)에
45
- * 목록으로 화면을 채울 이유가 없다. **넘친 수는 함께 낸다** — 조용히 자르지 않는다.
45
+ * 목록으로 화면을 채울 이유가 없다. **넘친 수는 함께 낸다** — 알리지 않고 자르지 않는다.
46
46
  */
47
47
  export declare const GAP_ID_LIMIT = 20;
48
48
  export interface ModelGapAxis {
@@ -69,7 +69,7 @@ export interface ModelGapView {
69
69
  * EMS 계량 지점.
70
70
  *
71
71
  * ── 이 축이 가장 무겁다 ────────────────────────────────────────────────────
72
- * 모르는 `meterId` 는 조용히 새 계량기가 되고 그 kW 가 그 현장의 **수요 구간·피크·요금 판정**에
72
+ * 모르는 `meterId` 는 알리지 않고 새 계량기가 되고 그 kW 가 그 현장의 **수요 구간·피크·요금 판정**에
73
73
  * 더해진다. 오타 하나면 진짜 계량기는 멈추고 유령이 누적된다 — 숫자는 그럴듯하고 틀린다. 자리 오타는
74
74
  * 스키매틱이 비는 것으로 드러나지만, 이건 그럴듯한 숫자로 나온다.
75
75
  *
@@ -33,7 +33,7 @@ exports.modelGapOf = modelGapOf;
33
33
  * 화면에 내는 id 수 상한.
34
34
  *
35
35
  * 오타 하나를 찾는 데 스무 개면 충분하고, 원천이 수천 개를 보내는 경우(모델을 아예 안 맞춘 경우)에
36
- * 목록으로 화면을 채울 이유가 없다. **넘친 수는 함께 낸다** — 조용히 자르지 않는다.
36
+ * 목록으로 화면을 채울 이유가 없다. **넘친 수는 함께 낸다** — 알리지 않고 자르지 않는다.
37
37
  */
38
38
  exports.GAP_ID_LIMIT = 20;
39
39
  /** 계량기 축이 아직 덮이지 않는 이유 — 화면이 그대로 말할 수 있게 이름을 여기 둔다. */
@@ -1 +1 @@
1
- {"version":3,"file":"model-gap.js","sourceRoot":"","sources":["../../server/engine/model-gap.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;;;AA6GH,gCAgCC;AAnHD;;;;;GAKG;AACU,QAAA,YAAY,GAAG,EAAE,CAAA;AAoD9B,uDAAuD;AAC1C,QAAA,kBAAkB,GAAG,QAAQ,CAAA;AAE1C,SAAS,MAAM,CAAC,IAAoD;IAClE,MAAM,GAAG,GAAa,EAAE,CAAA;IACxB,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,IAAI,OAAO,GAAG,CAAC,CAAA;IACf,KAAK,MAAM,CAAC,IAAI,IAAI,IAAI,EAAE,EAAE,CAAC;QAC3B,IAAI,CAAC,EAAE,MAAM,KAAK,UAAU,EAAE,CAAC;YAC7B,KAAK,EAAE,CAAA;YACP,IAAI,GAAG,CAAC,MAAM,GAAG,oBAAY;gBAAE,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;QAC7D,CAAC;aAAM,IAAI,CAAC,EAAE,MAAM,KAAK,QAAQ,EAAE,CAAC;YAClC,kDAAkD;YAClD,OAAO,EAAE,CAAA;QACX,CAAC;IACH,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAA;AACvE,CAAC;AAED;;;;;GAKG;AACH,SAAgB,UAAU,CAAC,QAA2C;IACpE,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAA;IAE1B,MAAM,SAAS,GAAG,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAA;IAC5C,MAAM,SAAS,GAAG,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAA;IAC5C,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;IACxC,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAE9C,OAAO;QACL,SAAS;QACT,SAAS;QACT,OAAO;QACP,MAAM;QACN,KAAK,EAAE,SAAS,CAAC,KAAK,GAAG,SAAS,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK;QACvE,SAAS,EAAE,CAAC,QAAQ,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YAC9C,SAAS,EAAE,CAAC,CAAC,SAAS;YACtB,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,8CAA8C;YAC9C,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,QAAkB,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI;SAC1F,CAAC,CAAC;QACH;;;WAGG;QACH,uBAAuB,EACrB,OAAO,QAAQ,CAAC,MAAM,EAAE,gBAAgB,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI;QACvH;;;WAGG;QACH,UAAU,EAAE,EAAE;KACf,CAAA;AACH,CAAC","sourcesContent":["/*\n * **원본이 말한 적 없는 것** — 미해결 참조를 세는 한 자리. 순수(import 없음).\n *\n * ── 무엇을 답하나 ───────────────────────────────────────────────────────────\n * 원천이 우리 모델에 없는 id 를 보내면 어떻게 되나. 유입 문(`ingestCanonicalRecords`)은 **거부하지\n * 않는다** — 실측으로 확인했다(2026-08-20):\n *\n * 모르는 로케이션 「오타-99」 → 통과\n * 모르는 bizStep 「PUTAWAY_X17」 → 통과\n * 모르는 계량기 「MMXU-오타-99」 → 통과\n *\n * 거부하지 않는 것이 옳다. 원천이 모델보다 먼저 새 로케이션을 말하는 것은 정상이고(구조 갱신은 늦게 온다),\n * 거부하면 그 선행 계측을 잃는다. 그래서 커널은 **흡수하고 표시한다** — 마스터에 없던 로케이션은\n * `origin: 'observed'` 로 승격되어 스냅샷에 실린다(`ObservedReducer.touchLocation`).\n *\n * ── 왜 이 파일이 있나 ───────────────────────────────────────────────────────\n * 그 표시를 읽는 곳이 **모델 인스펙션 화면 하나**였다. 그런데 이 사실을 필요로 하는 사람은 「이 트윈이\n * 현장과 맞춰지고 있나」를 보는 사람이다 — 원천이 우리가 모르는 로케이션만 보내고 있으면 트윈은 도는데\n * 스키매틱은 비어 있고, 그것이 우리가 「빈 공장」이라 불러 온 상태다.\n *\n * 도출을 두 곳에 적으면 두 화면이 다른 수를 말하게 된다. 그래서 여기 한 벌만 둔다.\n *\n * ── 판정을 바꾸지 않는다 ────────────────────────────────────────────────────\n * 동기화 판정(`syncVerdictOf`)은 **피드**를 말한다: 오고 있나, 통과하나. 이 갭은 **모델**을 말한다:\n * 원천이 아는 것을 우리가 선언했나. 둘을 한 판정에 섞으면 「유입은 정상인데 모델이 뒤처졌다」와\n * 「유입이 막혔다」가 같은 색으로 보이고, 처방이 완전히 다르다.\n */\n\n/** 커널 스냅샷에서 우리가 읽는 것만. 나머지 필드는 알 필요가 없다. */\nexport interface ModelGapSource {\n locations?: { id?: string; origin?: string }[]\n equipment?: { id?: string; origin?: string }[]\n persons?: { id?: string; origin?: string }[]\n /** 커널이 담을 줄 몰라 세어 둔 사건 — 종류별 개수와 처음·마지막 시각. */\n unhandled?: { eventType: string; count: number; firstAtMs?: number; lastAtMs?: number }[]\n /**\n * EMS 상태 — **`energy` 안에 있다.**\n *\n * `unknownEquipment` 를 루트에서 읽고 있었다(2026-08-20에 잡음). 커널은 그것을 `EnergyState` 안에\n * 담으므로, 루트에서 읽으면 **영원히 `null`** 이다 — 오류도 없이 「에너지 트윈이 아니다」로 읽힌다.\n * 나만 읽는 필드를 잘못된 경로에서 읽으면 이렇게 조용히 사라진다.\n *\n * 계량 지점의 `origin` 은 **없을 수 있다**(옛 스냅샷에서 이어받은 지점). 그때는 「선언되었다」가\n * 아니라 **모른다**다.\n */\n energy?: {\n points?: { id?: string; origin?: string }[]\n /** 모델이 모르는 설비가 에너지 상태를 보내 온 횟수. 0 이면 커널이 이 칸을 만들지 않는다. */\n unknownEquipment?: number\n }\n}\n\n/**\n * 화면에 내는 id 수 상한.\n *\n * 오타 하나를 찾는 데 스무 개면 충분하고, 원천이 수천 개를 보내는 경우(모델을 아예 안 맞춘 경우)에\n * 목록으로 화면을 채울 이유가 없다. **넘친 수는 함께 낸다** — 조용히 자르지 않는다.\n */\nexport const GAP_ID_LIMIT = 20\n\nexport interface ModelGapAxis {\n /** 관측으로만 알게 된 id 수. */\n count: number\n /** 그중 화면에 싣는 것(최대 `GAP_ID_LIMIT`). */\n ids: string[]\n /** 상한에서 밀려난 수. 0 이면 전부 실었다. */\n more: number\n /**\n * 출처를 **모르는** 수 — 표시가 없는 항목.\n *\n * 「선언되었다」로 세지 않는다. 계량기는 이 표시가 늦게 생겼으므로 옛 스냅샷에서 이어받은 지점에는\n * 칸이 없다(`undefined`). 그것을 마스터로 세면 오타를 놓치고, 관측으로 세면 정상을 문제로 만든다.\n * 둘 다 틀리므로 **모른다고 센다.**\n */\n unknown: number\n}\n\nexport interface ModelGapView {\n locations: ModelGapAxis\n equipment: ModelGapAxis\n persons: ModelGapAxis\n /**\n * EMS 계량 지점.\n *\n * ── 이 축이 가장 무겁다 ────────────────────────────────────────────────────\n * 모르는 `meterId` 는 조용히 새 계량기가 되고 그 kW 가 그 현장의 **수요 구간·피크·요금 판정**에\n * 더해진다. 오타 하나면 진짜 계량기는 멈추고 유령이 누적된다 — 숫자는 그럴듯하고 틀린다. 자리 오타는\n * 스키매틱이 비는 것으로 드러나지만, 이건 그럴듯한 숫자로 나온다.\n *\n * 커널이 `origin` 을 **표본마다 다시 맞춘다** — 선언의 정본은 모델이고 모델은 채택으로 늘어나므로,\n * 한 번 찍고 두면 나중에 선언된 계량기가 영원히 `observed` 로 남는다. 그래서 이 값은 「지금 모델에\n * 있는가」의 답이고, 「한 번 오타였다」의 답이 아니다.\n */\n meters: ModelGapAxis\n /** 세 축의 합 — 화면이 「있나 없나」를 한 번에 묻기 위한 값. */\n total: number\n /** 커널이 담을 줄 모른 사건들(종류별). 참조 문제와 다른 사실이므로 따로 낸다. */\n unhandled: { eventType: string; count: number; lastAt: string | null }[]\n /** EMS: 모델이 모르는 설비의 에너지 보고 횟수. 없으면 `null`(0 과 구별한다). */\n unknownEquipmentReports: number | null\n /**\n * 아직 덮지 못하는 축 — **모르는 것을 모른다고 말한다.**\n *\n * 계량기는 커널 0.7.39 부터 덮인다(그 전에는 `origin` 표시가 없어 오타를 찾아 줄 수 없었다).\n * 비어 있는 것이 정상이고, 새 축이 표시 없이 들어오면 여기에 이름을 넣는다 — 화면이 「다 찾아\n * 준다」고 말하지 않게.\n */\n notCovered: string[]\n}\n\n/** 계량기 축이 아직 덮이지 않는 이유 — 화면이 그대로 말할 수 있게 이름을 여기 둔다. */\nexport const NOT_COVERED_METERS = 'meters'\n\nfunction axisOf(rows: { id?: string; origin?: string }[] | undefined): ModelGapAxis {\n const ids: string[] = []\n let count = 0\n let unknown = 0\n for (const r of rows ?? []) {\n if (r?.origin === 'observed') {\n count++\n if (ids.length < GAP_ID_LIMIT) ids.push(String(r.id ?? ''))\n } else if (r?.origin !== 'master') {\n /* 표시가 없다 — 마스터로도 관측으로도 세지 않는다. 모르는 것은 모른다고 센다. */\n unknown++\n }\n }\n return { count, ids, more: Math.max(0, count - ids.length), unknown }\n}\n\n/**\n * 스냅샷 → 모델 갭.\n *\n * 스냅샷이 없으면 `null` 이다 — **0 이 아니다.** 트윈이 돌지 않으면 비교할 상대가 없고, 「없다」와\n * 「세어 보지 못했다」는 다른 사실이다(모델 인스펙션이 같은 규율을 쓴다).\n */\nexport function modelGapOf(snapshot: ModelGapSource | null | undefined): ModelGapView | null {\n if (!snapshot) return null\n\n const locations = axisOf(snapshot.locations)\n const equipment = axisOf(snapshot.equipment)\n const persons = axisOf(snapshot.persons)\n const meters = axisOf(snapshot.energy?.points)\n\n return {\n locations,\n equipment,\n persons,\n meters,\n total: locations.count + equipment.count + persons.count + meters.count,\n unhandled: (snapshot.unhandled ?? []).map(u => ({\n eventType: u.eventType,\n count: u.count,\n /* 마지막 시각이 멈춘 종류는 이미 고쳐진 것이다. 없으면 지어내지 않는다. */\n lastAt: Number.isFinite(u.lastAtMs) ? new Date(u.lastAtMs as number).toISOString() : null\n })),\n /*\n * 커널은 0 이면 이 칸을 만들지 않는다. 그래서 「없음」은 두 뜻이다 — 에너지 트윈이 아니거나, 세었고\n * 0 이었다. 둘을 가르려면 `energy` 자체가 있는지를 본다: 있으면 세었고 0 이다.\n */\n unknownEquipmentReports:\n typeof snapshot.energy?.unknownEquipment === 'number' ? snapshot.energy.unknownEquipment : snapshot.energy ? 0 : null,\n /*\n * 이제 계량기도 덮는다(커널 0.7.39 가 `MeterPointState.origin` 을 낸다). 덮지 못하는 축이 생기면\n * 다시 여기에 이름을 넣는다 — 화면이 「다 찾아 준다」고 말하지 않게.\n */\n notCovered: []\n }\n}\n"]}
1
+ {"version":3,"file":"model-gap.js","sourceRoot":"","sources":["../../server/engine/model-gap.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;;;AA6GH,gCAgCC;AAnHD;;;;;GAKG;AACU,QAAA,YAAY,GAAG,EAAE,CAAA;AAoD9B,uDAAuD;AAC1C,QAAA,kBAAkB,GAAG,QAAQ,CAAA;AAE1C,SAAS,MAAM,CAAC,IAAoD;IAClE,MAAM,GAAG,GAAa,EAAE,CAAA;IACxB,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,IAAI,OAAO,GAAG,CAAC,CAAA;IACf,KAAK,MAAM,CAAC,IAAI,IAAI,IAAI,EAAE,EAAE,CAAC;QAC3B,IAAI,CAAC,EAAE,MAAM,KAAK,UAAU,EAAE,CAAC;YAC7B,KAAK,EAAE,CAAA;YACP,IAAI,GAAG,CAAC,MAAM,GAAG,oBAAY;gBAAE,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;QAC7D,CAAC;aAAM,IAAI,CAAC,EAAE,MAAM,KAAK,QAAQ,EAAE,CAAC;YAClC,kDAAkD;YAClD,OAAO,EAAE,CAAA;QACX,CAAC;IACH,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAA;AACvE,CAAC;AAED;;;;;GAKG;AACH,SAAgB,UAAU,CAAC,QAA2C;IACpE,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAA;IAE1B,MAAM,SAAS,GAAG,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAA;IAC5C,MAAM,SAAS,GAAG,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAA;IAC5C,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;IACxC,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAE9C,OAAO;QACL,SAAS;QACT,SAAS;QACT,OAAO;QACP,MAAM;QACN,KAAK,EAAE,SAAS,CAAC,KAAK,GAAG,SAAS,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK;QACvE,SAAS,EAAE,CAAC,QAAQ,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YAC9C,SAAS,EAAE,CAAC,CAAC,SAAS;YACtB,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,8CAA8C;YAC9C,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,QAAkB,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI;SAC1F,CAAC,CAAC;QACH;;;WAGG;QACH,uBAAuB,EACrB,OAAO,QAAQ,CAAC,MAAM,EAAE,gBAAgB,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI;QACvH;;;WAGG;QACH,UAAU,EAAE,EAAE;KACf,CAAA;AACH,CAAC","sourcesContent":["/*\n * **원본이 말한 적 없는 것** — 미해결 참조를 세는 한 자리. 순수(import 없음).\n *\n * ── 무엇을 답하나 ───────────────────────────────────────────────────────────\n * 원천이 우리 모델에 없는 id 를 보내면 어떻게 되나. 유입 문(`ingestCanonicalRecords`)은 **거부하지\n * 않는다** — 실측으로 확인했다(2026-08-20):\n *\n * 모르는 로케이션 「오타-99」 → 통과\n * 모르는 bizStep 「PUTAWAY_X17」 → 통과\n * 모르는 계량기 「MMXU-오타-99」 → 통과\n *\n * 거부하지 않는 것이 옳다. 원천이 모델보다 먼저 새 로케이션을 말하는 것은 정상이고(구조 갱신은 늦게 온다),\n * 거부하면 그 선행 계측을 잃는다. 그래서 커널은 **흡수하고 표시한다** — 마스터에 없던 로케이션은\n * `origin: 'observed'` 로 승격되어 스냅샷에 실린다(`ObservedReducer.touchLocation`).\n *\n * ── 왜 이 파일이 있나 ───────────────────────────────────────────────────────\n * 그 표시를 읽는 곳이 **모델 인스펙션 화면 하나**였다. 그런데 이 사실을 필요로 하는 사람은 「이 트윈이\n * 현장과 맞춰지고 있나」를 보는 사람이다 — 원천이 우리가 모르는 로케이션만 보내고 있으면 트윈은 도는데\n * 스키매틱은 비어 있고, 그것이 우리가 「빈 공장」이라 불러 온 상태다.\n *\n * 도출을 두 곳에 적으면 두 화면이 다른 수를 말하게 된다. 그래서 여기 한 벌만 둔다.\n *\n * ── 판정을 바꾸지 않는다 ────────────────────────────────────────────────────\n * 동기화 판정(`syncVerdictOf`)은 **피드**를 말한다: 오고 있나, 통과하나. 이 갭은 **모델**을 말한다:\n * 원천이 아는 것을 우리가 선언했나. 둘을 한 판정에 섞으면 「유입은 정상인데 모델이 뒤처졌다」와\n * 「유입이 막혔다」가 같은 색으로 보이고, 처방이 완전히 다르다.\n */\n\n/** 커널 스냅샷에서 우리가 읽는 것만. 나머지 필드는 알 필요가 없다. */\nexport interface ModelGapSource {\n locations?: { id?: string; origin?: string }[]\n equipment?: { id?: string; origin?: string }[]\n persons?: { id?: string; origin?: string }[]\n /** 커널이 담을 줄 몰라 세어 둔 사건 — 종류별 개수와 처음·마지막 시각. */\n unhandled?: { eventType: string; count: number; firstAtMs?: number; lastAtMs?: number }[]\n /**\n * EMS 상태 — **`energy` 안에 있다.**\n *\n * `unknownEquipment` 를 루트에서 읽고 있었다(2026-08-20에 잡음). 커널은 그것을 `EnergyState` 안에\n * 담으므로, 루트에서 읽으면 **영원히 `null`** 이다 — 오류도 없이 「에너지 트윈이 아니다」로 읽힌다.\n * 나만 읽는 필드를 잘못된 경로에서 읽으면 이렇게 오류 없이 사라진다.\n *\n * 계량 지점의 `origin` 은 **없을 수 있다**(옛 스냅샷에서 이어받은 지점). 그때는 「선언되었다」가\n * 아니라 **모른다**다.\n */\n energy?: {\n points?: { id?: string; origin?: string }[]\n /** 모델이 모르는 설비가 에너지 상태를 보내 온 횟수. 0 이면 커널이 이 칸을 만들지 않는다. */\n unknownEquipment?: number\n }\n}\n\n/**\n * 화면에 내는 id 수 상한.\n *\n * 오타 하나를 찾는 데 스무 개면 충분하고, 원천이 수천 개를 보내는 경우(모델을 아예 안 맞춘 경우)에\n * 목록으로 화면을 채울 이유가 없다. **넘친 수는 함께 낸다** — 알리지 않고 자르지 않는다.\n */\nexport const GAP_ID_LIMIT = 20\n\nexport interface ModelGapAxis {\n /** 관측으로만 알게 된 id 수. */\n count: number\n /** 그중 화면에 싣는 것(최대 `GAP_ID_LIMIT`). */\n ids: string[]\n /** 상한에서 밀려난 수. 0 이면 전부 실었다. */\n more: number\n /**\n * 출처를 **모르는** 수 — 표시가 없는 항목.\n *\n * 「선언되었다」로 세지 않는다. 계량기는 이 표시가 늦게 생겼으므로 옛 스냅샷에서 이어받은 지점에는\n * 칸이 없다(`undefined`). 그것을 마스터로 세면 오타를 놓치고, 관측으로 세면 정상을 문제로 만든다.\n * 둘 다 틀리므로 **모른다고 센다.**\n */\n unknown: number\n}\n\nexport interface ModelGapView {\n locations: ModelGapAxis\n equipment: ModelGapAxis\n persons: ModelGapAxis\n /**\n * EMS 계량 지점.\n *\n * ── 이 축이 가장 무겁다 ────────────────────────────────────────────────────\n * 모르는 `meterId` 는 알리지 않고 새 계량기가 되고 그 kW 가 그 현장의 **수요 구간·피크·요금 판정**에\n * 더해진다. 오타 하나면 진짜 계량기는 멈추고 유령이 누적된다 — 숫자는 그럴듯하고 틀린다. 자리 오타는\n * 스키매틱이 비는 것으로 드러나지만, 이건 그럴듯한 숫자로 나온다.\n *\n * 커널이 `origin` 을 **표본마다 다시 맞춘다** — 선언의 정본은 모델이고 모델은 채택으로 늘어나므로,\n * 한 번 찍고 두면 나중에 선언된 계량기가 영원히 `observed` 로 남는다. 그래서 이 값은 「지금 모델에\n * 있는가」의 답이고, 「한 번 오타였다」의 답이 아니다.\n */\n meters: ModelGapAxis\n /** 세 축의 합 — 화면이 「있나 없나」를 한 번에 묻기 위한 값. */\n total: number\n /** 커널이 담을 줄 모른 사건들(종류별). 참조 문제와 다른 사실이므로 따로 낸다. */\n unhandled: { eventType: string; count: number; lastAt: string | null }[]\n /** EMS: 모델이 모르는 설비의 에너지 보고 횟수. 없으면 `null`(0 과 구별한다). */\n unknownEquipmentReports: number | null\n /**\n * 아직 덮지 못하는 축 — **모르는 것을 모른다고 말한다.**\n *\n * 계량기는 커널 0.7.39 부터 덮인다(그 전에는 `origin` 표시가 없어 오타를 찾아 줄 수 없었다).\n * 비어 있는 것이 정상이고, 새 축이 표시 없이 들어오면 여기에 이름을 넣는다 — 화면이 「다 찾아\n * 준다」고 말하지 않게.\n */\n notCovered: string[]\n}\n\n/** 계량기 축이 아직 덮이지 않는 이유 — 화면이 그대로 말할 수 있게 이름을 여기 둔다. */\nexport const NOT_COVERED_METERS = 'meters'\n\nfunction axisOf(rows: { id?: string; origin?: string }[] | undefined): ModelGapAxis {\n const ids: string[] = []\n let count = 0\n let unknown = 0\n for (const r of rows ?? []) {\n if (r?.origin === 'observed') {\n count++\n if (ids.length < GAP_ID_LIMIT) ids.push(String(r.id ?? ''))\n } else if (r?.origin !== 'master') {\n /* 표시가 없다 — 마스터로도 관측으로도 세지 않는다. 모르는 것은 모른다고 센다. */\n unknown++\n }\n }\n return { count, ids, more: Math.max(0, count - ids.length), unknown }\n}\n\n/**\n * 스냅샷 → 모델 갭.\n *\n * 스냅샷이 없으면 `null` 이다 — **0 이 아니다.** 트윈이 돌지 않으면 비교할 상대가 없고, 「없다」와\n * 「세어 보지 못했다」는 다른 사실이다(모델 인스펙션이 같은 규율을 쓴다).\n */\nexport function modelGapOf(snapshot: ModelGapSource | null | undefined): ModelGapView | null {\n if (!snapshot) return null\n\n const locations = axisOf(snapshot.locations)\n const equipment = axisOf(snapshot.equipment)\n const persons = axisOf(snapshot.persons)\n const meters = axisOf(snapshot.energy?.points)\n\n return {\n locations,\n equipment,\n persons,\n meters,\n total: locations.count + equipment.count + persons.count + meters.count,\n unhandled: (snapshot.unhandled ?? []).map(u => ({\n eventType: u.eventType,\n count: u.count,\n /* 마지막 시각이 멈춘 종류는 이미 고쳐진 것이다. 없으면 지어내지 않는다. */\n lastAt: Number.isFinite(u.lastAtMs) ? new Date(u.lastAtMs as number).toISOString() : null\n })),\n /*\n * 커널은 0 이면 이 칸을 만들지 않는다. 그래서 「없음」은 두 뜻이다 — 에너지 트윈이 아니거나, 세었고\n * 0 이었다. 둘을 가르려면 `energy` 자체가 있는지를 본다: 있으면 세었고 0 이다.\n */\n unknownEquipmentReports:\n typeof snapshot.energy?.unknownEquipment === 'number' ? snapshot.energy.unknownEquipment : snapshot.energy ? 0 : null,\n /*\n * 이제 계량기도 덮는다(커널 0.7.39 가 `MeterPointState.origin` 을 낸다). 덮지 못하는 축이 생기면\n * 다시 여기에 이름을 넣는다 — 화면이 「다 찾아 준다」고 말하지 않게.\n */\n notCovered: []\n }\n}\n"]}
@@ -22,7 +22,7 @@ exports.canonicalModel = canonicalModel;
22
22
  *
23
23
  * ── 왜 자리마다 고치지 않나 ─────────────────────────────────────────────────
24
24
  * 저장된 모델를 읽는 자리가 **27곳**이다. 자리마다 리더를 쓰게 하면 다음에 추가되는 자리가 또 빠진다 —
25
- * 그리고 빠진 것은 오류가 아니라 **빈 공장**이라 조용하다. 그래서 **DB에서 읽는 순간 한 번** 정규화한다
25
+ * 그리고 빠진 것은 오류가 아니라 **빈 공장**이라 들어온 것이 없다. 그래서 **DB에서 읽는 순간 한 번** 정규화한다
26
26
  * (엔티티 컬럼 변환기). 그 뒤로는 코드가 세대를 알 필요가 없다.
27
27
  *
28
28
  * ── 쓰기는 건드리지 않는다 ──────────────────────────────────────────────────
@@ -1 +1 @@
1
- {"version":3,"file":"model-vocabulary.js","sourceRoot":"","sources":["../../server/engine/model-vocabulary.ts"],"names":[],"mappings":";;;AAyCA,wCA6BC;AAtED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,sDAA6E;AAE7E,wDAAwD;AACxD,MAAM,YAAY,GAAG,CAAC,OAAO,EAAE,QAAQ,EAAE,eAAe,CAAU,CAAA,CAAC,yEAAyE;AAE5I,yDAAyD;AACzD,MAAM,UAAU,GAAG,CAAC,kBAAkB,EAAE,kBAAkB,EAAE,cAAc,EAAE,iBAAiB,CAAU,CAAA;AAEvG;;;;;GAKG;AACH,SAAgB,cAAc,CAAU,KAAQ;IAC9C,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IAE7E,MAAM,IAAI,GAA4B,EAAE,CAAA;IACxC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC,EAAE,CAAC;QACtE,IAAK,YAAkC,CAAC,QAAQ,CAAC,CAAC,CAAC;YAAE,SAAQ;QAC7D,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;IACb,CAAC;IAED;;;;;;;OAOG;IACH,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAA;QACtB,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;YAAE,SAAQ;QAClC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAC9B,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,WAAW;YACpD,CAAC,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;YACrE,CAAC,CAAC,CAAC,CACN,CAAA;IACH,CAAC;IAED,uDAAuD;IACvD,OAAO,EAAE,GAAG,IAAI,EAAE,SAAS,EAAE,IAAA,gCAAkB,EAAC,KAAY,CAAC,EAAE,SAAS,EAAE,IAAA,gCAAkB,EAAC,KAAY,CAAC,EAAO,CAAA;AACnH,CAAC;AAED;;;;;GAKG;AACU,QAAA,sBAAsB,GAAG;IACpC,IAAI,EAAE,CAAC,KAAU,EAAE,EAAE,CAAC,cAAc,CAAC,KAAK,CAAC;IAC3C,EAAE,EAAE,CAAC,KAAU,EAAE,EAAE,CAAC,KAAK;CAC1B,CAAA","sourcesContent":["/*\n * 저장된 트윈 모델의 **어휘 세대를 흡수한다** — 읽는 순간 한 어휘로. 순수.\n *\n * ── 계기 (2026-08-06, 사용자 신고) ───────────────────────────────────────────\n * 트윈 모델러의 엔티티 목록이 **통째로 비어 있었다.** 오류도 없었다. 원인은 이 인스턴스의 저장된 모델가\n * 옛 어휘였고 화면과 호스트는 새 어휘(`locations`/`equipment`)를 읽었기 때문이다. 그 옛 이름들은\n * 아래 `RETIRED_KEYS` 에 있다 — 흡수할 세대의 이름을 적지 않고는 이 파일을 쓸 수 없다.\n *\n * 커널에는 이미 세대를 흡수하는 리더가 있다(`readBoardLocations`·`readBoardEquipment` — 세 세대\n * `movers`→`equipmentList`→`equipment`, 실측 주석에 \"저장된 모델 23개 중 13개가 `movers`\"). 그런데 **그\n * 리더를 거치는 곳이 목록뿐**이었다. 상세·구조 서명·주행 추정기·설비 동기는 컬렉션을 직접 읽어\n * **오류 없이 빈 공장**으로 읽었다:\n *\n * · 모델러·팔레트·보드 자동생성 → 엔티티 0(사용자가 본 증상)\n * · 구조 서명 → `N[]M[]` — **서로 다른 두 공장이 구조적으로 같아 보인다**(변경 감지가 죽는다)\n * · 주행 추정기 → 설비 0이라 거리 기반 소요시간을 못 만든다\n * · 설비 동기 → 아는 설비가 0이라 중복 append + 한 모델에 두 세대가 섞인다\n *\n * ── 왜 자리마다 고치지 않나 ─────────────────────────────────────────────────\n * 저장된 모델를 읽는 자리가 **27곳**이다. 자리마다 리더를 쓰게 하면 다음에 추가되는 자리가 또 빠진다 —\n * 그리고 빠진 것은 오류가 아니라 **빈 공장**이라 조용하다. 그래서 **DB에서 읽는 순간 한 번** 정규화한다\n * (엔티티 컬럼 변환기). 그 뒤로는 코드가 세대를 알 필요가 없다.\n *\n * ── 쓰기는 건드리지 않는다 ──────────────────────────────────────────────────\n * 저장은 주는 대로 한다(새 쓰기는 이미 새 어휘다). 읽기가 정규화되므로, 저장된 옛 모델는 **다음에\n * 저장될 때 자연히 새 어휘로 옮겨간다** — 마이그레이션을 따로 실행하지 않는다.\n */\nimport { readBoardLocations, readBoardEquipment } from '@operato/twin-kernel'\n\n/** 흡수하고 **내보내지 않는** 옛 키 — 남겨 두면 소비처가 두 어휘를 다시 보게 된다. */\nconst RETIRED_KEYS = ['nodes', 'movers', 'equipmentList'] as const // vocabulary-guard: allow — 옛 어휘를 흡수하는 것이 이 코드의 일이다(세대 이름을 적어야 흡수할 수 있다)\n\n/** 등급 축들 — 넷이 같은 모양이라 같은 규칙을 쓴다(어느 자원이든 표시명은 `name`). */\nconst CLASS_KEYS = ['personnelClasses', 'equipmentClasses', 'assetClasses', 'materialClasses'] as const\n\n/**\n * 보드를 정본 어휘로 — `locations`·`equipment` 만 남는다. 새 어휘 모델에는 **아무 일도 하지 않는다**\n * (같은 값을 다시 담을 뿐이라 멱등).\n *\n * 모델이 아닌 것(없음·배열·원시값)은 **그대로 통과**시킨다 — 모양을 짐작해 고치지 않는다.\n */\nexport function canonicalModel<T = any>(model: T): T {\n if (!model || typeof model !== 'object' || Array.isArray(model)) return model\n\n const rest: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(model as Record<string, unknown>)) {\n if ((RETIRED_KEYS as readonly string[]).includes(k)) continue\n rest[k] = v\n }\n\n /*\n * **등급의 표시명은 `name` 이다** — 옛 모델는 `description` 에 담았다.\n *\n * 표준 마스터가 등급 표시명을 `description` 으로 주기 때문에 그대로 저장돼 있었고, 화면은 그것을\n * \"설명\" 열에 앉혔다 — 사용자에게는 이름인데 머리글은 설명이라, 같은 것을 축마다 다른 이름으로\n * 부르게 된다. 인제스트 문에서 이미 `name` 으로 옮기고 있지만, **이미 저장된 모델**가 그대로 남아\n * 새 트윈만 고쳐진다. 정규화의 집은 여기 하나다(위 머리말의 이유 그대로).\n */\n for (const key of CLASS_KEYS) {\n const list = rest[key]\n if (!Array.isArray(list)) continue\n rest[key] = list.map((c: any) =>\n c && typeof c === 'object' && !c.name && c.description\n ? (({ description, ...keep }) => ({ ...keep, name: description }))(c)\n : c\n )\n }\n\n /* 리더가 세대를 고른다 — 여기서 `??` 사슬을 다시 적으면 세대 규칙이 두 곳이 된다. */\n return { ...rest, locations: readBoardLocations(model as any), equipment: readBoardEquipment(model as any) } as T\n}\n\n/**\n * 엔티티 컬럼 변환기 — **읽을 때만** 정규화한다.\n *\n * `to`(쓰기)는 손대지 않는다: 저장은 호출부가 준 그대로가 진실이고, 읽기가 정규화되므로 다음 저장에서\n * 자연히 새 어휘가 된다.\n */\nexport const modelColumnTransformer = {\n from: (value: any) => canonicalModel(value),\n to: (value: any) => value\n}\n"]}
1
+ {"version":3,"file":"model-vocabulary.js","sourceRoot":"","sources":["../../server/engine/model-vocabulary.ts"],"names":[],"mappings":";;;AAyCA,wCA6BC;AAtED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,sDAA6E;AAE7E,wDAAwD;AACxD,MAAM,YAAY,GAAG,CAAC,OAAO,EAAE,QAAQ,EAAE,eAAe,CAAU,CAAA,CAAC,yEAAyE;AAE5I,yDAAyD;AACzD,MAAM,UAAU,GAAG,CAAC,kBAAkB,EAAE,kBAAkB,EAAE,cAAc,EAAE,iBAAiB,CAAU,CAAA;AAEvG;;;;;GAKG;AACH,SAAgB,cAAc,CAAU,KAAQ;IAC9C,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IAE7E,MAAM,IAAI,GAA4B,EAAE,CAAA;IACxC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC,EAAE,CAAC;QACtE,IAAK,YAAkC,CAAC,QAAQ,CAAC,CAAC,CAAC;YAAE,SAAQ;QAC7D,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;IACb,CAAC;IAED;;;;;;;OAOG;IACH,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAA;QACtB,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;YAAE,SAAQ;QAClC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAC9B,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,WAAW;YACpD,CAAC,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;YACrE,CAAC,CAAC,CAAC,CACN,CAAA;IACH,CAAC;IAED,uDAAuD;IACvD,OAAO,EAAE,GAAG,IAAI,EAAE,SAAS,EAAE,IAAA,gCAAkB,EAAC,KAAY,CAAC,EAAE,SAAS,EAAE,IAAA,gCAAkB,EAAC,KAAY,CAAC,EAAO,CAAA;AACnH,CAAC;AAED;;;;;GAKG;AACU,QAAA,sBAAsB,GAAG;IACpC,IAAI,EAAE,CAAC,KAAU,EAAE,EAAE,CAAC,cAAc,CAAC,KAAK,CAAC;IAC3C,EAAE,EAAE,CAAC,KAAU,EAAE,EAAE,CAAC,KAAK;CAC1B,CAAA","sourcesContent":["/*\n * 저장된 트윈 모델의 **어휘 세대를 흡수한다** — 읽는 순간 한 어휘로. 순수.\n *\n * ── 계기 (2026-08-06, 사용자 신고) ───────────────────────────────────────────\n * 트윈 모델러의 엔티티 목록이 **통째로 비어 있었다.** 오류도 없었다. 원인은 이 인스턴스의 저장된 모델가\n * 옛 어휘였고 화면과 호스트는 새 어휘(`locations`/`equipment`)를 읽었기 때문이다. 그 옛 이름들은\n * 아래 `RETIRED_KEYS` 에 있다 — 흡수할 세대의 이름을 적지 않고는 이 파일을 쓸 수 없다.\n *\n * 커널에는 이미 세대를 흡수하는 리더가 있다(`readBoardLocations`·`readBoardEquipment` — 세 세대\n * `movers`→`equipmentList`→`equipment`, 실측 주석에 \"저장된 모델 23개 중 13개가 `movers`\"). 그런데 **그\n * 리더를 거치는 곳이 목록뿐**이었다. 상세·구조 서명·주행 추정기·설비 동기는 컬렉션을 직접 읽어\n * **오류 없이 빈 공장**으로 읽었다:\n *\n * · 모델러·팔레트·보드 자동생성 → 엔티티 0(사용자가 본 증상)\n * · 구조 서명 → `N[]M[]` — **서로 다른 두 공장이 구조적으로 같아 보인다**(변경 감지가 죽는다)\n * · 주행 추정기 → 설비 0이라 거리 기반 소요시간을 못 만든다\n * · 설비 동기 → 아는 설비가 0이라 중복 append + 한 모델에 두 세대가 섞인다\n *\n * ── 왜 자리마다 고치지 않나 ─────────────────────────────────────────────────\n * 저장된 모델를 읽는 자리가 **27곳**이다. 자리마다 리더를 쓰게 하면 다음에 추가되는 자리가 또 빠진다 —\n * 그리고 빠진 것은 오류가 아니라 **빈 공장**이라 들어온 것이 없다. 그래서 **DB에서 읽는 순간 한 번** 정규화한다\n * (엔티티 컬럼 변환기). 그 뒤로는 코드가 세대를 알 필요가 없다.\n *\n * ── 쓰기는 건드리지 않는다 ──────────────────────────────────────────────────\n * 저장은 주는 대로 한다(새 쓰기는 이미 새 어휘다). 읽기가 정규화되므로, 저장된 옛 모델는 **다음에\n * 저장될 때 자연히 새 어휘로 옮겨간다** — 마이그레이션을 따로 실행하지 않는다.\n */\nimport { readBoardLocations, readBoardEquipment } from '@operato/twin-kernel'\n\n/** 흡수하고 **내보내지 않는** 옛 키 — 남겨 두면 소비처가 두 어휘를 다시 보게 된다. */\nconst RETIRED_KEYS = ['nodes', 'movers', 'equipmentList'] as const // vocabulary-guard: allow — 옛 어휘를 흡수하는 것이 이 코드의 일이다(세대 이름을 적어야 흡수할 수 있다)\n\n/** 등급 축들 — 넷이 같은 모양이라 같은 규칙을 쓴다(어느 자원이든 표시명은 `name`). */\nconst CLASS_KEYS = ['personnelClasses', 'equipmentClasses', 'assetClasses', 'materialClasses'] as const\n\n/**\n * 보드를 정본 어휘로 — `locations`·`equipment` 만 남는다. 새 어휘 모델에는 **아무 일도 하지 않는다**\n * (같은 값을 다시 담을 뿐이라 멱등).\n *\n * 모델이 아닌 것(없음·배열·원시값)은 **그대로 통과**시킨다 — 모양을 짐작해 고치지 않는다.\n */\nexport function canonicalModel<T = any>(model: T): T {\n if (!model || typeof model !== 'object' || Array.isArray(model)) return model\n\n const rest: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(model as Record<string, unknown>)) {\n if ((RETIRED_KEYS as readonly string[]).includes(k)) continue\n rest[k] = v\n }\n\n /*\n * **등급의 표시명은 `name` 이다** — 옛 모델는 `description` 에 담았다.\n *\n * 표준 마스터가 등급 표시명을 `description` 으로 주기 때문에 그대로 저장돼 있었고, 화면은 그것을\n * \"설명\" 열에 앉혔다 — 사용자에게는 이름인데 머리글은 설명이라, 같은 것을 축마다 다른 이름으로\n * 부르게 된다. 인제스트 문에서 이미 `name` 으로 옮기고 있지만, **이미 저장된 모델**가 그대로 남아\n * 새 트윈만 고쳐진다. 정규화의 집은 여기 하나다(위 머리말의 이유 그대로).\n */\n for (const key of CLASS_KEYS) {\n const list = rest[key]\n if (!Array.isArray(list)) continue\n rest[key] = list.map((c: any) =>\n c && typeof c === 'object' && !c.name && c.description\n ? (({ description, ...keep }) => ({ ...keep, name: description }))(c)\n : c\n )\n }\n\n /* 리더가 세대를 고른다 — 여기서 `??` 사슬을 다시 적으면 세대 규칙이 두 곳이 된다. */\n return { ...rest, locations: readBoardLocations(model as any), equipment: readBoardEquipment(model as any) } as T\n}\n\n/**\n * 엔티티 컬럼 변환기 — **읽을 때만** 정규화한다.\n *\n * `to`(쓰기)는 손대지 않는다: 저장은 호출부가 준 그대로가 진실이고, 읽기가 정규화되므로 다음 저장에서\n * 자연히 새 어휘가 된다.\n */\nexport const modelColumnTransformer = {\n from: (value: any) => canonicalModel(value),\n to: (value: any) => value\n}\n"]}
@@ -8,7 +8,7 @@ interface Acc {
8
8
  goodCount: number;
9
9
  scrapCount: number;
10
10
  }
11
- /** 누적기의 완전한 재개점 — 저장했다 되살릴 수 있어야 꼬리만 접을 수 있다. */
11
+ /** 누적기의 완전한 재개점 — 저장했다 되살릴 수 있어야 꼬리만 계산할 수 있다. */
12
12
  export interface OeeCheckpoint {
13
13
  maxMs: number;
14
14
  acc: ({
@@ -20,12 +20,12 @@ export declare class OeeAccumulator {
20
20
  private maxMs;
21
21
  private ensure;
22
22
  /**
23
- * **재개점** — 이 누적기의 상태 전부(이어 접기의 씨앗).
23
+ * **재개점** — 이 누적기의 상태 전부(이어 계산의 씨앗).
24
24
  *
25
25
  * ── 왜 필요한가 (2026-08-18) ────────────────────────────────────────────────
26
26
  * 가동 이력은 원 시스템이 누적을 주지 않아 **우리가 이벤트를 적분해** 만든다. 그래서 저널을 꼬리만
27
- * 접으면 이 누적기는 꼬리분만 갖게 되고, 화면의 가용률·수율이 조용히 작아진다(오류는 나지 않는다).
28
- * 재개점을 함께 저장·복원해야 「이어 접은 값」이 「0부터 접은 값」과 같아진다.
27
+ * 계산하면 이 누적기는 꼬리분만 갖게 되고, 화면의 가용률·수율이 알리지 않고 작아진다(오류는 나지 않는다).
28
+ * 재개점을 함께 저장·복원해야 「이어 계산한 값」이 「0부터 계산한 값」과 같아진다.
29
29
  */
30
30
  serialize(): OeeCheckpoint;
31
31
  /** 재개점에서 되세운다 — 모르는 것은 지어내지 않는다(없으면 빈 누적기다). */
@@ -29,12 +29,12 @@ class OeeAccumulator {
29
29
  return a;
30
30
  }
31
31
  /**
32
- * **재개점** — 이 누적기의 상태 전부(이어 접기의 씨앗).
32
+ * **재개점** — 이 누적기의 상태 전부(이어 계산의 씨앗).
33
33
  *
34
34
  * ── 왜 필요한가 (2026-08-18) ────────────────────────────────────────────────
35
35
  * 가동 이력은 원 시스템이 누적을 주지 않아 **우리가 이벤트를 적분해** 만든다. 그래서 저널을 꼬리만
36
- * 접으면 이 누적기는 꼬리분만 갖게 되고, 화면의 가용률·수율이 조용히 작아진다(오류는 나지 않는다).
37
- * 재개점을 함께 저장·복원해야 「이어 접은 값」이 「0부터 접은 값」과 같아진다.
36
+ * 계산하면 이 누적기는 꼬리분만 갖게 되고, 화면의 가용률·수율이 알리지 않고 작아진다(오류는 나지 않는다).
37
+ * 재개점을 함께 저장·복원해야 「이어 계산한 값」이 「0부터 계산한 값」과 같아진다.
38
38
  */
39
39
  serialize() {
40
40
  return {
@@ -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\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"]}
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\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"]}
@@ -39,7 +39,7 @@ exports.PROPERTY_EFFECTS = [
39
39
  *
40
40
  * 이 값들은 **오래전부터 읽히고 있었다** — 커널이 부하·피크·배터리를 그것으로 계산하고, 호스트가
41
41
  * 금액을 그것으로 만든다. 그런데 등록부에 없어서 화면은 그 값들에 대해 「표시만 됩니다」라고 말했다.
42
- * 조용한 거짓말이다: 사용자는 계약전력을 넣고도 「이 값은 아무것도 바꾸지 않는다」는 문장을 읽었다.
42
+ * 들어온 것이 없는 거짓말이다: 사용자는 계약전력을 넣고도 「이 값은 아무것도 바꾸지 않는다」는 문장을 읽었다.
43
43
  *
44
44
  * 효과를 새로 만든 것이 아니라 **있는 효과를 드러낸다.** `readBy` 는 실제로 읽는 자리를 가리킨다.
45
45
  */
@@ -103,7 +103,7 @@ exports.PROPERTY_EFFECTS = [
103
103
  effect: 'twin.propeffect.genDailyProfile',
104
104
  readBy: '@operato/twin-kernel generationFractionAt (시각별 발전 비율)'
105
105
  },
106
- /* 요금은 커널이 읽지 않는다 — 금액은 저널을 접는 호스트가 만든다. readBy 가 그 사실을 말한다. */
106
+ /* 요금은 커널이 읽지 않는다 — 금액은 저널을 계산하는 호스트가 만든다. readBy 가 그 사실을 말한다. */
107
107
  {
108
108
  id: 'tariff.demandChargePerKW',
109
109
  axes: ['locations'],
@@ -1 +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 * ── 공정 모수 (2026-08-19) ────────────────────────────────────────────────\n * 양품률의 기본값은 커널 소스의 상수(0.8)였고, **그 상수가 불량 과다 주목 신호까지 만들었다** — 초기\n * 라인이든 성숙한 라인이든 같은 수로 판정된다. 셋업도 상수였다. 둘 다 현장이 말할 수 있어야 한다.\n */\n {\n id: 'operation.yield',\n axes: ['operations'],\n effect: 'twin.propeffect.operationYield',\n readBy: '@operato/twin-kernel MesKernel(OP_PARAM.yield) ← declareParameters (양품/불량 판정과 산출량)'\n },\n {\n id: 'operation.setupDuration',\n axes: ['operations'],\n effect: 'twin.propeffect.operationSetup',\n readBy: '@operato/twin-kernel MesKernel(OP_PARAM.setupDuration) ← declareParameters (가동 앞에 붙는 셋업)'\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"]}
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 * ── 공정 모수 (2026-08-19) ────────────────────────────────────────────────\n * 양품률의 기본값은 커널 소스의 상수(0.8)였고, **그 상수가 불량 과다 주목 신호까지 만들었다** — 초기\n * 라인이든 성숙한 라인이든 같은 수로 판정된다. 셋업도 상수였다. 둘 다 현장이 말할 수 있어야 한다.\n */\n {\n id: 'operation.yield',\n axes: ['operations'],\n effect: 'twin.propeffect.operationYield',\n readBy: '@operato/twin-kernel MesKernel(OP_PARAM.yield) ← declareParameters (양품/불량 판정과 산출량)'\n },\n {\n id: 'operation.setupDuration',\n axes: ['operations'],\n effect: 'twin.propeffect.operationSetup',\n readBy: '@operato/twin-kernel MesKernel(OP_PARAM.setupDuration) ← declareParameters (가동 앞에 붙는 셋업)'\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"]}
@@ -0,0 +1,22 @@
1
+ import type { ObjectLiteral, Repository } from 'typeorm';
2
+ /**
3
+ * 원시 결과에서 꺼낸 **컬럼 값** → 날짜. 읽을 수 없으면 `null`.
4
+ *
5
+ * 해석은 드라이버가 한다(`prepareHydratedValue`) — 엔티티로 받았을 때와 **같은 값**이 나온다.
6
+ * 그래서 속도 때문에 원시로 받는 자리와 엔티티로 받는 자리가 서로 다른 답을 낼 수 없다.
7
+ *
8
+ * 원시로 받는 이유는 그대로 유효하다: 경계 한 줄을 찾을 때 색인만 읽으면 테이블을 열지 않는다.
9
+ * 잃는 것은 컬럼 변환뿐이었고, 이 함수가 그것만 되돌려 준다.
10
+ */
11
+ export declare function hydratedDate<T extends ObjectLiteral>(repository: Repository<T>, property: keyof T & string, value: unknown): Date | null;
12
+ /**
13
+ * **계약이 절대시각이라고 약속한** 값 → 날짜. 읽을 수 없으면 `null`.
14
+ *
15
+ * 쓰는 자리는 둘이다. 유입 봉투의 `eventTime`(정규 계약이 절대시각을 요구한다)과 우리가 만든
16
+ * 커서(우리가 적은 것을 우리가 읽는다). 둘 다 테이블의 컬럼이 아니므로 드라이버에게 물을 수 없다.
17
+ *
18
+ * 시간대가 적혀 있지 않으면 UTC 로 읽는다 — 계약이 절대시각을 요구하므로 지역 시간으로 읽을 근거가
19
+ * 없다. 「지금」으로 메우지 않는다: 옛 사건이 최근으로 보이고, 경계를 정하는 자리에서는 지우면 안
20
+ * 되는 것을 지운다.
21
+ */
22
+ export declare function absoluteTime(value: unknown): Date | null;
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hydratedDate = hydratedDate;
4
+ exports.absoluteTime = absoluteTime;
5
+ /*
6
+ * **저장된 값을 시각으로 읽는 두 가지 물음** (2026-08-28 실측).
7
+ *
8
+ * ── 무엇이 어긋났나 ─────────────────────────────────────────────────────────
9
+ * `getRawOne`·`getRawMany` 는 드라이버가 준 값을 그대로 준다. sqlite 가 주는 그 값에는 **시간대가
10
+ * 없다**(`2026-08-27 14:37:20.049`). 그것을 `new Date()` 로 읽으면 서버의 지역 시간으로 해석해
11
+ * 한국에서 9시간이 밀린다. 저장된 값은 UTC 이므로 그 해석이 틀렸다.
12
+ *
13
+ * 네 자리에서 났다. 둘은 **지우는 경계**(경계가 이르게 밀리면 정리가 0건이 되어 멈춘다), 하나는
14
+ * 이어 읽기 지점(9시간을 건너뛰어 본 줄이 또 나왔다), 하나는 「이 목록은 언제부터 말할 수 있나」.
15
+ *
16
+ * ── 처음에 편법을 썼다 ──────────────────────────────────────────────────────
17
+ * 「시간대가 없으면 UTC 로 읽는다」를 직접 적었다. 그것은 **드라이버가 어떤 문자열로 저장하는지를
18
+ * 응용 코드가 아는 것**이고, 이 저장소의 절대 규칙(다섯 드라이버·TypeORM 관용구만)에 어긋난다.
19
+ *
20
+ * 표준 문이 이미 있다. `Driver.prepareHydratedValue(value, column)` — 엔티티 변환
21
+ * (`RawSqlResultsToEntityTransformer`)이 지나는 바로 그 함수다. sqlite 구현이 공백을 `T` 로 바꾸고
22
+ * `Z` 를 붙이고, Postgres 구현은 이미 날짜 객체인 값을 그대로 낸다. 드라이버마다 자기 구현을 갖고
23
+ * 있으므로 이식성이 **정의상** 보장된다.
24
+ *
25
+ * ── 두 물음을 섞지 않는다 ───────────────────────────────────────────────────
26
+ * `hydratedDate` **테이블의 컬럼** 값을 읽는다 — 드라이버에게 물어본다
27
+ * `absoluteTime` **계약이 절대시각이라고 약속한** 값을 읽는다(유입 봉투 · 우리가 만든 커서)
28
+ *
29
+ * 앞의 것에 뒤의 규칙을 쓰면 편법이 되고, 뒤의 것에 앞의 것을 쓸 수는 없다(컬럼이 아니다).
30
+ */
31
+ /** 시간대가 적혀 있나 — `Z`·`+09:00`·`-0500`. */
32
+ const HAS_ZONE = /([Zz]|[+-]\d{2}:?\d{2})$/;
33
+ /**
34
+ * 원시 결과에서 꺼낸 **컬럼 값** → 날짜. 읽을 수 없으면 `null`.
35
+ *
36
+ * 해석은 드라이버가 한다(`prepareHydratedValue`) — 엔티티로 받았을 때와 **같은 값**이 나온다.
37
+ * 그래서 속도 때문에 원시로 받는 자리와 엔티티로 받는 자리가 서로 다른 답을 낼 수 없다.
38
+ *
39
+ * 원시로 받는 이유는 그대로 유효하다: 경계 한 줄을 찾을 때 색인만 읽으면 테이블을 열지 않는다.
40
+ * 잃는 것은 컬럼 변환뿐이었고, 이 함수가 그것만 되돌려 준다.
41
+ */
42
+ function hydratedDate(repository, property, value) {
43
+ if (value === null || value === undefined)
44
+ return null;
45
+ const column = repository.metadata.findColumnWithPropertyName(property);
46
+ if (!column) {
47
+ /* 컬럼을 못 찾으면 짐작하지 않는다 — 이름이 바뀌었다는 뜻이고, 그때는 값을 지어내면 안 된다. */
48
+ throw new Error(`[read-time] "${repository.metadata.name}" has no column for property "${property}"`);
49
+ }
50
+ const at = repository.manager.connection.driver.prepareHydratedValue(value, column);
51
+ return at instanceof Date && !Number.isNaN(at.getTime()) ? at : null;
52
+ }
53
+ /**
54
+ * **계약이 절대시각이라고 약속한** 값 → 날짜. 읽을 수 없으면 `null`.
55
+ *
56
+ * 쓰는 자리는 둘이다. 유입 봉투의 `eventTime`(정규 계약이 절대시각을 요구한다)과 우리가 만든
57
+ * 커서(우리가 적은 것을 우리가 읽는다). 둘 다 테이블의 컬럼이 아니므로 드라이버에게 물을 수 없다.
58
+ *
59
+ * 시간대가 적혀 있지 않으면 UTC 로 읽는다 — 계약이 절대시각을 요구하므로 지역 시간으로 읽을 근거가
60
+ * 없다. 「지금」으로 메우지 않는다: 옛 사건이 최근으로 보이고, 경계를 정하는 자리에서는 지우면 안
61
+ * 되는 것을 지운다.
62
+ */
63
+ function absoluteTime(value) {
64
+ if (value === null || value === undefined)
65
+ return null;
66
+ if (value instanceof Date)
67
+ return Number.isNaN(value.getTime()) ? null : value;
68
+ if (typeof value === 'number') {
69
+ const at = new Date(value);
70
+ return Number.isNaN(at.getTime()) ? null : at;
71
+ }
72
+ if (typeof value !== 'string')
73
+ return null;
74
+ const s = value.trim();
75
+ if (!s)
76
+ return null;
77
+ /* 시간대가 적혀 있으면 그 말을 믿는다 — 우리가 고쳐 붙이지 않는다. */
78
+ const at = new Date(HAS_ZONE.test(s) ? s : `${s.replace(' ', 'T')}Z`);
79
+ return Number.isNaN(at.getTime()) ? null : at;
80
+ }
81
+ //# sourceMappingURL=read-time.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-time.js","sourceRoot":"","sources":["../../server/engine/read-time.ts"],"names":[],"mappings":";;AAyCA,oCASC;AAYD,oCAcC;AA1ED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,yCAAyC;AACzC,MAAM,QAAQ,GAAG,0BAA0B,CAAA;AAE3C;;;;;;;;GAQG;AACH,SAAgB,YAAY,CAA0B,UAAyB,EAAE,QAA0B,EAAE,KAAc;IACzH,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAA;IACtD,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,0BAA0B,CAAC,QAAQ,CAAC,CAAA;IACvE,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,2DAA2D;QAC3D,MAAM,IAAI,KAAK,CAAC,gBAAgB,UAAU,CAAC,QAAQ,CAAC,IAAI,iCAAiC,QAAQ,GAAG,CAAC,CAAA;IACvG,CAAC;IACD,MAAM,EAAE,GAAG,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,oBAAoB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;IACnF,OAAO,EAAE,YAAY,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;AACtE,CAAC;AAED;;;;;;;;;GASG;AACH,SAAgB,YAAY,CAAC,KAAc;IACzC,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAA;IACtD,IAAI,KAAK,YAAY,IAAI;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAA;IAC9E,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,EAAE,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,CAAA;QAC1B,OAAO,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAA;IAC/C,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAA;IAE1C,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAA;IACtB,IAAI,CAAC,CAAC;QAAE,OAAO,IAAI,CAAA;IACnB,4CAA4C;IAC5C,MAAM,EAAE,GAAG,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,CAAA;IACrE,OAAO,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAA;AAC/C,CAAC","sourcesContent":["import type { ObjectLiteral, Repository } from 'typeorm'\n\n/*\n * **저장된 값을 시각으로 읽는 두 가지 물음** (2026-08-28 실측).\n *\n * ── 무엇이 어긋났나 ─────────────────────────────────────────────────────────\n * `getRawOne`·`getRawMany` 는 드라이버가 준 값을 그대로 준다. sqlite 가 주는 그 값에는 **시간대가\n * 없다**(`2026-08-27 14:37:20.049`). 그것을 `new Date()` 로 읽으면 서버의 지역 시간으로 해석해\n * 한국에서 9시간이 밀린다. 저장된 값은 UTC 이므로 그 해석이 틀렸다.\n *\n * 네 자리에서 났다. 둘은 **지우는 경계**(경계가 이르게 밀리면 정리가 0건이 되어 멈춘다), 하나는\n * 이어 읽기 지점(9시간을 건너뛰어 본 줄이 또 나왔다), 하나는 「이 목록은 언제부터 말할 수 있나」.\n *\n * ── 처음에 편법을 썼다 ──────────────────────────────────────────────────────\n * 「시간대가 없으면 UTC 로 읽는다」를 직접 적었다. 그것은 **드라이버가 어떤 문자열로 저장하는지를\n * 응용 코드가 아는 것**이고, 이 저장소의 절대 규칙(다섯 드라이버·TypeORM 관용구만)에 어긋난다.\n *\n * 표준 문이 이미 있다. `Driver.prepareHydratedValue(value, column)` — 엔티티 변환\n * (`RawSqlResultsToEntityTransformer`)이 지나는 바로 그 함수다. sqlite 구현이 공백을 `T` 로 바꾸고\n * `Z` 를 붙이고, Postgres 구현은 이미 날짜 객체인 값을 그대로 낸다. 드라이버마다 자기 구현을 갖고\n * 있으므로 이식성이 **정의상** 보장된다.\n *\n * ── 두 물음을 섞지 않는다 ───────────────────────────────────────────────────\n * `hydratedDate` **테이블의 컬럼** 값을 읽는다 — 드라이버에게 물어본다\n * `absoluteTime` **계약이 절대시각이라고 약속한** 값을 읽는다(유입 봉투 · 우리가 만든 커서)\n *\n * 앞의 것에 뒤의 규칙을 쓰면 편법이 되고, 뒤의 것에 앞의 것을 쓸 수는 없다(컬럼이 아니다).\n */\n\n/** 시간대가 적혀 있나 — `Z`·`+09:00`·`-0500`. */\nconst HAS_ZONE = /([Zz]|[+-]\\d{2}:?\\d{2})$/\n\n/**\n * 원시 결과에서 꺼낸 **컬럼 값** → 날짜. 읽을 수 없으면 `null`.\n *\n * 해석은 드라이버가 한다(`prepareHydratedValue`) — 엔티티로 받았을 때와 **같은 값**이 나온다.\n * 그래서 속도 때문에 원시로 받는 자리와 엔티티로 받는 자리가 서로 다른 답을 낼 수 없다.\n *\n * 원시로 받는 이유는 그대로 유효하다: 경계 한 줄을 찾을 때 색인만 읽으면 테이블을 열지 않는다.\n * 잃는 것은 컬럼 변환뿐이었고, 이 함수가 그것만 되돌려 준다.\n */\nexport function hydratedDate<T extends ObjectLiteral>(repository: Repository<T>, property: keyof T & string, value: unknown): Date | null {\n if (value === null || value === undefined) return null\n const column = repository.metadata.findColumnWithPropertyName(property)\n if (!column) {\n /* 컬럼을 못 찾으면 짐작하지 않는다 — 이름이 바뀌었다는 뜻이고, 그때는 값을 지어내면 안 된다. */\n throw new Error(`[read-time] \"${repository.metadata.name}\" has no column for property \"${property}\"`)\n }\n const at = repository.manager.connection.driver.prepareHydratedValue(value, column)\n return at instanceof Date && !Number.isNaN(at.getTime()) ? at : null\n}\n\n/**\n * **계약이 절대시각이라고 약속한** 값 → 날짜. 읽을 수 없으면 `null`.\n *\n * 쓰는 자리는 둘이다. 유입 봉투의 `eventTime`(정규 계약이 절대시각을 요구한다)과 우리가 만든\n * 커서(우리가 적은 것을 우리가 읽는다). 둘 다 테이블의 컬럼이 아니므로 드라이버에게 물을 수 없다.\n *\n * 시간대가 적혀 있지 않으면 UTC 로 읽는다 — 계약이 절대시각을 요구하므로 지역 시간으로 읽을 근거가\n * 없다. 「지금」으로 메우지 않는다: 옛 사건이 최근으로 보이고, 경계를 정하는 자리에서는 지우면 안\n * 되는 것을 지운다.\n */\nexport function absoluteTime(value: unknown): Date | null {\n if (value === null || value === undefined) return null\n if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value\n if (typeof value === 'number') {\n const at = new Date(value)\n return Number.isNaN(at.getTime()) ? null : at\n }\n if (typeof value !== 'string') return null\n\n const s = value.trim()\n if (!s) return null\n /* 시간대가 적혀 있으면 그 말을 믿는다 — 우리가 고쳐 붙이지 않는다. */\n const at = new Date(HAS_ZONE.test(s) ? s : `${s.replace(' ', 'T')}Z`)\n return Number.isNaN(at.getTime()) ? null : at\n}\n"]}
@@ -17,7 +17,7 @@
17
17
  * reset — 저널을 비우고 씨앗부터 다시 돌린다(재현 가능한 실험).
18
18
  *
19
19
  * ── 기본값을 두지 않는다 ────────────────────────────────────────────────────
20
- * 예전에는 미선언이 조용히 `sim-experiment`(=저널 초기화)로 떨어졌다. 그 관용은 값이 하나 어긋나는
20
+ * 예전에는 미선언이 알리지 않고 `sim-experiment`(=저널 초기화)로 떨어졌다. 그 관용은 값이 하나 어긋나는
21
21
  * 순간 **저널을 지우는 길**이 된다 — 개명 뒤 옛 값이 남아 있는 행을 그 길로 읽으면 재기동 한 번에
22
22
  * 이력이 사라진다. 그래서 모르는 값은 **오류를 낸다.** 부르는 쪽이 선언하게 만드는 것이 이 축의 요점이다.
23
23
  */
@@ -1 +1 @@
1
- {"version":3,"file":"restart-policy.js","sourceRoot":"","sources":["../../server/engine/restart-policy.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;;;AAcH,8CAcC;AAGD,0CAEC;AA5BY,QAAA,gBAAgB,GAA6B,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,CAAU,CAAA;AAEhG;;;;;;GAMG;AACH,SAAgB,iBAAiB,CAAC,GAAY,EAAE,KAAa;IAC3D,MAAM,KAAK,GAAG,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;IACvD,IAAK,wBAAsC,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,KAAsB,CAAA;IAC1F,MAAM,MAAM,GAAkC,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,gBAAgB,EAAE,OAAO,EAAE,CAAA;IACpH,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CACb,GAAG,KAAK,qBAAqB,KAAK,uDAAuD,MAAM,CAAC,KAAK,CAAC,IAAI;YACxG,4IAA4I,CAC/I,CAAA;IACH,CAAC;IACD,MAAM,IAAI,KAAK,CACb,GAAG,KAAK,oBAAoB,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,YAAY,qBAAqB,wBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI;QAClH,0HAA0H,CAC7H,CAAA;AACH,CAAC;AAED,8CAA8C;AAC9C,SAAgB,eAAe,CAAC,GAAY;IAC1C,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAK,wBAAsC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAA;AACzF,CAAC","sourcesContent":["/*\n * **재기동 정책** — 이 트윈은 다시 세워질 때 자기 과거를 어떻게 대하나.\n *\n * ── 왜 이 이름인가 (ADR-0029 · plans/simulator-as-source.md §2) ─────────────\n * 옛 이름은 `realityMode` 였고 값은 `mirror`·`sim-world`·`sim-experiment` 였다. 그 축은 「현실이 어디서\n * 오나」를 말하는 것처럼 보였지만, 코드가 그것으로 가른 것은 **재기동 거동 하나**였다. 그리고 「현실이\n * 어디서 오나」는 ADR-0029 가 **원본의 종류**로 옮긴 물음이다(`TwinReference.adapterType`) — 트윈은 한\n * 종류이고 언제나 원본을 반영한다.\n *\n * 그래서 이 축은 사라지지 않고 **트윈에 남아 이름을 바꿨다.** 남는 이유: 「자기 저널을 잇는가」는 원본의\n * 성질이 아니라 **트윈과 자기 과거의 관계**다. 같은 시뮬레이터 원본을 보는 두 트윈이 하나는 이어가고\n * 하나는 매번 처음부터 재현할 수 있다 — 원본은 그 둘을 구별하지 못한다.\n *\n * resync — 외부 실물에서 다시 읽는다(원본이 진실이므로 우리 과거를 이어 붙이지 않는다).\n * resume — 저널을 이어 세운다(이 트윈이 낳은 타임라인이 곧 사실이다).\n * reset — 저널을 비우고 씨앗부터 다시 돌린다(재현 가능한 실험).\n *\n * ── 기본값을 두지 않는다 ────────────────────────────────────────────────────\n * 예전에는 미선언이 조용히 `sim-experiment`(=저널 초기화)로 떨어졌다. 그 관용은 값이 하나 어긋나는\n * 순간 **저널을 지우는 길**이 된다 — 개명 뒤 옛 값이 남아 있는 행을 그 길로 읽으면 재기동 한 번에\n * 이력이 사라진다. 그래서 모르는 값은 **오류를 낸다.** 부르는 쪽이 선언하게 만드는 것이 이 축의 요점이다.\n */\n\n/** 재기동 정책 — 셋뿐이다. */\nexport type RestartPolicy = 'resync' | 'resume' | 'reset'\n\nexport const RESTART_POLICIES: readonly RestartPolicy[] = ['resync', 'resume', 'reset'] as const\n\n/**\n * 저장된 값을 정책으로 읽는다 — **모르면 오류를 낸다**(기본값으로 메우지 않는다).\n *\n * 옛 어휘(`mirror`·`sim-world`·`sim-experiment`)를 여기서 번역하지 않는다: 번역을 두면 옛 값이 살아남고,\n * 그 값이 다시 어딘가에 저장된다. 저장된 값은 **한 번 옮기고**(데이터 이관) 코드는 새 어휘만 안다.\n * 그래서 이 함수는 옛 값에 대해서도 오류를 내며, 무엇으로 옮겨야 하는지 문장으로 말한다.\n */\nexport function readRestartPolicy(raw: unknown, where: string): RestartPolicy {\n const value = typeof raw === 'string' ? raw.trim() : ''\n if ((RESTART_POLICIES as readonly string[]).includes(value)) return value as RestartPolicy\n const legacy: Record<string, RestartPolicy> = { mirror: 'resync', 'sim-world': 'resume', 'sim-experiment': 'reset' }\n if (legacy[value]) {\n throw new Error(\n `${where}: restart policy \"${value}\" is the old vocabulary — move the stored value to \"${legacy[value]}\" ` +\n '(ADR-0029 · plans/simulator-as-source.md §2). Nothing is translated at read time on purpose: a translation would keep the old value alive.'\n )\n }\n throw new Error(\n `${where}: restart policy ${value ? `\"${value}\"` : 'is missing'} — declare one of ${RESTART_POLICIES.join(' | ')}. ` +\n 'There is no default: an unspecified policy used to fall through to \"reset\", which erases the journal on the next reboot.'\n )\n}\n\n/** 이 값이 정책인가 — 오류를 내지 않고 묻는 자리(화면·검사)를 위해. */\nexport function isRestartPolicy(raw: unknown): raw is RestartPolicy {\n return typeof raw === 'string' && (RESTART_POLICIES as readonly string[]).includes(raw)\n}\n"]}
1
+ {"version":3,"file":"restart-policy.js","sourceRoot":"","sources":["../../server/engine/restart-policy.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;;;AAcH,8CAcC;AAGD,0CAEC;AA5BY,QAAA,gBAAgB,GAA6B,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,CAAU,CAAA;AAEhG;;;;;;GAMG;AACH,SAAgB,iBAAiB,CAAC,GAAY,EAAE,KAAa;IAC3D,MAAM,KAAK,GAAG,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;IACvD,IAAK,wBAAsC,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,KAAsB,CAAA;IAC1F,MAAM,MAAM,GAAkC,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,gBAAgB,EAAE,OAAO,EAAE,CAAA;IACpH,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CACb,GAAG,KAAK,qBAAqB,KAAK,uDAAuD,MAAM,CAAC,KAAK,CAAC,IAAI;YACxG,4IAA4I,CAC/I,CAAA;IACH,CAAC;IACD,MAAM,IAAI,KAAK,CACb,GAAG,KAAK,oBAAoB,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,YAAY,qBAAqB,wBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI;QAClH,0HAA0H,CAC7H,CAAA;AACH,CAAC;AAED,8CAA8C;AAC9C,SAAgB,eAAe,CAAC,GAAY;IAC1C,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAK,wBAAsC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAA;AACzF,CAAC","sourcesContent":["/*\n * **재기동 정책** — 이 트윈은 다시 세워질 때 자기 과거를 어떻게 대하나.\n *\n * ── 왜 이 이름인가 (ADR-0029 · plans/simulator-as-source.md §2) ─────────────\n * 옛 이름은 `realityMode` 였고 값은 `mirror`·`sim-world`·`sim-experiment` 였다. 그 축은 「현실이 어디서\n * 오나」를 말하는 것처럼 보였지만, 코드가 그것으로 가른 것은 **재기동 거동 하나**였다. 그리고 「현실이\n * 어디서 오나」는 ADR-0029 가 **원본의 종류**로 옮긴 물음이다(`TwinReference.adapterType`) — 트윈은 한\n * 종류이고 언제나 원본을 반영한다.\n *\n * 그래서 이 축은 사라지지 않고 **트윈에 남아 이름을 바꿨다.** 남는 이유: 「자기 저널을 잇는가」는 원본의\n * 성질이 아니라 **트윈과 자기 과거의 관계**다. 같은 시뮬레이터 원본을 보는 두 트윈이 하나는 이어가고\n * 하나는 매번 처음부터 재현할 수 있다 — 원본은 그 둘을 구별하지 못한다.\n *\n * resync — 외부 실물에서 다시 읽는다(원본이 진실이므로 우리 과거를 이어 붙이지 않는다).\n * resume — 저널을 이어 세운다(이 트윈이 낳은 타임라인이 곧 사실이다).\n * reset — 저널을 비우고 씨앗부터 다시 돌린다(재현 가능한 실험).\n *\n * ── 기본값을 두지 않는다 ────────────────────────────────────────────────────\n * 예전에는 미선언이 알리지 않고 `sim-experiment`(=저널 초기화)로 떨어졌다. 그 관용은 값이 하나 어긋나는\n * 순간 **저널을 지우는 길**이 된다 — 개명 뒤 옛 값이 남아 있는 행을 그 길로 읽으면 재기동 한 번에\n * 이력이 사라진다. 그래서 모르는 값은 **오류를 낸다.** 부르는 쪽이 선언하게 만드는 것이 이 축의 요점이다.\n */\n\n/** 재기동 정책 — 셋뿐이다. */\nexport type RestartPolicy = 'resync' | 'resume' | 'reset'\n\nexport const RESTART_POLICIES: readonly RestartPolicy[] = ['resync', 'resume', 'reset'] as const\n\n/**\n * 저장된 값을 정책으로 읽는다 — **모르면 오류를 낸다**(기본값으로 메우지 않는다).\n *\n * 옛 어휘(`mirror`·`sim-world`·`sim-experiment`)를 여기서 번역하지 않는다: 번역을 두면 옛 값이 살아남고,\n * 그 값이 다시 어딘가에 저장된다. 저장된 값은 **한 번 옮기고**(데이터 이관) 코드는 새 어휘만 안다.\n * 그래서 이 함수는 옛 값에 대해서도 오류를 내며, 무엇으로 옮겨야 하는지 문장으로 말한다.\n */\nexport function readRestartPolicy(raw: unknown, where: string): RestartPolicy {\n const value = typeof raw === 'string' ? raw.trim() : ''\n if ((RESTART_POLICIES as readonly string[]).includes(value)) return value as RestartPolicy\n const legacy: Record<string, RestartPolicy> = { mirror: 'resync', 'sim-world': 'resume', 'sim-experiment': 'reset' }\n if (legacy[value]) {\n throw new Error(\n `${where}: restart policy \"${value}\" is the old vocabulary — move the stored value to \"${legacy[value]}\" ` +\n '(ADR-0029 · plans/simulator-as-source.md §2). Nothing is translated at read time on purpose: a translation would keep the old value alive.'\n )\n }\n throw new Error(\n `${where}: restart policy ${value ? `\"${value}\"` : 'is missing'} — declare one of ${RESTART_POLICIES.join(' | ')}. ` +\n 'There is no default: an unspecified policy used to fall through to \"reset\", which erases the journal on the next reboot.'\n )\n}\n\n/** 이 값이 정책인가 — 오류를 내지 않고 묻는 자리(화면·검사)를 위해. */\nexport function isRestartPolicy(raw: unknown): raw is RestartPolicy {\n return typeof raw === 'string' && (RESTART_POLICIES as readonly string[]).includes(raw)\n}\n"]}
@@ -26,7 +26,7 @@
26
26
  * `${domainId}:${instanceId}` — 이 저장소가 이미 쓰는 모양이다(`structureRevCache`·`measuredCache`).
27
27
  * 새 모양을 만들지 않는다. 파싱은 **첫 구분자**에서 자르므로 `instanceId` 에 `:` 가 들어 있어도
28
28
  * 안전하다(레퍼런스 소스 이름에 들어갈 수 있다). 대신 `domainId` 에 구분자가 있으면 키가 모호해지므로
29
- * **그때는 오류를 낸다** — 조용히 잘못된 키를 만들면 그게 곧 이 결함의 재발이다.
29
+ * **그때는 오류를 낸다** — 알리지 않고 잘못된 키를 만들면 그게 곧 이 결함의 재발이다.
30
30
  */
31
31
  Object.defineProperty(exports, "__esModule", { value: true });
32
32
  exports.RUNTIME_KEY_SEP = void 0;