@things-factory/headless-twin 10.0.11 → 10.0.13

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 (369) hide show
  1. package/dist-server/engine/canonical-ingest.d.ts +81 -3
  2. package/dist-server/engine/canonical-ingest.js +89 -10
  3. package/dist-server/engine/canonical-ingest.js.map +1 -1
  4. package/dist-server/engine/command-routing.js +1 -1
  5. package/dist-server/engine/command-routing.js.map +1 -1
  6. package/dist-server/engine/declared-stimulus.d.ts +43 -0
  7. package/dist-server/engine/declared-stimulus.js +57 -0
  8. package/dist-server/engine/declared-stimulus.js.map +1 -0
  9. package/dist-server/engine/index.d.ts +5 -0
  10. package/dist-server/engine/index.js +6 -0
  11. package/dist-server/engine/index.js.map +1 -1
  12. package/dist-server/engine/ingest-health.d.ts +335 -0
  13. package/dist-server/engine/ingest-health.js +434 -0
  14. package/dist-server/engine/ingest-health.js.map +1 -0
  15. package/dist-server/engine/integration-coverage.d.ts +76 -0
  16. package/dist-server/engine/integration-coverage.js +73 -0
  17. package/dist-server/engine/integration-coverage.js.map +1 -0
  18. package/dist-server/engine/integration-probes.d.ts +65 -0
  19. package/dist-server/engine/integration-probes.js +100 -0
  20. package/dist-server/engine/integration-probes.js.map +1 -0
  21. package/dist-server/engine/integration-runner.d.ts +45 -0
  22. package/dist-server/engine/integration-runner.js +59 -0
  23. package/dist-server/engine/integration-runner.js.map +1 -0
  24. package/dist-server/engine/integration-target-profile.d.ts +57 -0
  25. package/dist-server/engine/integration-target-profile.js +79 -0
  26. package/dist-server/engine/integration-target-profile.js.map +1 -0
  27. package/dist-server/engine/kpi-fold.d.ts +39 -0
  28. package/dist-server/engine/kpi-fold.js +68 -5
  29. package/dist-server/engine/kpi-fold.js.map +1 -1
  30. package/dist-server/engine/kpi-query.d.ts +3 -3
  31. package/dist-server/engine/kpi-query.js +132 -18
  32. package/dist-server/engine/kpi-query.js.map +1 -1
  33. package/dist-server/engine/live-feed-registry.d.ts +1 -1
  34. package/dist-server/engine/live-feed-registry.js +3 -17
  35. package/dist-server/engine/live-feed-registry.js.map +1 -1
  36. package/dist-server/engine/load-meter.d.ts +1 -1
  37. package/dist-server/engine/load-meter.js +12 -3
  38. package/dist-server/engine/load-meter.js.map +1 -1
  39. package/dist-server/engine/local-declarations.d.ts +3 -6
  40. package/dist-server/engine/local-declarations.js +97 -16
  41. package/dist-server/engine/local-declarations.js.map +1 -1
  42. package/dist-server/engine/log.d.ts +18 -0
  43. package/dist-server/engine/log.js +80 -0
  44. package/dist-server/engine/log.js.map +1 -0
  45. package/dist-server/engine/loop-lag.d.ts +54 -0
  46. package/dist-server/engine/loop-lag.js +87 -0
  47. package/dist-server/engine/loop-lag.js.map +1 -0
  48. package/dist-server/engine/measured-yield.d.ts +42 -0
  49. package/dist-server/engine/measured-yield.js +75 -0
  50. package/dist-server/engine/measured-yield.js.map +1 -0
  51. package/dist-server/engine/model-gap.d.ts +108 -0
  52. package/dist-server/engine/model-gap.js +95 -0
  53. package/dist-server/engine/model-gap.js.map +1 -0
  54. package/dist-server/engine/operation-basis.d.ts +16 -0
  55. package/dist-server/engine/operation-basis.js +20 -2
  56. package/dist-server/engine/operation-basis.js.map +1 -1
  57. package/dist-server/engine/property-effects.js +17 -0
  58. package/dist-server/engine/property-effects.js.map +1 -1
  59. package/dist-server/engine/restart-policy.d.ts +13 -0
  60. package/dist-server/engine/restart-policy.js +52 -0
  61. package/dist-server/engine/restart-policy.js.map +1 -0
  62. package/dist-server/engine/runtime-key.js +1 -1
  63. package/dist-server/engine/runtime-key.js.map +1 -1
  64. package/dist-server/engine/spec-coverage.d.ts +8 -0
  65. package/dist-server/engine/spec-coverage.js +3 -1
  66. package/dist-server/engine/spec-coverage.js.map +1 -1
  67. package/dist-server/engine/stage-path.d.ts +83 -0
  68. package/dist-server/engine/stage-path.js +118 -0
  69. package/dist-server/engine/stage-path.js.map +1 -0
  70. package/dist-server/engine/twin-engine.d.ts +400 -31
  71. package/dist-server/engine/twin-engine.js +1198 -187
  72. package/dist-server/engine/twin-engine.js.map +1 -1
  73. package/dist-server/index.js +35 -3
  74. package/dist-server/index.js.map +1 -1
  75. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.d.ts +5 -0
  76. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js +58 -0
  77. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js.map +1 -0
  78. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.d.ts +5 -0
  79. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js +55 -0
  80. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js.map +1 -0
  81. package/dist-server/migrations/index.js +7 -1
  82. package/dist-server/migrations/index.js.map +1 -1
  83. package/dist-server/service/index.d.ts +5 -2
  84. package/dist-server/service/index.js +18 -7
  85. package/dist-server/service/index.js.map +1 -1
  86. package/dist-server/service/reference/control-routing.d.ts +14 -0
  87. package/dist-server/service/reference/control-routing.js +64 -0
  88. package/dist-server/service/reference/control-routing.js.map +1 -0
  89. package/dist-server/service/reference/discovery-result.d.ts +1 -1
  90. package/dist-server/service/reference/discovery-result.js +1 -1
  91. package/dist-server/service/reference/discovery-result.js.map +1 -1
  92. package/dist-server/service/reference/index.d.ts +5 -1
  93. package/dist-server/service/reference/index.js +8 -1
  94. package/dist-server/service/reference/index.js.map +1 -1
  95. package/dist-server/service/reference/reference-adapter.d.ts +251 -3
  96. package/dist-server/service/reference/reference-adapter.js +43 -1
  97. package/dist-server/service/reference/reference-adapter.js.map +1 -1
  98. package/dist-server/service/reference/reference-assessment.d.ts +68 -0
  99. package/dist-server/service/reference/reference-assessment.js +136 -0
  100. package/dist-server/service/reference/reference-assessment.js.map +1 -0
  101. package/dist-server/service/reference/reference-live.d.ts +12 -1
  102. package/dist-server/service/reference/reference-live.js +119 -14
  103. package/dist-server/service/reference/reference-live.js.map +1 -1
  104. package/dist-server/service/reference/reference-master.d.ts +47 -1
  105. package/dist-server/service/reference/reference-master.js +34 -4
  106. package/dist-server/service/reference/reference-master.js.map +1 -1
  107. package/dist-server/service/reference/reference-probe.d.ts +92 -0
  108. package/dist-server/service/reference/reference-probe.js +186 -0
  109. package/dist-server/service/reference/reference-probe.js.map +1 -0
  110. package/dist-server/service/reference/reference-progress-subscription.d.ts +17 -0
  111. package/dist-server/service/reference/reference-progress-subscription.js +94 -0
  112. package/dist-server/service/reference/reference-progress-subscription.js.map +1 -0
  113. package/dist-server/service/reference/reference-progress.d.ts +38 -0
  114. package/dist-server/service/reference/reference-progress.js +71 -0
  115. package/dist-server/service/reference/reference-progress.js.map +1 -0
  116. package/dist-server/service/reference/reference-resolver.d.ts +3 -3
  117. package/dist-server/service/reference/reference-resolver.js +242 -58
  118. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  119. package/dist-server/service/reference/twin-reference.d.ts +32 -0
  120. package/dist-server/service/reference/twin-reference.js +10 -0
  121. package/dist-server/service/reference/twin-reference.js.map +1 -1
  122. package/dist-server/service/twin-audit/command-audit.d.ts +34 -0
  123. package/dist-server/service/twin-audit/command-audit.js +15 -1
  124. package/dist-server/service/twin-audit/command-audit.js.map +1 -1
  125. package/dist-server/service/twin-audit/twin-audit-event.d.ts +3 -0
  126. package/dist-server/service/twin-audit/twin-audit-event.js +10 -0
  127. package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -1
  128. package/dist-server/service/twin-control/twin-control-mutation.d.ts +15 -2
  129. package/dist-server/service/twin-control/twin-control-mutation.js +88 -36
  130. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  131. package/dist-server/service/twin-event/journal-count.d.ts +21 -0
  132. package/dist-server/service/twin-event/journal-count.js +26 -0
  133. package/dist-server/service/twin-event/journal-count.js.map +1 -0
  134. package/dist-server/service/twin-event/twin-event-keys.d.ts +9 -0
  135. package/dist-server/service/twin-event/twin-event-keys.js +15 -18
  136. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  137. package/dist-server/service/twin-event/twin-event-type.d.ts +3 -1
  138. package/dist-server/service/twin-event/twin-event-type.js +18 -1
  139. package/dist-server/service/twin-event/twin-event-type.js.map +1 -1
  140. package/dist-server/service/twin-event/twin-event.d.ts +1 -0
  141. package/dist-server/service/twin-event/twin-event.js +9 -1
  142. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  143. package/dist-server/service/twin-forecast/twin-forecast-query.js +2 -1
  144. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  145. package/dist-server/service/twin-ingest-window/index.d.ts +5 -0
  146. package/dist-server/service/twin-ingest-window/index.js +10 -0
  147. package/dist-server/service/twin-ingest-window/index.js.map +1 -0
  148. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.d.ts +12 -0
  149. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js +123 -0
  150. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js.map +1 -0
  151. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.d.ts +1 -0
  152. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js +51 -0
  153. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js.map +1 -0
  154. package/dist-server/service/twin-ingest-window/twin-ingest-window.d.ts +16 -0
  155. package/dist-server/service/twin-ingest-window/twin-ingest-window.js +108 -0
  156. package/dist-server/service/twin-ingest-window/twin-ingest-window.js.map +1 -0
  157. package/dist-server/service/twin-instance/twin-instance.d.ts +1 -1
  158. package/dist-server/service/twin-instance/twin-instance.js +2 -2
  159. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  160. package/dist-server/service/twin-journal/twin-journal-query.js +58 -5
  161. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  162. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +14 -0
  163. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +74 -3
  164. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  165. package/dist-server/service/twin-metrics/twin-metrics-query.d.ts +1 -0
  166. package/dist-server/service/twin-metrics/twin-metrics-query.js +78 -3
  167. package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
  168. package/dist-server/service/twin-model/axis-journal-evidence.d.ts +2 -0
  169. package/dist-server/service/twin-model/axis-journal-evidence.js +89 -0
  170. package/dist-server/service/twin-model/axis-journal-evidence.js.map +1 -0
  171. package/dist-server/service/twin-model/epcis-coverage.d.ts +1 -1
  172. package/dist-server/service/twin-model/epcis-coverage.js +19 -7
  173. package/dist-server/service/twin-model/epcis-coverage.js.map +1 -1
  174. package/dist-server/service/twin-model/iec61850-coverage.d.ts +1 -1
  175. package/dist-server/service/twin-model/iec61850-coverage.js +18 -7
  176. package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -1
  177. package/dist-server/service/twin-model/isa95-coverage.d.ts +14 -0
  178. package/dist-server/service/twin-model/isa95-coverage.js +23 -9
  179. package/dist-server/service/twin-model/isa95-coverage.js.map +1 -1
  180. package/dist-server/service/twin-model/item-ref.d.ts +24 -0
  181. package/dist-server/service/twin-model/item-ref.js +88 -0
  182. package/dist-server/service/twin-model/item-ref.js.map +1 -0
  183. package/dist-server/service/twin-model/name-index.d.ts +36 -0
  184. package/dist-server/service/twin-model/name-index.js +116 -0
  185. package/dist-server/service/twin-model/name-index.js.map +1 -0
  186. package/dist-server/service/twin-model/project-structure.js +1 -1
  187. package/dist-server/service/twin-model/project-structure.js.map +1 -1
  188. package/dist-server/service/twin-model/status-tally.d.ts +9 -0
  189. package/dist-server/service/twin-model/status-tally.js +37 -0
  190. package/dist-server/service/twin-model/status-tally.js.map +1 -0
  191. package/dist-server/service/twin-model/twin-lineage-query.js +40 -12
  192. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
  193. package/dist-server/service/twin-model/twin-model-item-query.js +38 -39
  194. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
  195. package/dist-server/service/twin-model/twin-model-query.js +157 -5
  196. package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
  197. package/dist-server/service/twin-model/twin-model-tree-query.js +7 -0
  198. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
  199. package/dist-server/service/twin-readiness/index.d.ts +2 -0
  200. package/dist-server/service/twin-readiness/index.js +6 -0
  201. package/dist-server/service/twin-readiness/index.js.map +1 -0
  202. package/dist-server/service/twin-readiness/twin-readiness-query.d.ts +3 -0
  203. package/dist-server/service/twin-readiness/twin-readiness-query.js +103 -0
  204. package/dist-server/service/twin-readiness/twin-readiness-query.js.map +1 -0
  205. package/dist-server/service/twin-space/twin-space-resolver.d.ts +2 -2
  206. package/dist-server/service/twin-space/twin-space-resolver.js +5 -4
  207. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  208. package/dist-shared/entity-delta.d.ts +23 -3
  209. package/dist-shared/entity-delta.js +12 -8
  210. package/dist-shared/entity-delta.js.map +1 -1
  211. package/dist-shared/kpi-broadcast.js +1 -1
  212. package/dist-shared/kpi-broadcast.js.map +1 -1
  213. package/dist-shared/touched-items.d.ts +7 -0
  214. package/dist-shared/touched-items.js +80 -0
  215. package/dist-shared/touched-items.js.map +1 -0
  216. package/package.json +7 -7
  217. package/server/engine/canonical-ingest.ts +164 -15
  218. package/server/engine/command-routing.ts +1 -1
  219. package/server/engine/declared-stimulus.ts +66 -0
  220. package/server/engine/index.ts +6 -0
  221. package/server/engine/ingest-health.ts +704 -0
  222. package/server/engine/integration-coverage.ts +147 -0
  223. package/server/engine/integration-probes.ts +144 -0
  224. package/server/engine/integration-runner.ts +95 -0
  225. package/server/engine/integration-target-profile.ts +103 -0
  226. package/server/engine/kpi-fold.ts +101 -5
  227. package/server/engine/kpi-query.ts +137 -23
  228. package/server/engine/live-feed-registry.ts +4 -3
  229. package/server/engine/load-meter.ts +12 -3
  230. package/server/engine/local-declarations.ts +97 -19
  231. package/server/engine/log.ts +72 -0
  232. package/server/engine/loop-lag.ts +120 -0
  233. package/server/engine/measured-yield.ts +89 -0
  234. package/server/engine/model-gap.ts +168 -0
  235. package/server/engine/operation-basis.ts +33 -2
  236. package/server/engine/property-effects.ts +17 -0
  237. package/server/engine/restart-policy.ts +55 -0
  238. package/server/engine/runtime-key.ts +1 -1
  239. package/server/engine/spec-coverage.ts +23 -3
  240. package/server/engine/stage-path.ts +172 -0
  241. package/server/engine/twin-engine.ts +1318 -186
  242. package/server/index.ts +36 -3
  243. package/server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.ts +54 -0
  244. package/server/migrations/1786200000000-CarryLiveFeedCursor.ts +53 -0
  245. package/server/migrations/index.ts +7 -1
  246. package/server/service/index.ts +11 -0
  247. package/server/service/reference/control-routing.ts +62 -0
  248. package/server/service/reference/discovery-result.ts +1 -1
  249. package/server/service/reference/index.ts +7 -1
  250. package/server/service/reference/reference-adapter.ts +275 -5
  251. package/server/service/reference/reference-assessment.ts +215 -0
  252. package/server/service/reference/reference-live.ts +126 -14
  253. package/server/service/reference/reference-master.ts +64 -4
  254. package/server/service/reference/reference-probe.ts +264 -0
  255. package/server/service/reference/reference-progress-subscription.ts +73 -0
  256. package/server/service/reference/reference-progress.ts +95 -0
  257. package/server/service/reference/reference-resolver.ts +246 -19
  258. package/server/service/reference/twin-reference.ts +39 -1
  259. package/server/service/twin-audit/command-audit.ts +34 -1
  260. package/server/service/twin-audit/twin-audit-event.ts +20 -0
  261. package/server/service/twin-control/twin-control-mutation.ts +78 -30
  262. package/server/service/twin-event/journal-count.ts +53 -0
  263. package/server/service/twin-event/twin-event-keys.ts +16 -1
  264. package/server/service/twin-event/twin-event-type.ts +26 -2
  265. package/server/service/twin-event/twin-event.ts +7 -0
  266. package/server/service/twin-forecast/twin-forecast-query.ts +2 -1
  267. package/server/service/twin-ingest-window/index.ts +7 -0
  268. package/server/service/twin-ingest-window/twin-ingest-window-query.ts +125 -0
  269. package/server/service/twin-ingest-window/twin-ingest-window-writer.ts +57 -0
  270. package/server/service/twin-ingest-window/twin-ingest-window.ts +113 -0
  271. package/server/service/twin-instance/twin-instance.ts +16 -10
  272. package/server/service/twin-journal/twin-journal-query.ts +59 -5
  273. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +79 -4
  274. package/server/service/twin-metrics/twin-metrics-query.ts +81 -3
  275. package/server/service/twin-model/axis-journal-evidence.ts +57 -0
  276. package/server/service/twin-model/epcis-coverage.ts +2 -8
  277. package/server/service/twin-model/iec61850-coverage.ts +2 -8
  278. package/server/service/twin-model/isa95-coverage.ts +22 -9
  279. package/server/service/twin-model/item-ref.ts +80 -0
  280. package/server/service/twin-model/name-index.ts +94 -0
  281. package/server/service/twin-model/project-structure.ts +1 -1
  282. package/server/service/twin-model/status-tally.ts +37 -0
  283. package/server/service/twin-model/twin-lineage-query.ts +38 -9
  284. package/server/service/twin-model/twin-model-item-query.ts +26 -27
  285. package/server/service/twin-model/twin-model-query.ts +156 -5
  286. package/server/service/twin-model/twin-model-tree-query.ts +7 -0
  287. package/server/service/twin-readiness/index.ts +3 -0
  288. package/server/service/twin-readiness/twin-readiness-query.ts +96 -0
  289. package/server/service/twin-space/twin-space-resolver.ts +5 -4
  290. package/shared/entity-delta.ts +31 -7
  291. package/shared/kpi-broadcast.ts +1 -1
  292. package/shared/touched-items.ts +73 -0
  293. package/test/adopt-structure-live.test.ts +7 -7
  294. package/test/axis-journal-evidence.test.ts +71 -0
  295. package/test/axis-read.test.ts +101 -1
  296. package/test/boot-resume.test.ts +93 -27
  297. package/test/broadcast-cost-baseline.test.ts +200 -0
  298. package/test/broadcast-period.test.ts +58 -0
  299. package/test/canonical-ingest-vocabularies.test.ts +36 -1
  300. package/test/canonical-quantity-door.test.ts +61 -0
  301. package/test/command-routing.test.ts +1 -1
  302. package/test/control-capability.test.ts +103 -0
  303. package/test/declared-location-types.test.ts +89 -0
  304. package/test/declared-stimulus.test.ts +88 -0
  305. package/test/discovery-result.test.ts +1 -1
  306. package/test/duration-estimators.test.ts +1 -1
  307. package/test/entity-delta.test.ts +2 -2
  308. package/test/ingest-bench.test.ts +3 -3
  309. package/test/ingest-health-engine.test.ts +248 -0
  310. package/test/ingest-health-wiring.test.ts +119 -0
  311. package/test/ingest-health.test.ts +306 -0
  312. package/test/ingest-history.test.ts +247 -0
  313. package/test/ingest-running-guard.test.ts +5 -5
  314. package/test/instance-cache-lifecycle.test.ts +1 -1
  315. package/test/integration-probes.test.ts +103 -0
  316. package/test/integration-runner.test.ts +95 -0
  317. package/test/item-ref.test.ts +78 -0
  318. package/test/journal-read-discipline.test.ts +177 -0
  319. package/test/journal-retention.test.ts +133 -0
  320. package/test/journal-sort-axis.test.ts +142 -0
  321. package/test/journal-write-door.test.ts +110 -0
  322. package/test/journal-write-trend.test.ts +115 -0
  323. package/test/kernel-kind-guard.test.ts +4 -4
  324. package/test/kpi-baseline-db.test.ts +1 -1
  325. package/test/kpi-fold.test.ts +90 -3
  326. package/test/kpi-query-bench.test.ts +3 -3
  327. package/test/lineage-survives-restart.test.ts +20 -4
  328. package/test/live-cursor-wiring.test.ts +87 -0
  329. package/test/live-feed-registry.test.ts +15 -7
  330. package/test/live-kernel-facts.test.ts +7 -7
  331. package/test/live-mirror-parity.test.ts +6 -0
  332. package/test/load-meter.test.ts +29 -15
  333. package/test/local-declarations.test.ts +109 -2
  334. package/test/log-stamp.test.ts +59 -0
  335. package/test/loop-lag.test.ts +82 -0
  336. package/test/measured-yield.test.ts +90 -0
  337. package/test/model-gap.test.ts +153 -0
  338. package/test/oee-accumulator.test.ts +4 -0
  339. package/test/operation-basis.test.ts +28 -1
  340. package/test/operational-vocabulary.test.ts +108 -0
  341. package/test/operations-capability-db.test.ts +4 -4
  342. package/test/project-structure-db.test.ts +1 -1
  343. package/test/projection-reaches-screen.test.ts +1 -1
  344. package/test/property-effects.test.ts +28 -0
  345. package/test/read-failure-visible.test.ts +145 -0
  346. package/test/reference-grounding.test.ts +70 -0
  347. package/test/resolve-ts-siblings.mjs +52 -0
  348. package/test/restart-policy.test.ts +111 -0
  349. package/test/resync-origin-site.test.ts +7 -1
  350. package/test/revision-axis.test.ts +93 -0
  351. package/test/runtime-key.test.ts +2 -2
  352. package/test/scale-twin-bench.test.ts +2 -2
  353. package/test/source-outcome-audit.test.ts +104 -0
  354. package/test/spec-coverage.test.ts +1 -1
  355. package/test/stage-path.test.ts +95 -0
  356. package/test/standard-coverage.test.ts +15 -3
  357. package/test/status-tally.test.ts +55 -0
  358. package/test/structure-revision-db.test.ts +31 -27
  359. package/test/tenant-registry-db.test.ts +3 -3
  360. package/test/time-range.test.ts +152 -0
  361. package/test/touched-items.test.ts +61 -0
  362. package/test/twin-event-keys.test.ts +10 -2
  363. package/test/twin-model-item-db.test.ts +5 -3
  364. package/test/twin-model-tree-db.test.ts +7 -7
  365. package/test/twin-origin-resync.test.ts +4 -4
  366. package/test/warm-start-seam.test.ts +4 -0
  367. package/test/yield-loop.test.ts +199 -0
  368. package/tsconfig.shared.tsbuildinfo +1 -1
  369. package/tsconfig.tsbuildinfo +1 -1
@@ -14,11 +14,12 @@
14
14
  * 상수를 앱에서 재현하지 않고(무방언), 라이브 트윈에서도 자연스럽다(마지막 기록 ≈ 지금).
15
15
  * 어떤 기준을 썼는지는 결과의 `window.basis` 로 밝힌다 — 숨기면 사용자가 숫자를 오해한다.
16
16
  */
17
+ import { twinWarn } from './log.js'
17
18
  import { Between, In } from 'typeorm'
18
19
 
19
20
  import { hierarchyOf, levelOfLocationType, electricalUpstreamOf, EMS_LOCATION_TYPES, EMS_PROPERTY, type TariffDeclaration } from '@operato/twin-kernel'
20
21
 
21
- import { getRepository } from '@things-factory/shell'
22
+ import { getReadRepository, getRepository } from '@things-factory/shell'
22
23
 
23
24
  import { TwinEvent } from '../service/twin-event/twin-event.js'
24
25
  import { TwinInstance } from '../service/twin-instance/twin-instance.js'
@@ -135,11 +136,11 @@ export interface TwinKpiOutput extends Omit<Partial<KpiResult>, 'window'> {
135
136
  /**
136
137
  * 무엇을 집계했나 — 공간이면 합쳐 접은 트윈 목록.
137
138
  *
138
- * `realityModes` 는 **이 숫자가 무엇의 성과인지**를 말한다: mirror(실제 시스템을 반영) ·
139
- * sim-world(가상 세계) · sim-experiment(실험 사본). 같은 "345건" 이 현실의 실적일 수도, 시뮬레이션의
139
+ * `restartPolicies` 는 **이 숫자가 무엇의 성과인지**를 말한다: mirror(실제 시스템을 반영) ·
140
+ * `resume`(이어지는 가상 세계) · `reset`(재현 실험 사본). 같은 "345건" 이 현실의 실적일 수도, 시뮬레이션의
140
141
  * 산출일 수도 있다 — 그 구별 없이 숫자만 보여주면 사용자가 시뮬 결과를 실적으로 착각한다.
141
142
  */
142
- scope: { spaceId?: string; instanceIds: string[]; realityModes: string[] }
143
+ scope: { spaceId?: string; instanceIds: string[]; restartPolicies: string[] }
143
144
  /** 구간별 값(요청했을 때만) — 추세를 그리기 위한 최소 정보. */
144
145
  buckets?: { fromTime: string; toTime: string; tasks: number; orders: number; workP50Ms: number }[]
145
146
  /**
@@ -283,7 +284,7 @@ async function locationAreaMap(
283
284
  for (const row of rows) {
284
285
  const locations = (((row.model as any)?.locations ?? []) as any[]).filter(n => n?.id)
285
286
  if (!locations.length) continue
286
- /* 순환은 hierarchyOf 가 던진다 — 한 트윈의 잘못된 계층이 다른 트윈의 집계까지 죽이지 않게 여기서 가둔다. */
287
+ /* 순환은 hierarchyOf 가 오류를 낸다 — 한 트윈의 잘못된 계층이 다른 트윈의 집계까지 죽이지 않게 여기서 가둔다. */
287
288
  let h
288
289
  try {
289
290
  /*
@@ -293,7 +294,7 @@ async function locationAreaMap(
293
294
  */
294
295
  h = hierarchyOf({ locations }, t => levelOfLocationType(t, row.kind))
295
296
  } catch (e: any) {
296
- console.warn(`[twin-kpi] "${row.instanceId}": location hierarchy is broken — area rollup skipped for this twin: ${e?.message}`)
297
+ twinWarn(`[twin-kpi] "${row.instanceId}": location hierarchy is broken — area rollup skipped for this twin: ${e?.message}`)
297
298
  continue
298
299
  }
299
300
  for (const n of locations) {
@@ -483,22 +484,110 @@ async function latestEventTime(domainId: string, instanceIds: string[]): Promise
483
484
  */
484
485
  async function fetchEvents(domainId: string, instanceIds: string[], fromMs: number, toMs: number, lookbackMs: number) {
485
486
  if (instanceIds.length === 0) return { events: [] as KpiEvent[], capped: false }
486
- const rows = await getRepository(TwinEvent).find({
487
- where: {
488
- domain: { id: domainId },
489
- instanceId: In(instanceIds),
490
- eventType: In(BUSINESS_EVENTS),
491
- eventTime: Between(new Date(fromMs - lookbackMs), new Date(toMs))
492
- },
493
- order: { revision: 'ASC' },
494
- take: MAX_EVENTS
495
- })
487
+ /*
488
+ * ── 필요한 세 컬럼만, 그리고 **읽기 연결로** (2026-08-22 실측) ─────────────
489
+ * 읽기가 서버를 붙잡고 있었다. 실측(개발 DB 16.3GB · 저널 1,134만 행): 2만 행을 실어 오는 동안
490
+ * 다른 가벼운 읽기가 **47.6초** 기다렸다. 로그에서 이 자리 뒤로 사용자·도메인 조회까지 30.9초로
491
+ * 찍혔다 — TypeORM sqlite 드라이버는 연결을 하나만 들기 때문이다.
492
+ *
493
+ * **행 단위로 읽는 것 자체는 정당하다**: 이 폴드는 `p10·p50·p90` 을 낸다(§`kpi-fold`). 분위수는
494
+ * 카운터로 만들 없고 트윈별 사전집계로도 만들 수 없다(p90 의 평균은 p90 이 아니다). 개별 값이
495
+ * 있어야 한다.
496
+ *
497
+ * 정당하지 않은 것은 **읽는 방식**이었다. 세 가지를 고친다.
498
+ *
499
+ * ① **엔티티를 만들지 않는다.** 폴드가 쓰는 것은 `eventType`·`eventTime`·`payload` 뿐인데
500
+ * `find()` 는 열여섯 컬럼을 실어 엔티티로 만들고 관계 로더를 지난다. CPU 프로파일에서
501
+ * `RawSqlResultsToEntityTransformer` 와 `stringToSimpleJson` 이 상위였다.
502
+ * ② **읽기 연결로 보낸다**(`getReadRepository`). 순수 읽기이므로 쓰기와 순서를 다툴 것이 없다.
503
+ * 실측: 같은 2만 행 읽기에서 다른 읽기의 대기가 47.6초 → 2.8초.
504
+ * ③ **정렬 축을 거르는 축과 맞춘다.** 여기가 이 자리의 진짜 비용이었다.
505
+ *
506
+ * ── `ORDER BY revision` 이 `LIMIT` 을 무력화했다 (실측) ────────────────────
507
+ * 거르는 축은 `event_time`(창)이고 정렬 축은 `revision` 이었다. 그러면 sqlite 는 창에 맞는 행을
508
+ * **전부** 찾아 `revision` 으로 **전부 정렬한 뒤** 상한을 적용한다 — 상한이 정렬 뒤에 걸리므로
509
+ * 아무것도 줄여 주지 않는다. 실측이 그것을 그대로 보였다(인스턴스 하나가 375만 행):
510
+ *
511
+ * ORDER BY revision LIMIT 20000 → 32.2초 · LIMIT 5000 → 28.3초 · LIMIT 2000 → 28.2초
512
+ * ORDER BY eventTime, revision LIMIT 20000 → 0.41초 (78배)
513
+ *
514
+ * 계획도 그렇게 말한다: 앞은 `USE TEMP B-TREE FOR ORDER BY`(전체 정렬), 뒤는
515
+ * `USE TEMP B-TREE FOR LAST TERM OF ORDER BY` — 시각은 인덱스(`ix_twin_event_1`)에서 순서대로
516
+ * 나오고 **동시각만** 임시 정렬한다. 즉 범위를 훑다가 상한에서 멈춘다.
517
+ *
518
+ * ── 순서의 뜻은 **완전히 같지는 않다** (측정해서 고친 문장) ────────────────
519
+ * 처음에 「순서의 뜻은 같다」고 적었다. 재 보니 아니었다 — `revision` 순서와 `eventTime` 순서가
520
+ * 어긋나는 자리가 실재한다(hatio-mx2: 426만 행 중 **7건** · rosarito-mes: 11만 중 8건). 미러는
521
+ * 원본의 시각을 싣고 도착 순서로 리비전을 받으므로, 늦게 도착한 사건이 그 어긋남을 만든다.
522
+ *
523
+ * **그러나 폴드의 수치는 같다.** `foldTaskRecords`(커널 `task-fold.ts`)가 스트림 위치가 아니라
524
+ * **시각으로** 판정한다:
525
+ *
526
+ * startedMs = Math.min(startedMs, at)
527
+ * completedMs = Math.max(completedMs, at)
528
+ *
529
+ * `min`·`max` 이므로 행이 어떤 순서로 와도 답이 같다. 소요시간·분위수·처리량은 순서와 무관하다.
530
+ *
531
+ * 순서에 민감한 자리는 **하나**다: 그 폴드의 `location` 이 「마지막에 본 것」이다. 같은 밀리초에 같은
532
+ * 작업의 자리가 두 번 바뀌면 어느 것이 남는지가 순서로 갈린다(라이브 미러가 같은 밀리초에 여러 행을
533
+ * 보내는 경우다). 수치가 아니라 **표시되는 자리 하나**이고, 그래서 이 바꿈의 값이 그 위험보다 크다.
534
+ *
535
+ * `revision` 을 둘째 축으로 남기는 이유도 그것이다 — 동시각의 순서가 실행마다 달라지지 않게 한다.
536
+ *
537
+ * **상한을 낮추는 것으로는 고쳐지지 않는다**(위 숫자가 그것을 말한다). 정렬 축이 원인이다.
538
+ *
539
+ * 남는 비용을 감추지 않는다: `payload` 파싱은 여전히 메인 스레드 일이고 연결을 나눠도 나뉘지 않는다
540
+ * (실측 평균 1.5초). 그것을 더 줄이려면 폴드가 payload 에서 무엇을 쓰는지 좁혀야 한다 — 별 건이다.
541
+ */
542
+ const raw = await getReadRepository(TwinEvent)
543
+ .createQueryBuilder('e')
544
+ .select(['e.eventType AS "eventType"', 'e.eventTime AS "eventTime"', 'e.payload AS "payload"'])
545
+ .where('e.domain = :domainId', { domainId })
546
+ .andWhere('e.instanceId IN (:...instanceIds)', { instanceIds })
547
+ .andWhere('e.eventType IN (:...types)', { types: BUSINESS_EVENTS })
548
+ .andWhere('e.eventTime BETWEEN :from AND :to', { from: new Date(fromMs - lookbackMs), to: new Date(toMs) })
549
+ .orderBy('e.eventTime', 'ASC')
550
+ .addOrderBy('e.revision', 'ASC')
551
+ .limit(MAX_EVENTS)
552
+ .getRawMany<{ eventType: string; eventTime: unknown; payload: unknown }>()
553
+
496
554
  /* 상한에 정확히 닿으면 **더 있었을 수 있다** — 그 사실을 돌려준다. 조용히 잘라내면 숫자가 조용히
497
555
  * 작아지고, 사용자는 그것을 성과 하락으로 읽는다(가장 나쁜 종류의 거짓이다). */
498
556
  return {
499
557
  /* 폴드는 ISO 문자열 축으로 수행된다(kpi-fold) — 경계에서 한 번만 옮긴다. */
500
- events: rows.map(r => ({ eventType: r.eventType, eventTime: r.eventTime?.toISOString(), payload: r.payload })),
501
- capped: rows.length >= MAX_EVENTS
558
+ events: raw.map(r => ({ eventType: r.eventType, eventTime: isoOf(r.eventTime), payload: jsonOf(r.payload) })),
559
+ capped: raw.length >= MAX_EVENTS
560
+ }
561
+ }
562
+
563
+ /**
564
+ * 원시 결과의 시각을 ISO 로 — **드라이버마다 모양이 다르다.**
565
+ *
566
+ * 엔티티 경로에서는 TypeORM 이 `Date` 로 바꿔 주지만, 원시 결과는 드라이버가 준 그대로다(sqlite 는
567
+ * `'2026-08-07 11:00:00.000'`, pg 는 `Date`). 경계에서 한 번 정규화한다 — 폴드는 ISO 문자열만 안다.
568
+ */
569
+ function isoOf(v: unknown): string | undefined {
570
+ if (v == null) return undefined
571
+ if (v instanceof Date) return v.toISOString()
572
+ const s = String(v)
573
+ /* sqlite 의 공백 구분 형식을 ISO 로 — 그대로 `Date` 에 주면 시간대 해석이 갈린다. */
574
+ const d = new Date(s.includes('T') ? s : s.replace(' ', 'T') + (s.endsWith('Z') ? '' : 'Z'))
575
+ return Number.isNaN(d.getTime()) ? undefined : d.toISOString()
576
+ }
577
+
578
+ /**
579
+ * 원시 결과의 `payload` 를 객체로 — **엔티티 경로의 `simple-json` 변환을 여기서 한다.**
580
+ *
581
+ * 깨진 JSON 은 **버리지 않고 비운다**: 그 사건이 있었다는 사실은 남고, 안을 읽을 수 없다는 것만
582
+ * 폴드에 전해진다(폴드는 없는 필드를 건너뛴다). 던지면 창 하나가 통째로 사라진다.
583
+ */
584
+ function jsonOf(v: unknown): any {
585
+ if (v == null) return undefined
586
+ if (typeof v !== 'string') return v
587
+ try {
588
+ return JSON.parse(v)
589
+ } catch {
590
+ return undefined
502
591
  }
503
592
  }
504
593
 
@@ -508,7 +597,7 @@ async function fetchEvents(domainId: string, instanceIds: string[], fromMs: numb
508
597
  * 공간이 주어지면 **가동 중이고 운영 목적인** 것만 모은다 — 벤치 사본은 사용자가 보는 현장이 아니다.
509
598
  * 트윈이 지정되면 그것 하나. 둘 다 없으면 무엇을 재야 할지 알 수 없으므로 명시 실패한다.
510
599
  */
511
- async function resolveTargets(input: TwinKpiInput): Promise<{ instanceIds: string[]; realityModes: string[] }> {
600
+ async function resolveTargets(input: TwinKpiInput): Promise<{ instanceIds: string[]; restartPolicies: string[] }> {
512
601
  if (!input.instanceId && !input.spaceId) throw new Error('either instanceId or spaceId is required')
513
602
  const list = await TwinEngine.list(input.domainId)
514
603
  const rows = input.instanceId
@@ -517,8 +606,8 @@ async function resolveTargets(input: TwinKpiInput): Promise<{ instanceIds: strin
517
606
  /* 목록에서 못 찾아도(정지·비등록) 지정된 트윈은 그대로 조회한다 — 저널은 남아 있을 수 있다. */
518
607
  const instanceIds = rows.length ? rows.map((x: any) => x.instanceId as string) : input.instanceId ? [input.instanceId] : []
519
608
  /* 컬럼이 NOT NULL 이라 'unknown' 으로 메울 일이 없다 — 비면 계약이 깨진 것이지 모르는 상태가 아니다. */
520
- const realityModes = [...new Set(rows.map((x: any) => String(x.realityMode)))]
521
- return { instanceIds, realityModes }
609
+ const restartPolicies = [...new Set(rows.map((x: any) => String(x.restartPolicy)))]
610
+ return { instanceIds, restartPolicies }
522
611
  }
523
612
 
524
613
  /**
@@ -541,8 +630,8 @@ export async function computeTwinKpi(input: TwinKpiInput): Promise<TwinKpiOutput
541
630
  const minutes = Math.max(1, Math.min(24 * 60, Number(input.windowMinutes) || 60))
542
631
  const lookbackMs = Math.max(0, Math.min(24 * 60, input.lookbackMinutes ?? 60)) * 60_000
543
632
 
544
- const { instanceIds, realityModes } = await resolveTargets(input)
545
- const scope = { spaceId: input.spaceId, instanceIds, realityModes }
633
+ const { instanceIds, restartPolicies } = await resolveTargets(input)
634
+ const scope = { spaceId: input.spaceId, instanceIds, restartPolicies }
546
635
 
547
636
  /* 끝 시각 — 지정이 없으면 대상들의 마지막 기록. 그것도 없으면 측정할 것이 없다. */
548
637
  let basis: 'given' | 'latest-event' = 'given'
@@ -597,6 +686,31 @@ export async function computeTwinKpi(input: TwinKpiInput): Promise<TwinKpiOutput
597
686
  ...shiftCtx,
598
687
  groupLimit: input.groupLimit
599
688
  })
689
+ /*
690
+ * **완료로 세지 않은 오더 상태 낱말을 말한다** (2026-08-23).
691
+ *
692
+ * 폴드는 순수하다(로그를 모른다) — 그래서 부르는 쪽이 말한다. 왜 말해야 하나: 이 폴드는 오더의
693
+ * 완료를 **동의어 목록**으로 판정하고, 그것은 원본의 낱말에서 뜻을 추론하는 것이다. 커넥터가
694
+ * 자기 표로 옮기지 않으면 그 낱말이 목록에 없어 **완료가 조용히 0 으로 세어진다.**
695
+ *
696
+ * 실제로 그랬다: 첫 실 원본의 `FINISHED` 가 목록에 없어 완료 오더가 전부 0 이었고, 사람이 화면을
697
+ * 보고 의심해서야 드러났다(그 커넥터에는 상태 표가 정의돼 **있으면서** 오더 경로에서 쓰이지 않았다).
698
+ * 그때 목록에 낱말을 더한 것은 그 사실을 가린 땜빵이었다.
699
+ *
700
+ * 그러니 이 로그는 「목록을 늘려라」가 아니라 **「그 커넥터가 옮기지 않았다」**를 뜻한다. 화면을 보지
701
+ * 않는 사람도 알아야 하므로 로그에 남긴다 — 조용한 0 이 이 시스템에서 가장 비싼 침묵이다.
702
+ *
703
+ * 완료가 하나도 없을 때만 말한다: `picking` 처럼 완료가 아닌 낱말이 섞여 있는 것은 정상이고,
704
+ * 그것까지 매번 경고하면 진짜 신호가 묻힌다.
705
+ */
706
+ const notCounted = Object.entries(kpi.orderStatusNotCounted ?? {})
707
+ if (notCounted.length && kpi.throughput.orders === 0) {
708
+ twinWarn(
709
+ `[twin-kpi] no order counted as completed in this window, but these order status words were seen: ` +
710
+ `${notCounted.map(([w, n]) => `${w}×${n}`).join(' · ')} — the connector may not be translating them ` +
711
+ `to the kernel's vocabulary (see its own status table).`
712
+ )
713
+ }
600
714
  /*
601
715
  * 목표를 **폴드 뒤, 그룹 조립 앞**에 읽는다 — 전체 판정과 축별 판정이 같은 목록에서 나와야
602
716
  * 두 곳의 임계가 갈라지지 않는다. 폴드 자체는 목표를 모른다(사실 층과 판정 층의 분리).
@@ -1,3 +1,4 @@
1
+ import { twinWarn } from './log.js'
1
2
  /*
2
3
  * 「이 트윈에 계측이 들어오고 있나」 — **그 사실을 한 자리에서 묻는다.**
3
4
  *
@@ -35,7 +36,7 @@ export function liveFeedAttached(instanceId: string): boolean {
35
36
  try {
36
37
  if (probe().includes(instanceId)) return true
37
38
  } catch (e: any) {
38
- console.warn('[twin-live] a feed registry could not answer —', e?.message ?? e)
39
+ twinWarn('[twin-live] a feed registry could not answer —', e?.message ?? e)
39
40
  }
40
41
  }
41
42
  return false
@@ -51,8 +52,8 @@ export function liveFeedAttached(instanceId: string): boolean {
51
52
  */
52
53
  export type LiveFeedState = 'attached' | 'detached' | 'not-applicable'
53
54
 
54
- export function liveFeedStateOf(args: { realityMode?: string; running?: boolean; instanceId: string }): LiveFeedState {
55
- if (args.realityMode !== 'mirror') return 'not-applicable'
55
+ export function liveFeedStateOf(args: { restartPolicy?: string; running?: boolean; instanceId: string }): LiveFeedState {
56
+ if (args.restartPolicy !== 'resync') return 'not-applicable'
56
57
  if (!args.running) return 'not-applicable'
57
58
  return liveFeedAttached(args.instanceId) ? 'attached' : 'detached'
58
59
  }
@@ -8,11 +8,11 @@
8
8
  *
9
9
  * 실제로 그런 상태를 만났다: 포트는 LISTEN 이고 webpack 은 `compiled successfully` 인데 요청이 하나도
10
10
  * 처리되지 않고 CPU 만 99% 였다. **터지면 알지만 느려지면 몰랐다** — `tickGuarded` 는 예외만 잡고
11
- * 시간은 재지 않았고, `TwinMetrics` 에는 라이브 지표(유입·방송·저널)뿐이라 시뮬 틱은 아무도 안 봤다.
11
+ * 시간은 재지 않았고, `TwinMetrics` 에는 라이브 지표(유입·브로드캐스팅·저널)뿐이라 시뮬 틱은 아무도 안 봤다.
12
12
  *
13
13
  * ── 총량만으로는 부족하다 ─────────────────────────────────────────────────
14
14
  * "틱이 800ms 걸렸다" 는 어디를 고칠지 알려 주지 않는다. 한 주기는 여러 작업으로 이뤄진다 —
15
- * 커널 틱 · 상태 투영(snapshot) · 방송 payload 만들기 · 발행 · 저널 기록 · 유입 흡수. 그래서
15
+ * 커널 틱 · 상태 투영(snapshot) · 브로드캐스팅 payload 만들기 · 발행 · 저널 기록 · 유입 흡수. 그래서
16
16
  * **작업마다 따로** 잰다. 어느 작업이 총 시간의 몇 %인지 보이면 손댈 곳이 정해진다.
17
17
  *
18
18
  * 재고 말하는 것이 먼저다. 워커 스레드로 옮기는 것은 그다음이다 — 무엇이 느린지 모른 채 옮기면
@@ -20,7 +20,16 @@
20
20
  */
21
21
 
22
22
  /** 작업 이름 — 새 작업을 재기 시작하면 여기 늘린다(문자열을 자유롭게 쓰지 않는다: 오타가 새 칸이 된다). */
23
- export const LOAD_PHASES = ['tick', 'snapshot', 'deltas', 'publish', 'journal', 'ingest', 'forecast', 'forkTick'] as const
23
+ /*
24
+ * ── 부팅 구간을 재는 두 이름 (2026-08-22) ───────────────────────────────────
25
+ * 실측으로 부팅이 620~717초였고 트윈 하나를 세우는 데 15~93초였다. 그런데 **어느 작업이 그 시간을
26
+ * 쓰는지 아무 계기도 답하지 못했다** — 재는 이름이 도는 중의 작업(틱·브로드캐스팅·저널)뿐이었다.
27
+ * 짐작으로 원인을 적으면 그것이 사실로 읽히므로, 그 자리에 이름을 둔다.
28
+ *
29
+ * · `warmStart` — 저장된 상태를 커널에 심는다(물품·오더·작업·주목 시각).
30
+ * · `stimulus` — 선언된 자극을 싣고 시작한다(실측: 큰 트윈에서 이 뒤가 29~72초였다).
31
+ */
32
+ export const LOAD_PHASES = ['tick', 'snapshot', 'deltas', 'publish', 'journal', 'ingest', 'forecast', 'forkTick', 'warmStart', 'stimulus'] as const
24
33
  export type LoadPhase = (typeof LOAD_PHASES)[number]
25
34
 
26
35
  export interface PhaseStats {
@@ -37,7 +37,7 @@
37
37
  * **속성 어휘는 사용자가 정의하지 않는다** — 커널이 정한 속성에 값을 넣는 문이다(그래서 이름도
38
38
  * 「사용자 정의 속성」이 아니다).
39
39
  */
40
- import { ATTENTION_PROPERTY, EMS_PROPERTY_SPEC, OPERATION_PROPERTY } from '@operato/twin-kernel'
40
+ import { ATTENTION_PROPERTY, EMS_PROPERTY_SPEC, OPERATION_PROPERTY, OP_PARAM } from '@operato/twin-kernel'
41
41
 
42
42
  import { EQUIPMENT_PROPERTY, SPEED_TO_MPS } from './travel-estimator.ts'
43
43
  import { declarationTargetsOf } from './property-effects.ts'
@@ -241,6 +241,28 @@ const COMMON_SPECS: DeclarablePropertySpec[] = [
241
241
  range: [0.01, 100000],
242
242
  note: 'how long this operation takes at this site — the simulation and the forecast use it instead of the built-in constant. a unit is required. history still wins where it exists.',
243
243
  uomChoices: Object.keys(DURATION_UOM_MS)
244
+ },
245
+ /*
246
+ * 양품률 — **퍼센트로 받는다**(80 이 80%다).
247
+ *
248
+ * 커널이 읽는 값은 0..1 이지만(ISA-95 `Parameter` 값 그대로) 사람에게 0.8 을 넣으라고 하면 SOC 에서
249
+ * 겪은 함정을 되풀게 된다(0.8 을 0.8% 로 쓴 일이 있다). 그래서 화면·AI 는 퍼센트로 말하고, 비율로
250
+ * 바꾸는 일은 얹는 자리 한 곳에서 한다.
251
+ */
252
+ {
253
+ id: OPERATION_PROPERTY.yield,
254
+ uom: '%',
255
+ dataType: 'xs:double',
256
+ range: [1, 100],
257
+ note: 'good-output rate of this operation as a percentage 1-100 (95 means 95%) — the kernel reads it as a ratio, and this value replaces the built-in default that also drives the high-scrap attention.'
258
+ },
259
+ /* 셋업·체인지오버 — 가동 앞에 붙는 시간. 소요시간과 같은 단위 목록을 쓴다(같은 종류의 값이다). */
260
+ {
261
+ id: OPERATION_PROPERTY.setupDuration,
262
+ dataType: 'xs:double',
263
+ range: [0.01, 100000],
264
+ note: 'changeover time before this operation runs — added in front of the cycle. a unit is required.',
265
+ uomChoices: Object.keys(DURATION_UOM_MS)
244
266
  }
245
267
  ]
246
268
 
@@ -250,6 +272,18 @@ const COMMON_SPECS: DeclarablePropertySpec[] = [
250
272
  * 읽을 수 없으면 `undefined` — 부르는 쪽이 그 사실을 거절 사유로 올린다(0 을 만들지 않는다: 소요 0 인
251
273
  * 작업은 영원히 끝나지 않는다).
252
274
  */
275
+ /**
276
+ * 공정 모수 표에 한 칸 담는다 — **커널이 읽는 표기 그대로**(수율 0..1, 셋업 ISO 8601).
277
+ *
278
+ * 표를 통째로 새로 만들어 얹는다: 같은 모델 객체를 여러 겹이 지나므로(현장 → 트윈) 그때마다 새 객체여야
279
+ * 앞 겹의 것을 조용히 덮어쓰지 않는다.
280
+ */
281
+ function putParam(next: any, kind: string, paramId: string, value: string): void {
282
+ const table = { ...(next.localParams ?? {}) }
283
+ table[kind] = { ...(table[kind] ?? {}), [paramId]: value }
284
+ next.localParams = table
285
+ }
286
+
253
287
  export function isoDurationOf(value: string | number, uom: string): string | undefined {
254
288
  const n = Number(value)
255
289
  const per = DURATION_UOM_MS[String(uom ?? '').trim()]
@@ -418,7 +452,9 @@ export function mergeLocalDeclarations(
418
452
  * 모델에 없는 대상에 얹으려 하면 **조용히 버리지 않고** 그 사실을 돌려준다(구조가 바뀌어 그 설비가
419
453
  * 사라졌을 수 있고, 그때 사용자의 선언은 갈 곳이 없다).
420
454
  */
421
- export function applyLocalDeclarations<T extends { locations?: any[]; equipment?: any[]; localDurations?: Record<string, string> }>(
455
+ export function applyLocalDeclarations<
456
+ T extends { locations?: any[]; equipment?: any[]; localDurations?: Record<string, string>; localParams?: Record<string, Record<string, string>> }
457
+ >(
422
458
  model: T,
423
459
  declarations: readonly LocalDeclaration[] | undefined | null
424
460
  ): {
@@ -489,20 +525,37 @@ export function applyLocalDeclarations<T extends { locations?: any[]; equipment?
489
525
  * 이 표는 **전부 우리가 정한 값**이다(원천이 그리는 자리가 아니다). 그래서 기억(`replaced`)은 언제나
490
526
  * `null` 이고, 철회는 「표에서 뺀다」로 끝난다 — 그러면 원천 명세나 커널 상수가 다시 답한다.
491
527
  */
492
- const putOperation = (durations: Record<string, string>, d: LocalDeclaration, memo: LocalDeclaration): void => {
528
+ const putOperation = (next: any, d: LocalDeclaration, memo: LocalDeclaration): void => {
493
529
  for (let i = 0; i < d.properties.length; i++) {
494
530
  const p = d.properties[i]
495
- if (p.id !== OPERATION_PROPERTY.duration) {
496
- unreadable.push({ target: 'operation', id: d.id, propertyId: p.id })
531
+ const uom = String((p as any).uom ?? '')
532
+ /*
533
+ * 값을 **커널이 읽는 표기로** 바꿔 담는다 — 시간은 ISO 8601, 수율은 비율(0..1).
534
+ * 바꿀 수 없으면 담지 않고 그 사실을 낸다(조용히 0 이나 빈 값으로 만들지 않는다).
535
+ */
536
+ if (p.id === OPERATION_PROPERTY.duration) {
537
+ const iso = isoDurationOf(p.value, uom)
538
+ if (!iso) { unreadable.push({ target: 'operation', id: d.id, propertyId: p.id }); continue }
539
+ next.localDurations = { ...(next.localDurations ?? {}), [d.id]: iso }
540
+ memo.properties[i].replaced = null
497
541
  continue
498
542
  }
499
- const iso = isoDurationOf(p.value, String((p as any).uom ?? ''))
500
- if (!iso) {
501
- unreadable.push({ target: 'operation', id: d.id, propertyId: p.id })
543
+ if (p.id === OPERATION_PROPERTY.setupDuration) {
544
+ const iso = isoDurationOf(p.value, uom)
545
+ if (!iso) { unreadable.push({ target: 'operation', id: d.id, propertyId: p.id }); continue }
546
+ putParam(next, d.id, OP_PARAM.setupDuration, iso)
547
+ memo.properties[i].replaced = null
502
548
  continue
503
549
  }
504
- durations[d.id] = iso
505
- memo.properties[i].replaced = null
550
+ if (p.id === OPERATION_PROPERTY.yield) {
551
+ const pct = Number(p.value)
552
+ if (!Number.isFinite(pct) || pct <= 0 || pct > 100) { unreadable.push({ target: 'operation', id: d.id, propertyId: p.id }); continue }
553
+ /* 퍼센트 → 비율. 이 환산이 있는 자리는 여기 하나다(두 곳에서 나누면 100 배가 두 번 걸린다). */
554
+ putParam(next, d.id, OP_PARAM.yield, String(pct / 100))
555
+ memo.properties[i].replaced = null
556
+ continue
557
+ }
558
+ unreadable.push({ target: 'operation', id: d.id, propertyId: p.id })
506
559
  }
507
560
  }
508
561
 
@@ -511,15 +564,15 @@ export function applyLocalDeclarations<T extends { locations?: any[]; equipment?
511
564
  ...model,
512
565
  ...(model.locations ? { locations: model.locations.map(n => ({ ...n, ...(n?.properties ? { properties: [...n.properties] } : {}) })) } : {}),
513
566
  ...(model.equipment ? { equipment: model.equipment.map(e => ({ ...e, ...(e?.properties ? { properties: [...e.properties] } : {}) })) } : {}),
514
- ...(model.localDurations ? { localDurations: { ...model.localDurations } } : {})
567
+ ...(model.localDurations ? { localDurations: { ...model.localDurations } } : {}),
568
+ ...(model.localParams ? { localParams: Object.fromEntries(Object.entries(model.localParams).map(([k, v]) => [k, { ...v }])) } : {})
515
569
  }
516
570
  for (let i = 0; i < decls.length; i++) {
517
571
  const d = decls[i]
518
572
  if (d.target === 'operation') {
519
- /* 빈 표를 남기지 않는다 — 「선언이 있다」와 「읽을 수 있는 선언이 하나도 없다」는 다른 사실이다. */
520
- const table: Record<string, string> = next.localDurations ?? {}
521
- putOperation(table, d, layer[i])
522
- if (Object.keys(table).length) next.localDurations = table
573
+ /* 빈 표를 남기지 않는다 — 「선언이 있다」와 「읽을 수 있는 선언이 하나도 없다」는 다른 사실이다
574
+ (`putOperation` 담을 것이 있을 때만 표를 만든다). */
575
+ putOperation(next, d, layer[i])
523
576
  continue
524
577
  }
525
578
  const ok = put(d.target === 'equipment' ? next.equipment : next.locations, d, layer[i])
@@ -585,7 +638,9 @@ export function revokeLocalDeclarations(
585
638
  * · 기억이 없으면(옛 선언) **손대지 않고** `unknownSource` 로 말한다 — 원래 값을 지어내는 것은
586
639
  * 잘못 넣은 값을 그대로 두는 것보다 나쁘다(다음 원천 재읽기가 그 자리를 바로잡는다).
587
640
  */
588
- export function restoreRevoked<T extends { locations?: any[]; equipment?: any[]; localDurations?: Record<string, string> }>(
641
+ export function restoreRevoked<
642
+ T extends { locations?: any[]; equipment?: any[]; localDurations?: Record<string, string>; localParams?: Record<string, Record<string, string>> }
643
+ >(
589
644
  model: T,
590
645
  revoked: readonly RevokedDeclaration[]
591
646
  ): { model: T; restored: RevokedDeclaration[]; unknownSource: DeclarationRevocation[]; missingTargets: { target: string; id: string }[] } {
@@ -595,7 +650,8 @@ export function restoreRevoked<T extends { locations?: any[]; equipment?: any[];
595
650
  ...model,
596
651
  ...(model.locations ? { locations: model.locations.map(n => ({ ...n, ...(n?.properties ? { properties: [...n.properties] } : {}) })) } : {}),
597
652
  ...(model.equipment ? { equipment: model.equipment.map(e => ({ ...e, ...(e?.properties ? { properties: [...e.properties] } : {}) })) } : {}),
598
- ...(model.localDurations ? { localDurations: { ...model.localDurations } } : {})
653
+ ...(model.localDurations ? { localDurations: { ...model.localDurations } } : {}),
654
+ ...(model.localParams ? { localParams: Object.fromEntries(Object.entries(model.localParams).map(([k, v]) => [k, { ...v }])) } : {})
599
655
  }
600
656
  const restored: RevokedDeclaration[] = []
601
657
  const unknownSource: DeclarationRevocation[] = []
@@ -607,13 +663,33 @@ export function restoreRevoked<T extends { locations?: any[]; equipment?: any[];
607
663
  * 빼면 원천 명세(있으면)나 커널 상수가 다시 답한다: 그것이 「선언 이전」의 상태다.
608
664
  */
609
665
  if (r.target === 'operation') {
666
+ /* 시간은 시간 표에서, 모수(수율·셋업)는 모수 표에서 뺀다 — 담긴 자리에서 빼야 실제로 빠진다. */
667
+ const paramId =
668
+ r.propertyId === OPERATION_PROPERTY.yield
669
+ ? OP_PARAM.yield
670
+ : r.propertyId === OPERATION_PROPERTY.setupDuration
671
+ ? OP_PARAM.setupDuration
672
+ : undefined
673
+ if (paramId) {
674
+ const params: Record<string, Record<string, string>> = next.localParams ?? {}
675
+ if (!params[r.id] || !(paramId in params[r.id])) {
676
+ missingTargets.push({ target: r.target, id: r.id })
677
+ continue
678
+ }
679
+ delete params[r.id][paramId]
680
+ /* 빈 껍데기를 남기지 않는다 — 「선언이 있다」와 「빈 표가 있다」가 구별되지 않는다. */
681
+ if (!Object.keys(params[r.id]).length) delete params[r.id]
682
+ if (!Object.keys(params).length) delete next.localParams
683
+ else next.localParams = params
684
+ restored.push(r)
685
+ continue
686
+ }
610
687
  const table: Record<string, string> = next.localDurations ?? {}
611
688
  if (!(r.id in table)) {
612
689
  missingTargets.push({ target: r.target, id: r.id })
613
690
  continue
614
691
  }
615
692
  delete table[r.id]
616
- /* 빈 표는 남기지 않는다 — 「선언이 있다」와 「빈 표가 있다」가 구별되지 않는다. */
617
693
  if (!Object.keys(table).length) delete next.localDurations
618
694
  else next.localDurations = table
619
695
  restored.push(r)
@@ -661,7 +737,9 @@ export type DeclarationScope = 'space' | 'instance'
661
737
  * 기억(`replaced`)은 각 겹이 **자기가 덮은 값**을 적는다: 트윈 겹의 기억은 「현장 값을 덮었다」가 되고,
662
738
  * 철회하면 현장 값으로 돌아간다(원천까지 가지 않는다). 그것이 사람이 기대하는 되돌림이다.
663
739
  */
664
- export function applyDeclarationLayers<T extends { locations?: any[]; equipment?: any[]; localDurations?: Record<string, string> }>(
740
+ export function applyDeclarationLayers<
741
+ T extends { locations?: any[]; equipment?: any[]; localDurations?: Record<string, string>; localParams?: Record<string, Record<string, string>> }
742
+ >(
665
743
  model: T,
666
744
  spaceLayer: readonly LocalDeclaration[] | undefined | null,
667
745
  instanceLayer: readonly LocalDeclaration[] | undefined | null
@@ -0,0 +1,72 @@
1
+ /*
2
+ * 트윈 로그의 문 — **모든 줄에 시각을 찍는다.** 순수(프레임워크 의존 없음).
3
+ *
4
+ * ── 왜 필요한가 (2026-08-22) ────────────────────────────────────────────────
5
+ * 「서버가 왜 느린가」를 조사하면서 루프가 55초 막힌 것을 계기로 알아냈는데(`loop-lag.ts`),
6
+ * **그 시각에 무슨 일이 있었는지 로그에서 찾을 수 없었다.** 트윈의 로그가 `console.log` 로 나가
7
+ * 시각이 없었기 때문이다. 프레임워크 로그(`2026-08-22T05:54:12+09:00 info: …`)와 섞여 있어서, 어느
8
+ * 줄이 언제 찍혔는지 앞뒤 프레임워크 줄로 짐작해야 했다.
9
+ *
10
+ * ── 왜 프레임워크 로거를 그대로 쓰지 않나 ──────────────────────────────────
11
+ * 재발명을 피하려고 `@things-factory/env` 의 `logger` 를 쓰려 했는데, 실측으로 막혔다: 앱 **밖**에서는
12
+ * 그 로거에 전송지가 하나도 없다(설정이 앱에서 주입된다). 그래서 시험·도구에서 부르면
13
+ *
14
+ * [winston] Attempt to write logs with no transports … {"message":"…","level":"info"}
15
+ *
16
+ * 가 나오고 **메시지 자체가 사라진다.** 트윈 엔진은 시험과 CLI 도구에서도 불려 다니므로, 그 조건에서
17
+ * 로그가 없어지는 것은 바꿔선 안 되는 거동이다(로그가 사라지는 것이 지금 문제의 원인이었다).
18
+ *
19
+ * 그래서 **싱크는 그대로 두고**(console) 빠진 사실 하나만 채운다 — 시각. 형식은 프레임워크와 같은
20
+ * 모양으로 맞춰 한 파일에서 섞여 읽히게 하고, **밀리초까지** 남긴다(계기의 값이 epoch ms 라
21
+ * 초 단위로는 같은 초 안의 순서를 가릴 수 없다).
22
+ */
23
+
24
+ /** 두 자리·세 자리 0 채우기 — 형식이 흔들리면 정렬해 읽을 수 없다. */
25
+ const p2 = (n: number) => String(n).padStart(2, '0')
26
+ const p3 = (n: number) => String(n).padStart(3, '0')
27
+
28
+ /**
29
+ * 지역 시각 + 오프셋 — `2026-08-22T05:54:12.345+09:00`.
30
+ *
31
+ * UTC 로 찍지 않는다: 이 로그를 읽는 사람은 현장의 시각으로 이야기하고, 프레임워크의 다른 줄도
32
+ * 지역 시각이다. 두 줄이 다른 기준이면 나란히 놓고 읽을 수 없다.
33
+ */
34
+ export function stamp(at: Date = new Date()): string {
35
+ const off = -at.getTimezoneOffset()
36
+ const sign = off >= 0 ? '+' : '-'
37
+ const abs = Math.abs(off)
38
+ return (
39
+ `${at.getFullYear()}-${p2(at.getMonth() + 1)}-${p2(at.getDate())}` +
40
+ `T${p2(at.getHours())}:${p2(at.getMinutes())}:${p2(at.getSeconds())}.${p3(at.getMilliseconds())}` +
41
+ `${sign}${p2(Math.floor(abs / 60))}:${p2(abs % 60)}`
42
+ )
43
+ }
44
+
45
+ /**
46
+ * 인자 여럿을 한 줄로 — **어떤 인자도 버리지 않는다.**
47
+ *
48
+ * `console.error(msg, err)` 처럼 쓰던 자리가 많다. 프레임워크 로거에 그대로 넘기면 두 번째 인자는
49
+ * meta 로 들어가 우리 형식(`message` 만 찍는다)에서 **조용히 사라진다.** 그래서 여기서 문자열로 편다.
50
+ * 오류는 스택까지 — 스택이 없으면 「어디서 났나」를 잃는다.
51
+ */
52
+ export function line(args: unknown[]): string {
53
+ return args
54
+ .map(a => {
55
+ if (typeof a === 'string') return a
56
+ if (a instanceof Error) return a.stack ?? a.message
57
+ if (a === undefined) return 'undefined'
58
+ if (a === null) return 'null'
59
+ try {
60
+ return typeof a === 'object' ? JSON.stringify(a) : String(a)
61
+ } catch {
62
+ /* 순환 참조 등 — 값을 못 적어도 **무엇이 있었다는 사실**은 남긴다. */
63
+ return String(a)
64
+ }
65
+ })
66
+ .join(' ')
67
+ }
68
+
69
+ /* 수준 표기는 프레임워크와 같은 낱말을 쓴다(`info`·`warn`·`error`) — 같은 파일에서 걸러 읽는다. */
70
+ export const twinLog = (...args: unknown[]): void => console.log(`${stamp()} info: ${line(args)}`)
71
+ export const twinWarn = (...args: unknown[]): void => console.warn(`${stamp()} warn: ${line(args)}`)
72
+ export const twinError = (...args: unknown[]): void => console.error(`${stamp()} error: ${line(args)}`)