@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
@@ -0,0 +1,264 @@
1
+ import type { ReferenceAdapter, SiteDescriptor } from './reference-adapter.js'
2
+ import type { ReferenceMaster } from './reference-master.js'
3
+ import { capabilitiesOf } from './reference-adapter.js'
4
+ import { REFERENCE_STEPS, type ReferenceStep } from './reference-progress.js'
5
+
6
+ /*
7
+ * **커넥터 점검 하네스** — 실 원본에 붙여 「무엇이 채워지고 무엇이 비는가」를 한 번에 본다.
8
+ *
9
+ * ── 왜 필요한가 (2026-08-21) ────────────────────────────────────────────────
10
+ * chef v1 커넥터를 만들 때 실 인스턴스(포천)에 붙이는 확인을 매번 손으로 했다 — curl 로 필드를 묻고,
11
+ * 일회용 스크립트로 마스터를 읽고, 결과를 눈으로 셌다. 그 방식은 세 가지를 잃는다:
12
+ * · 같은 확인을 다음 커넥터에서 처음부터 다시 만든다.
13
+ * · 무엇을 확인했는지가 어디에도 남지 않는다.
14
+ * · **앱을 띄워야 알 수 있는 것**이 되어 버린다 — dev 서버가 남의 것이면 확인 자체가 막힌다.
15
+ *
16
+ * 그래서 어댑터 규약만 아는 자리에 둔다. 어댑터는 `testConnection` · `discoverSites` · `fetchMaster` ·
17
+ * `openLiveFeed` 를 갖는 객체이고, 그 넷을 순서대로 부르는 일에 커넥터별 지식이 필요하지 않다.
18
+ *
19
+ * ── 무엇을 하지 않는가 ──────────────────────────────────────────────────────
20
+ * · **판단하지 않는다.** 「합격」을 매기지 않고 센 것을 그대로 낸다 — 무엇이 충분한지는 커넥터마다
21
+ * 다르고, 그 판단은 체크리스트 문서가 한다.
22
+ * · **저장하지 않는다.** 트윈을 만들지도, 커서를 남기지도 않는다. 원본을 읽기만 한다.
23
+ * · **라이브는 부르지 않는 것이 기본이다.** 첫 주기는 전량 읽기이므로 실 운영 원본에 부담이 된다
24
+ * (포천은 로트 99,095 · 투입 290,688 · 재고 141,358건이다). 부를 때는 호출부가 명시한다.
25
+ */
26
+
27
+ /** 마스터 한 벌에서 센 것 — 커넥터와 무관한 자리 이름으로만 센다. */
28
+ export interface MasterCounts {
29
+ locations: number
30
+ areas: number
31
+ equipment: number
32
+ materialDefinitions: number
33
+ operations: number
34
+ /** 생산 선언이 없으면 `undefined` — 0 과 구별한다(선언 자체가 없는 것과 비어 있는 것은 다르다). */
35
+ materials?: number
36
+ materialsWithHome?: number
37
+ routes?: number
38
+ recipes?: number
39
+ recipesWithRoute?: number
40
+ locationTypes?: number
41
+ resourceTypes?: number
42
+ }
43
+
44
+ export interface ProbeStep {
45
+ key: ReferenceStep | string
46
+ detail?: string
47
+ /** 그 단계가 끝난 시각까지 걸린 밀리초 — 어느 단계가 느린지 보이게 한다. */
48
+ elapsedMs: number
49
+ }
50
+
51
+ export interface ProbeReport {
52
+ adapter: string
53
+ /** 어댑터가 선언하고 **구현까지 있는** 능력. 선언만 있는 것은 여기 나오지 않는다. */
54
+ capabilities: string[]
55
+ connection?: { ok: boolean; readiness?: string; notes?: string[]; error?: string }
56
+ sites?: { siteId: string; name?: string }[]
57
+ /** 사이트마다 한 벌 — 마스터는 사이트 단위다. */
58
+ masters: {
59
+ siteId: string
60
+ steps: ProbeStep[]
61
+ counts?: MasterCounts
62
+ warnings: string[]
63
+ space?: unknown
64
+ identity?: unknown
65
+ /** 표본 — 눈으로 확인할 몇 건. 전체를 싣지 않는다(원본이 수천 건이다). */
66
+ samples?: { recipes?: unknown[]; routes?: unknown[]; locations?: unknown[] }
67
+ elapsedMs: number
68
+ error?: string
69
+ }[]
70
+ /** 라이브를 부른 경우에만 — 한 주기가 낸 기록을 종류별로 센다. */
71
+ live?: { records: number; byShape: Record<string, number>; samples: unknown[]; elapsedMs: number; error?: string }
72
+ /** 선언한 단계 중 **한 번도 보고되지 않은** 것. 선언해 두고 보내지 않으면 화면의 진행이 멈춘 것처럼 보인다. */
73
+ silentSteps?: string[]
74
+ /** 선언하지 않은 이름으로 보고한 단계. 표준 낱말 밖의 값은 화면이 번역할 수 없다. */
75
+ unknownSteps?: string[]
76
+ }
77
+
78
+ /** 기록의 모양을 이름 붙인다 — 커널 낱말로만 가린다(커넥터를 알 필요가 없다). */
79
+ function shapeOf(r: any): string {
80
+ if (r?.inputQuantityList || r?.outputQuantityList) return 'transformation'
81
+ if (r?.taskId) return 'task'
82
+ if (r?.orderId) return 'order'
83
+ if (r?.quantityList || r?.epcList) return 'observation'
84
+ return 'other'
85
+ }
86
+
87
+ function countMaster(ref: ReferenceMaster): MasterCounts {
88
+ const m: any = ref
89
+ const d = m.productionSpec?.definition
90
+ return {
91
+ locations: m.locations?.length ?? 0,
92
+ areas: (m.locations ?? []).filter((l: any) => l.role === 'area').length,
93
+ equipment: m.equipment?.length ?? 0,
94
+ materialDefinitions: m.materialDefinitions?.length ?? 0,
95
+ operations: m.operations?.length ?? 0,
96
+ ...(d
97
+ ? {
98
+ materials: d.materials?.length ?? 0,
99
+ materialsWithHome: (d.materials ?? []).filter((x: any) => x.locationType).length,
100
+ routes: d.routes?.length ?? 0,
101
+ recipes: d.recipes?.length ?? 0,
102
+ recipesWithRoute: (d.recipes ?? []).filter((x: any) => x.route).length,
103
+ locationTypes: d.locationTypes?.length ?? 0,
104
+ resourceTypes: d.resourceTypes?.length ?? 0
105
+ }
106
+ : {})
107
+ }
108
+ }
109
+
110
+ const messageOf = (w: unknown): string => (typeof w === 'string' ? w : `${(w as any)?.code}: ${(w as any)?.message}`)
111
+
112
+ /**
113
+ * 사람이 읽는 줄로 자른다 — **자랐다는 사실을 함께 적는다.**
114
+ *
115
+ * 경고에 값 목록이 붙는다(어느 자리가 걸렀는지). 88건이면 UUID 88개가 한 줄에 쏟아져 보고를 읽을 수
116
+ * 없게 된다. 그래서 사람용 출력에서만 자르고, 전체는 보고 객체(`--json`)에 그대로 남긴다.
117
+ */
118
+ const brief = (text: string, max = 300): string =>
119
+ text.length <= max ? text : `${text.slice(0, max)}… (${text.length}자 중 앞 ${max}자 — 전체는 보고 객체에 있다)`
120
+
121
+ /**
122
+ * 어댑터 한 벌을 실 원본에 붙여 본다.
123
+ *
124
+ * 실패를 던지지 않는다 — **어디까지 갔는지가 보고의 내용**이다. 연결이 안 되면 그 사실이, 마스터가
125
+ * 실패하면 어느 단계에서 실패했는지가 보고에 남는다(예외로 끝나면 그 정보가 사라진다).
126
+ */
127
+ export async function probeReference(opts: {
128
+ adapter: ReferenceAdapter
129
+ config: any
130
+ /** 특정 사이트만 볼 때. 없으면 `discoverSites` 가 낸 전부. */
131
+ siteId?: string
132
+ /**
133
+ * 라이브 한 주기를 부를지. **기본은 부르지 않는다** — 첫 주기가 전량 읽기다.
134
+ * `waitMs` 만큼 기다린 뒤 멈춘다(주기가 끝나지 않았으면 그때까지 받은 것만 센다).
135
+ */
136
+ live?: { waitMs: number; sampleCount?: number }
137
+ /** 표본 건수 — 기본 3건. */
138
+ sampleCount?: number
139
+ }): Promise<ProbeReport> {
140
+ const { adapter, config } = opts
141
+ const sampleCount = opts.sampleCount ?? 3
142
+ const report: ProbeReport = { adapter: adapter.type, capabilities: capabilitiesOf(adapter), masters: [] }
143
+
144
+ if (adapter.testConnection) {
145
+ try {
146
+ report.connection = (await adapter.testConnection(config)) as any
147
+ } catch (e: any) {
148
+ report.connection = { ok: false, error: e?.message ?? String(e) }
149
+ }
150
+ /* 연결이 안 되면 더 가지 않는다 — 그 뒤 실패는 모두 같은 원인의 반복이다. */
151
+ if (report.connection && report.connection.ok === false) return report
152
+ }
153
+
154
+ let sites: SiteDescriptor[] = []
155
+ try {
156
+ sites = adapter.discoverSites ? await adapter.discoverSites(config) : []
157
+ } catch (e: any) {
158
+ report.sites = []
159
+ report.masters.push({ siteId: '(사이트를 찾지 못했다)', steps: [], warnings: [], elapsedMs: 0, error: e?.message ?? String(e) })
160
+ return report
161
+ }
162
+ if (opts.siteId) sites = sites.filter(s => s.siteId === opts.siteId)
163
+ report.sites = sites.map(s => ({ siteId: s.siteId, name: s.name }))
164
+
165
+ const declared = new Set<string>(adapter.masterSteps ?? [])
166
+ const reported = new Set<string>()
167
+ const unknown = new Set<string>()
168
+
169
+ for (const site of sites) {
170
+ const t0 = Date.now()
171
+ const steps: ProbeStep[] = []
172
+ try {
173
+ const ref = await adapter.fetchMaster!(config, site, s => {
174
+ reported.add(String(s.key))
175
+ if (!(REFERENCE_STEPS as readonly string[]).includes(String(s.key))) unknown.add(String(s.key))
176
+ steps.push({ key: s.key, detail: s.detail, elapsedMs: Date.now() - t0 })
177
+ })
178
+ const m: any = ref
179
+ const d = m.productionSpec?.definition
180
+ report.masters.push({
181
+ siteId: site.siteId,
182
+ steps,
183
+ counts: countMaster(ref),
184
+ warnings: (m.warnings ?? []).map(messageOf),
185
+ space: m.space,
186
+ identity: m.identity,
187
+ samples: {
188
+ recipes: (d?.recipes ?? []).slice(0, sampleCount),
189
+ routes: (d?.routes ?? []).slice(0, sampleCount),
190
+ locations: (m.locations ?? []).slice(0, sampleCount)
191
+ },
192
+ elapsedMs: Date.now() - t0
193
+ })
194
+ } catch (e: any) {
195
+ /* 어느 단계까지 갔는지를 남긴다 — 그것이 다음에 볼 자리다. */
196
+ report.masters.push({ siteId: site.siteId, steps, warnings: [], elapsedMs: Date.now() - t0, error: e?.message ?? String(e) })
197
+ }
198
+ }
199
+
200
+ const silent = [...declared].filter(s => !reported.has(s))
201
+ if (silent.length) report.silentSteps = silent
202
+ if (unknown.size) report.unknownSteps = [...unknown]
203
+
204
+ if (opts.live && adapter.openLiveFeed && sites.length) {
205
+ const t0 = Date.now()
206
+ const got: unknown[] = []
207
+ try {
208
+ const stop = adapter.openLiveFeed(config, sites[0], rs => got.push(...rs))
209
+ await new Promise(r => setTimeout(r, opts.live.waitMs))
210
+ stop()
211
+ const byShape: Record<string, number> = {}
212
+ for (const r of got) byShape[shapeOf(r)] = (byShape[shapeOf(r)] ?? 0) + 1
213
+ report.live = {
214
+ records: got.length,
215
+ byShape,
216
+ samples: got.slice(0, opts.live.sampleCount ?? sampleCount),
217
+ elapsedMs: Date.now() - t0
218
+ }
219
+ } catch (e: any) {
220
+ report.live = { records: got.length, byShape: {}, samples: [], elapsedMs: Date.now() - t0, error: e?.message ?? String(e) }
221
+ }
222
+ }
223
+
224
+ return report
225
+ }
226
+
227
+ /** 보고를 사람이 읽는 줄로 — 화면이 아니라 터미널·로그가 읽는다. */
228
+ export function formatProbeReport(r: ProbeReport): string {
229
+ const out: string[] = []
230
+ out.push(`커넥터 ${r.adapter} · 능력 ${r.capabilities.length ? r.capabilities.join(',') : '(없음)'}`)
231
+ if (r.connection) {
232
+ out.push(`연결 ${r.connection.ok ? 'ok' : '실패'}${r.connection.readiness ? ` · 읽기준비 ${r.connection.readiness}` : ''}`)
233
+ for (const n of r.connection.notes ?? []) out.push(` · ${brief(n)}`)
234
+ if (r.connection.error) out.push(` · ${r.connection.error}`)
235
+ }
236
+ for (const m of r.masters) {
237
+ out.push(`사이트 ${m.siteId} — ${(m.elapsedMs / 1000).toFixed(1)}초`)
238
+ for (const s of m.steps) out.push(` [${(s.elapsedMs / 1000).toFixed(1)}s] ${s.key}${s.detail ? ` — ${s.detail}` : ''}`)
239
+ if (m.error) out.push(` 실패: ${m.error}`)
240
+ if (m.counts) {
241
+ const c = m.counts
242
+ out.push(
243
+ ` 로케이션 ${c.locations}(존 ${c.areas}) · 설비 ${c.equipment} · 자재정의 ${c.materialDefinitions} · 공정 ${c.operations}`
244
+ )
245
+ if (c.materials !== undefined) {
246
+ out.push(
247
+ ` 선언자재 ${c.materials}(보관 로케이션 있는 것 ${c.materialsWithHome}) · 경로 ${c.routes} · 레시피 ${c.recipes}(경로 붙은 것 ${c.recipesWithRoute})`
248
+ )
249
+ } else {
250
+ out.push(' 생산 선언 없음 — 레시피가 하나도 성립하지 않았다')
251
+ }
252
+ }
253
+ for (const w of m.warnings) out.push(` 경고 ${brief(w)}`)
254
+ }
255
+ if (r.silentSteps?.length) out.push(`선언했으나 보고되지 않은 단계: ${r.silentSteps.join(', ')}`)
256
+ if (r.unknownSteps?.length) out.push(`표준 낱말 밖의 단계 이름: ${r.unknownSteps.join(', ')}`)
257
+ if (r.live) {
258
+ out.push(
259
+ `라이브 한 주기 — 기록 ${r.live.records}건 (${Object.entries(r.live.byShape).map(([k, v]) => `${k} ${v}`).join(' · ') || '없음'}) · ${(r.live.elapsedMs / 1000).toFixed(1)}초`
260
+ )
261
+ if (r.live.error) out.push(` 실패: ${r.live.error}`)
262
+ }
263
+ return out.join('\n')
264
+ }
@@ -0,0 +1,73 @@
1
+ /*
2
+ * 유입 진행 구독 — **트윈을 만드는 동안 무엇을 하고 있는지 화면이 안다.**
3
+ *
4
+ * 마스터 읽기는 원본에 여러 질의를 보내는 긴 작업이고, 그 사이 화면은 아무것도 알 수 없었다. 단계를
5
+ * 아는 것은 어댑터뿐이므로 어댑터가 **표준 낱말**로 말하고(`REFERENCE_STEPS`) 호스트가 자기 두 단계를
6
+ * 더해 이 채널로 흘린다.
7
+ *
8
+ * 낱말이 표준이므로 화면은 **단계마다 번역 하나**만 두면 모든 커넥터를 보여 준다.
9
+ */
10
+ import { Arg, Field, Int, ObjectType, Resolver, Root, Subscription } from 'type-graphql'
11
+ import { pubsub } from '@things-factory/shell'
12
+ import { filter, pipe } from 'graphql-yoga'
13
+
14
+ import { REFERENCE_STEPS } from './reference-progress.js'
15
+
16
+ @ObjectType({ description: 'One step of progress while a reference is being imported into twins.' })
17
+ export class TwinReferenceProgressMessage {
18
+ @Field({ description: 'Which reference (source) this belongs to.' })
19
+ source: string
20
+
21
+ @Field({ description: 'Which site is being built.' })
22
+ siteId: string
23
+
24
+ @Field({ description: `Standard step name — one of: ${REFERENCE_STEPS.join(' | ')}.` })
25
+ step: string
26
+
27
+ @Field(type => Int, { description: 'Steps finished for this site.' })
28
+ done: number
29
+
30
+ @Field(type => Int, {
31
+ nullable: true,
32
+ description: 'Total steps for this site. Absent when the adapter did not declare its steps — the UI then shows no percentage rather than inventing one.'
33
+ })
34
+ total?: number
35
+
36
+ @Field(type => Int, { description: 'Index of the site being built (0-based).' })
37
+ siteIndex: number
38
+
39
+ @Field(type => Int, { description: 'How many sites are being built.' })
40
+ siteCount: number
41
+
42
+ @Field({ nullable: true, description: 'A fact within the step, such as how many rows were read.' })
43
+ detail?: string
44
+
45
+ @Field({ nullable: true, description: 'True when this step failed. The UI keeps the progress panel and marks this step.' })
46
+ failed?: boolean
47
+
48
+ @Field({ nullable: true, description: 'Why it failed. Present only on failure.' })
49
+ error?: string
50
+ }
51
+
52
+ @Resolver()
53
+ export class TwinReferenceProgressSubscription {
54
+ @Subscription(returns => TwinReferenceProgressMessage, {
55
+ subscribe: ({ args, context }) => {
56
+ const { source } = args
57
+ const domainId = (context as any)?.state?.domain?.id
58
+ /* 테넌트 격리 — 도메인이 없으면 빈 구독이다(다른 테넌트의 진행을 흘리지 않는다). */
59
+ if (!domainId) return pipe(pubsub.subscribe('twin-reference-progress'), filter(() => false))
60
+ return pipe(
61
+ pubsub.subscribe('twin-reference-progress'),
62
+ filter((p: { twinReferenceProgress: TwinReferenceProgressMessage }) => p.twinReferenceProgress.source === source)
63
+ )
64
+ },
65
+ description: 'Progress while importing a reference — standard step names, so one translation per step covers every connector.'
66
+ })
67
+ twinReferenceProgress(
68
+ @Root() payload: { twinReferenceProgress: TwinReferenceProgressMessage },
69
+ @Arg('source') source: string
70
+ ): TwinReferenceProgressMessage {
71
+ return payload.twinReferenceProgress
72
+ }
73
+ }
@@ -0,0 +1,95 @@
1
+ /*
2
+ * 유입 진행 단계 — **모든 커넥터가 같은 낱말로 말한다.**
3
+ *
4
+ * ── 왜 표준화하나 (2026-08-21) ──────────────────────────────────────────────
5
+ * 마스터 읽기는 원본에 여러 질의를 보내는 긴 작업이고, 그 사이 화면은 아무것도 알 수 없었다. 커넥터마다
6
+ * 자기 낱말로 말하게 두면 화면이 커넥터 수만큼 문구를 알아야 하고, 새 커넥터가 생기면 화면에 낯선 글자가
7
+ * 나온다.
8
+ *
9
+ * 그래서 낱말을 고정한다. 그리고 그 낱말은 **트윈의 자리 이름**이다 — 커넥터가 지금 채우는 자리가 곧
10
+ * 지금 하는 일이다(`design/integration/connector-checklist.md` 의 마스터 축과 같은 이름).
11
+ *
12
+ * 그러면 셋이 함께 얻어진다.
13
+ * · 화면은 **단계마다 하나의 번역**만 두면 모든 커넥터를 보여 준다.
14
+ * · 사람은 원본이 달라도 **같은 말로** 진행을 읽는다.
15
+ * · 실패했을 때 「어느 자리를 읽다 실패했다」가 곧 「트윈의 무엇이 비었다」가 된다.
16
+ *
17
+ * ── 퍼센트를 만들려면 분모가 있어야 한다 ────────────────────────────────────
18
+ * 어댑터가 **자기가 보낼 단계를 미리 선언**한다(`ReferenceAdapter.masterSteps`). 그것이 분모다. 선언하지
19
+ * 않으면 화면은 퍼센트를 보이지 않고 단계 이름만 보인다 — **모르는 진행률을 지어내지 않는다.**
20
+ */
21
+
22
+ /**
23
+ * 표준 단계. 순서는 대개 이 차례이지만 **강제하지 않는다** — 원본에 따라 없는 자리가 있고, 그때는 그
24
+ * 단계를 선언하지도 보내지도 않는다.
25
+ */
26
+ export const REFERENCE_STEPS = [
27
+ /** 원본 스키마 확인 — 무엇을 물을 수 있는지 먼저 본다(판이 다를 수 있다). */
28
+ 'schema',
29
+ /** 공간·좌표 — 자리가 놓일 좌표계. */
30
+ 'space',
31
+ /** 자리 — 물건이 놓이고 작업이 일어나는 곳. */
32
+ 'locations',
33
+ /** 설비 — 일하는 것. */
34
+ 'equipment',
35
+ /** 공정 — 무슨 일을 하는가. */
36
+ 'operations',
37
+ /** 자재 — 무엇이 흐르는가. */
38
+ 'materials',
39
+ /** 경로 — 공정의 순서. */
40
+ 'routes',
41
+ /** 레시피 — 투입과 산출. */
42
+ 'recipes',
43
+ /** 시험 명세 — 무엇을 어떻게 검사하는가. */
44
+ 'testSpecifications',
45
+ /** 인원. */
46
+ 'persons',
47
+ /** 물리자산. */
48
+ 'assets',
49
+ /** 트윈 생성 — 여기부터는 호스트가 하는 일이다(어댑터가 선언하지 않는다). */
50
+ 'provision',
51
+ /** 기동. */
52
+ 'start'
53
+ ] as const
54
+
55
+ export type ReferenceStep = (typeof REFERENCE_STEPS)[number]
56
+
57
+ /** 호스트가 마스터 읽기 뒤에 하는 단계 — 어댑터의 선언에 이만큼을 더해 분모를 만든다. */
58
+ export const HOST_STEPS: ReferenceStep[] = ['provision', 'start']
59
+
60
+ /**
61
+ * 한 단계의 진행 — 구독으로 흐르는 메시지의 몸통.
62
+ *
63
+ * `detail` 은 **그 단계 안에서 사람이 알아야 하는 것**이다(읽은 건수 등). 비어 있어도 된다 — 지어내지
64
+ * 않는다.
65
+ */
66
+ export interface ReferenceProgress {
67
+ /** 어느 사이트를 만드는 중인가. */
68
+ siteId: string
69
+ step: ReferenceStep
70
+ /** 이 사이트에서 끝낸 단계 수. */
71
+ done: number
72
+ /** 이 사이트의 전체 단계 수 — 어댑터가 선언하지 않으면 없다(그때 퍼센트를 만들지 않는다). */
73
+ total?: number
74
+ /** 여러 사이트를 한 번에 만들 때, 지금 몇 번째인가. */
75
+ siteIndex: number
76
+ siteCount: number
77
+ /** 그 단계 안의 사실 — 「자리 128건」처럼. */
78
+ detail?: string
79
+ /** 이 단계에서 실패했는가. 실패해도 화면이 그 자리를 그대로 보여 줄 수 있어야 한다. */
80
+ failed?: boolean
81
+ /** 실패 이유 — 실패했을 때만. */
82
+ error?: string
83
+ }
84
+
85
+ /**
86
+ * 전체 진행률 — **분모를 알 때만 낸다.**
87
+ *
88
+ * 사이트가 여럿이면 사이트 사이의 진행도 함께 센다: 앞선 사이트는 끝난 것으로 본다.
89
+ */
90
+ export function referencePercent(p: ReferenceProgress): number | undefined {
91
+ if (!p.total || p.total <= 0 || p.siteCount <= 0) return undefined
92
+ const perSite = 1 / p.siteCount
93
+ const within = Math.min(p.done, p.total) / p.total
94
+ return Math.round((p.siteIndex * perSite + within * perSite) * 100)
95
+ }