@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,153 @@
1
+ /*
2
+ * ★ 「원본이 말한 적 없는 것」을 세는 규율.
3
+ *
4
+ * ── 왜 이것이 필요한가 (2026-08-20 실측) ────────────────────────────────────
5
+ * 동기화 건강 기능을 만들 때 제가 전제를 이렇게 적었다: *「커넥터의 매핑이 한 글자 틀리면 트윈은
6
+ * 빈 공장이 된다」*. 사용자가 그 전제를 검증하게 해서 재 봤더니, 유입 문은 참조 오류를 **거부하지
7
+ * 않았다**:
8
+ *
9
+ * 모르는 로케이션 「오타-99」 → 통과
10
+ * 모르는 bizStep 「PUTAWAY_X17」 → 통과
11
+ * 모르는 계량기 「MMXU-오타-99」 → 통과
12
+ *
13
+ * 즉 그 기능이 잡겠다고 내세운 대표 사례가 `mapping-blocked` 로는 **뜰 수 없었다.** 거부되는 것은
14
+ * 형태 오류뿐이다(시각 형식·숫자 아닌 값·epc 없음).
15
+ *
16
+ * 거부하지 않는 것이 옳다 — 원천이 모델보다 먼저 새 로케이션을 말하는 것은 정상이고, 거부하면 그 선행
17
+ * 계측을 잃는다. 커널은 흡수하고 **표시한다**(`origin: 'observed'`). 빠져 있던 것은 그 표시를
18
+ * 「맞춰지고 있나」를 보는 사람에게 잇는 일이었다.
19
+ *
20
+ * ── 지키는 것 ───────────────────────────────────────────────────────────────
21
+ * ① 마스터가 말한 것과 관측으로 알게 된 것을 섞지 않는다
22
+ * ② 트윈이 안 돌면 `null` — 「없다」와 「세어 보지 못했다」는 다르다
23
+ * ③ 조용히 자르지 않는다 — 상한에서 밀려난 수를 함께 낸다
24
+ * ④ 덮지 못하는 축을 밝힌다 — 계량기는 커널이 표시를 달지 않아 이 화면이 못 찾아 준다
25
+ */
26
+ import { test } from 'node:test'
27
+ import assert from 'node:assert/strict'
28
+
29
+ import { GAP_ID_LIMIT, modelGapOf } from '../server/engine/model-gap.ts'
30
+
31
+ const loc = (id: string, origin: string) => ({ id, origin })
32
+
33
+ test('마스터가 말한 로케이션은 갭이 아니다 — 선언된 것을 문제로 세지 않는다', () => {
34
+ const g = modelGapOf({
35
+ locations: [loc('dock-recv', 'master'), loc('bin-1', 'master')],
36
+ equipment: [loc('fk-1', 'master')]
37
+ })
38
+
39
+ assert.equal(g!.total, 0)
40
+ assert.deepEqual(g!.locations.ids, [])
41
+ })
42
+
43
+ test('관측으로만 알게 된 것을 축마다 센다 — 원천이 우리가 모르는 id 를 보내고 있다', () => {
44
+ const g = modelGapOf({
45
+ locations: [loc('dock-recv', 'master'), loc('오타-99', 'observed'), loc('A-77', 'observed')],
46
+ equipment: [loc('fk-1', 'master'), loc('없는-지게차', 'observed')],
47
+ persons: [loc('unknown-op', 'observed')]
48
+ })
49
+
50
+ assert.equal(g!.locations.count, 2)
51
+ assert.equal(g!.locations.unknown, 0, '표시가 다 있으면 모름은 0')
52
+ assert.deepEqual(g!.locations.ids, ['오타-99', 'A-77'], 'id 를 보여 준다 — 오타는 눈으로 찾는다')
53
+ assert.equal(g!.equipment.count, 1)
54
+ assert.equal(g!.persons.count, 1)
55
+ assert.equal(g!.total, 4, '세 축의 합')
56
+ })
57
+
58
+ test('트윈이 안 돌면 「없음」이다 — 0 이 아니다', () => {
59
+ /* 비교할 상대가 없다. 0 으로 답하면 「원천이 우리가 아는 것만 보낸다」는 없는 사실이 생긴다. */
60
+ assert.equal(modelGapOf(null), null)
61
+ assert.equal(modelGapOf(undefined), null)
62
+ })
63
+
64
+ test('돌지만 관측 갭이 없으면 0 이다 — 이건 「없음」과 다르다', () => {
65
+ const g = modelGapOf({ locations: [] })
66
+
67
+ assert.notEqual(g, null, '세어 봤다')
68
+ assert.equal(g!.total, 0, '세어 보니 없었다')
69
+ })
70
+
71
+ test('id 를 조용히 자르지 않는다 — 밀려난 수를 함께 낸다', () => {
72
+ const many = Array.from({ length: GAP_ID_LIMIT + 7 }, (_, i) => loc(`ghost-${i}`, 'observed'))
73
+ const g = modelGapOf({ locations: many })
74
+
75
+ assert.equal(g!.locations.count, GAP_ID_LIMIT + 7, '전부 센다')
76
+ assert.equal(g!.locations.ids.length, GAP_ID_LIMIT, '보이는 것은 상한까지')
77
+ assert.equal(g!.locations.more, 7, '밀려난 수를 말한다')
78
+ })
79
+
80
+ test('커널이 담을 줄 모른 사건을 함께 낸다 — 참조 문제와 다른 사실이다', () => {
81
+ const at = Date.parse('2026-08-20T01:02:03.000Z')
82
+ const g = modelGapOf({
83
+ unhandled: [
84
+ { eventType: 'epcis.TransformationEvent', count: 12, lastAtMs: at },
85
+ { eventType: 'aggregation-mixed-classes', count: 3 }
86
+ ]
87
+ })
88
+
89
+ assert.equal(g!.unhandled.length, 2)
90
+ assert.equal(g!.unhandled[0].lastAt, '2026-08-20T01:02:03.000Z', '언제까지 났는지 — 멈춘 종류는 고쳐진 것이다')
91
+ assert.equal(g!.unhandled[1].lastAt, null, '시각이 없으면 지어내지 않는다')
92
+ })
93
+
94
+ test('모르는 설비의 에너지 보고는 「없음」과 0 을 가른다 — 경로가 `energy` 안이다', () => {
95
+ /*
96
+ * 이 값을 **루트에서 읽고 있었다**(2026-08-20에 잡음). 커널은 `EnergyState` 안에 담으므로 루트에서
97
+ * 읽으면 영원히 `null` 이고, 오류 없이 「에너지 트윈이 아니다」로 읽힌다. 나만 읽는 필드를 잘못된
98
+ * 경로에서 읽으면 이렇게 조용히 사라진다.
99
+ *
100
+ * 그리고 커널은 0 이면 그 칸을 만들지 않는다. 그래서 `energy` 자체가 있는지로 「세었고 0」과
101
+ * 「에너지 트윈이 아님」을 가른다.
102
+ */
103
+ assert.equal(modelGapOf({})!.unknownEquipmentReports, null, '에너지 트윈이 아니면 이 수가 없다')
104
+ assert.equal(modelGapOf({ energy: { points: [] } })!.unknownEquipmentReports, 0, '에너지 트윈이면 세었고 0 이다')
105
+ assert.equal(modelGapOf({ energy: { unknownEquipment: 41 } })!.unknownEquipmentReports, 41)
106
+ assert.equal(
107
+ modelGapOf({ unknownEquipment: 41 } as any)!.unknownEquipmentReports,
108
+ null,
109
+ '루트에 있는 값을 집지 않는다 — 그 경로는 커널의 것이 아니다'
110
+ )
111
+ })
112
+
113
+ test('계량기도 덮는다 — 이 축이 가장 무겁다', () => {
114
+ /*
115
+ * 모르는 `meterId` 는 조용히 새 계량기가 되고 그 kW 가 그 현장의 수요·피크·요금 판정에 더해진다.
116
+ * 로케이션 오타는 스키매틱이 비는 것으로 드러나지만, 이건 **그럴듯한 숫자**로 나온다.
117
+ * 커널 0.7.39 가 `MeterPointState.origin` 을 내면서 덮을 수 있게 됐다.
118
+ */
119
+ const g = modelGapOf({
120
+ energy: { points: [loc('main-incoming', 'master'), loc('MMXU-오타-99', 'observed')] }
121
+ })
122
+
123
+ assert.equal(g!.meters.count, 1)
124
+ assert.deepEqual(g!.meters.ids, ['MMXU-오타-99'])
125
+ assert.equal(g!.total, 1, '계량기도 합에 든다')
126
+ assert.deepEqual(g!.notCovered, [], '덮지 못하는 축이 없다')
127
+ })
128
+
129
+ test('표시가 없는 항목은 「선언됨」이 아니라 「모름」으로 센다', () => {
130
+ /*
131
+ * 계량기의 `origin` 은 늦게 생긴 칸이라, 옛 스냅샷에서 이어받은 지점에는 없다(`undefined`).
132
+ * 마스터로 세면 오타를 놓치고, 관측으로 세면 정상을 문제로 만든다. 둘 다 틀리다.
133
+ */
134
+ const g = modelGapOf({
135
+ energy: { points: [{ id: 'carried-over' }, loc('main', 'master'), loc('ghost', 'observed')] }
136
+ })
137
+
138
+ assert.equal(g!.meters.unknown, 1, '표시 없는 것은 모름')
139
+ assert.equal(g!.meters.count, 1, '관측 갭에 섞이지 않는다')
140
+ assert.ok(!g!.meters.ids.includes('carried-over'), '모르는 것을 오타로 지목하지 않는다')
141
+ })
142
+
143
+ test('`origin` 은 「지금 모델에 있는가」의 답이다 — 채택되면 갭에서 빠진다', () => {
144
+ /*
145
+ * 커널이 표본마다 다시 맞춘다. 한 번 찍고 두면 나중에 선언된 계량기가 영원히 `observed` 로 남는다.
146
+ * 그래서 채택 뒤 스냅샷에서는 같은 id 가 `master` 로 오고, 이 도출은 그것을 그대로 따른다.
147
+ */
148
+ const before = modelGapOf({ energy: { points: [loc('new-meter', 'observed')] } })
149
+ const after = modelGapOf({ energy: { points: [loc('new-meter', 'master')] } })
150
+
151
+ assert.equal(before!.meters.count, 1)
152
+ assert.equal(after!.meters.count, 0, '모델에 들어오면 갭이 아니다')
153
+ })
@@ -18,6 +18,10 @@ const TICK = 1000
18
18
  function runSimCollect() {
19
19
  const m = {
20
20
  source: 'oee', system: 'wms' as const, siteName: 'O',
21
+ /* 이 픽스처가 식별자를 만드는 이름공간 — **픽스처마다 다르게** 둔다(같은 접두 아래에서 서로 다른
22
+ 시험의 번호가 부딪치면 표준의 유일성 요구를 깬다, CBV 2.0 §8.2.4). 커널은 선언 없이 팔레트·문서를
23
+ 만들지 않는다(0.7.45 에서 날조 프리픽스를 지웠다). */
24
+ identity: { namespaces: ['https://twin.test/oee-accumulator'] },
21
25
  space: { width: 90, depth: 40, unit: 'm' },
22
26
  locations: [
23
27
  { id: 'a', type: 'warehouse', role: 'area' as const },
@@ -93,8 +93,35 @@ test('이력 목록은 선언 대조에도 쓰인다 — 오타를 그때부터
93
93
  */
94
94
  test('시뮬 기동이 모드를 적는다 — 「돌고 있나」를 묻는 쪽이 답을 받아야 한다', () => {
95
95
  const src = readFileSync(fileURLToPath(new URL('../server/engine/twin-engine.ts', import.meta.url)), 'utf-8')
96
- assert.match(src, /id, domainId, mode: 'sim', runtime/, '시뮬 런타임도 모드를 든다')
96
+ assert.match(src, /mode: 'sim',\s*\n\s*runtime/, '시뮬 런타임도 모드를 든다')
97
97
  /* 답이 갈리는 자리 — 도는 트윈이면 「재기동 때 받는다」로 말해야 한다. */
98
98
  const mut = readFileSync(fileURLToPath(new URL('../server/service/twin-model/twin-model-mutation.ts', import.meta.url)), 'utf-8')
99
99
  assert.match(mut, /running \? 'on-restart' : 'stored'/)
100
100
  })
101
+
102
+ /*
103
+ * ── 공정 모수도 목록에 실린다 (2026-08-19) ──────────────────────────────────
104
+ * 수율·셋업은 시간과 다른 표에 담긴다(`localParams`). 목록이 그것을 함께 들지 않으면 선언해 둔 값이
105
+ * 화면에서 사라지고, 그러면 철회할 자리도 없어진다. 원천이 말한 모수도 함께 들어 견줄 수 있게 한다.
106
+ */
107
+ test('선언한 모수와 원천 모수를 함께 든다 — 무엇을 고쳤나가 보인다', () => {
108
+ const model = {
109
+ operations: [{ key: 'weld', label: 'Welding', intent: 'process', parameters: [{ id: 'yield', value: '0.7' }] }],
110
+ localParams: { weld: { yield: '0.92', setupDuration: 'PT30S' } }
111
+ }
112
+ const rows = operationBasis(model, kernelReporting([{ kind: 'weld', duration: 'declared' }]))
113
+ assert.deepEqual(rows, [
114
+ {
115
+ kind: 'weld',
116
+ label: 'Welding',
117
+ referenceParams: { yield: '0.7' },
118
+ basis: 'declared',
119
+ localParams: { yield: '0.92', setupDuration: 'PT30S' }
120
+ }
121
+ ])
122
+ })
123
+
124
+ test('모수만 선언한 공정도 목록에 남는다 — 커널이 아직 돌리지 않았어도', () => {
125
+ const rows = operationBasis({ localParams: { inspect: { yield: '0.99' } } }, undefined)
126
+ assert.deepEqual(rows, [{ kind: 'inspect', localParams: { yield: '0.99' } }])
127
+ })
@@ -0,0 +1,108 @@
1
+ /*
2
+ * **라이브 문이 운영 사실을 안다** — 미러가 시뮬보다 가난하지 않게 (ADR-0029 어휘 넓히기, 2026-08-19).
3
+ *
4
+ * ── 무엇이 막혀 있었나 ──────────────────────────────────────────────────────
5
+ * 이 문(`ingestCanonicalRecords`)은 어휘를 셋만 알았다: EPCIS 품목 · 에너지 계량 · 설비 에너지. 그리고
6
+ * **모르는 것은 EPCIS 로 떨어졌다** — 작업 사실을 넣으면 `epc` 가 없어 거부됐고, 원본은 「이 작업이
7
+ * 끝났다」를 말할 길이 없었다. 시뮬 커널만 그것을 알고 미러는 영원히 몰랐다.
8
+ *
9
+ * ── 이 시험이 지키는 것 ─────────────────────────────────────────────────────
10
+ * ① 네 어휘가 **한 문**에서 갈린다(판정은 커널의 것이고 여기서 다시 짐작하지 않는다).
11
+ * ② 운영 사실이 자기 봉투로 나온다 — EPCIS 로 감싸지지 않는다.
12
+ * ③ 섞여 와도 갈린다(실 피드는 한 배치에 여러 어휘가 섞인다).
13
+ * ④ 거부는 이유와 함께 남는다 — 피드 결선이 그 이유를 사람에게 말한다.
14
+ */
15
+ import { test } from 'node:test'
16
+ import assert from 'node:assert/strict'
17
+ import { readFileSync } from 'node:fs'
18
+ import { fileURLToPath } from 'node:url'
19
+
20
+ import { ingestCanonicalRecords } from '../server/engine/canonical-ingest.ts'
21
+
22
+ const AT = '2026-08-19T09:00:00.000Z'
23
+
24
+ test('운영 사실이 자기 봉투로 나온다 — EPCIS 로 감싸지지 않는다', () => {
25
+ const { accepted, rejected } = ingestCanonicalRecords(
26
+ [{ taskId: 'tk-1', kind: 'putaway', status: 'completed' } as any],
27
+ 'domain-1',
28
+ AT
29
+ )
30
+ assert.deepEqual(rejected, [])
31
+ assert.equal(accepted.length, 1)
32
+ assert.equal(accepted[0].eventType, 'task.status')
33
+ assert.equal(accepted[0].eventTime, AT, '시각을 주지 않으면 이 문이 정한 기본 시각을 쓴다')
34
+ assert.equal(accepted[0].tenantId, 'domain-1')
35
+ })
36
+
37
+ test('네 어휘가 한 배치에서 갈린다 — 실 피드는 섞여 온다', () => {
38
+ const { accepted, rejected } = ingestCanonicalRecords(
39
+ [
40
+ { epc: 'urn:epc:id:sgtin:9521321.012345.1', action: 'ADD', bizStep: 'urn:epcglobal:cbv:bizstep:receiving', readPoint: 'dock-1' },
41
+ { meterId: 'm-1', kW: 120 },
42
+ { equipmentId: 'pv-1', generatedKW: 40 },
43
+ { moverId: 'fk-1', kind: 'forklift', status: 'busy' }, // vocabulary-guard: allow 저널 와이어 필드
44
+ { orderId: 'or-1', kind: 'outbound', status: 'picking', requested: 10, fulfilled: 3 }
45
+ ] as any,
46
+ 'domain-1',
47
+ AT
48
+ )
49
+ assert.deepEqual(rejected, [], '어느 어휘도 남의 길로 떨어지지 않는다')
50
+ assert.deepEqual(accepted.map(e => e.eventType).sort(), [
51
+ 'energy.equipment', 'energy.measured', 'epcis.ObjectEvent', 'equipment.status', 'order.status'
52
+ ].sort())
53
+ })
54
+
55
+ test('거부는 이유와 함께 남는다 — 유효한 것은 함께 통과한다', () => {
56
+ const { accepted, rejected } = ingestCanonicalRecords(
57
+ [
58
+ { taskId: 'tk-1', kind: 'pick', status: 'completed' },
59
+ { taskId: 'tk-2', kind: 'pick', status: 'FINISHED' } // 커널이 접을 수 없는 낱말
60
+ ] as any,
61
+ 'domain-1',
62
+ AT
63
+ )
64
+ assert.equal(accepted.length, 1)
65
+ assert.equal(rejected.length, 1)
66
+ assert.ok(rejected[0].errors.join(' ').length > 0)
67
+ })
68
+
69
+ test('피드 결선이 거부를 조용히 버리지 않는다 — 이유를 사람에게 말한다', () => {
70
+ const src = readFileSync(fileURLToPath(new URL('../server/service/reference/reference-live.ts', import.meta.url)), 'utf-8')
71
+ assert.match(src, /record\(s\) rejected/, '거부 수와 이유를 남긴다')
72
+ })
73
+
74
+ /*
75
+ * ── 담김도 이 문을 지난다 (2026-08-19) ──────────────────────────────────────
76
+ * 실 앱에서 한 바퀴 돌리자 「옮기지 못했다」 로그가 `epcis.AggregationEvent` 를 짚었다 — 관측 리듀서는 그
77
+ * 사실로 자식 물품에 `parent` 를 붙이는데(3D 가 「이 상자가 어느 팔레트에 실렸나」를 묻는 자리) 문에 그
78
+ * 어휘가 없었다.
79
+ */
80
+ test('담김 레코드가 자기 봉투로 나온다 — 개체 관측과 한 배치에 섞여 와도 갈린다', () => {
81
+ const { accepted, rejected } = ingestCanonicalRecords(
82
+ [
83
+ { epc: 'urn:epc:id:sgtin:9521321.012345.1', action: 'ADD', bizStep: 'urn:epcglobal:cbv:bizstep:receiving', readPoint: 'dock-1' },
84
+ { parentID: 'urn:epc:id:sscc:0614141.0000000001', childEPCs: ['urn:epc:id:sgtin:9521321.012345.1'], action: 'ADD', bizStep: 'urn:epcglobal:cbv:bizstep:packing', readPoint: 'packing' }
85
+ ] as any,
86
+ 'domain-1',
87
+ AT
88
+ )
89
+ assert.deepEqual(rejected, [])
90
+ assert.deepEqual(accepted.map(e => e.eventType), ['epcis.ObjectEvent', 'epcis.AggregationEvent'])
91
+ assert.equal((accepted[1].data as any).parentID, 'urn:epc:id:sscc:0614141.0000000001')
92
+ })
93
+
94
+ test('자리를 구조째 실으면 거부한다 — 예전에는 "[object Object]" 로 굳어 저널에 쌓였다', () => {
95
+ const { accepted, rejected } = ingestCanonicalRecords(
96
+ [{ epc: 'urn:epc:id:sgtin:9521321.012345.1', action: 'ADD', bizStep: 'urn:epcglobal:cbv:bizstep:receiving', readPoint: { id: 'dock-1' } }] as any,
97
+ 'domain-1',
98
+ AT
99
+ )
100
+ assert.equal(accepted.length, 0)
101
+ assert.match(rejected[0].errors.join(' '), /readPoint/)
102
+ })
103
+
104
+ test('이 문이 어휘가 넷임을 스스로 말한다 — 다음 사람이 세 어휘로 읽지 않게', () => {
105
+ const src = readFileSync(fileURLToPath(new URL('../server/engine/canonical-ingest.ts', import.meta.url)), 'utf-8')
106
+ assert.match(src, /어휘가 넷이다/)
107
+ assert.match(src, /isOperationalRecord/, '판정은 커널의 것을 부른다(여기서 다시 짐작하지 않는다)')
108
+ })
@@ -102,8 +102,8 @@ before(async () => {
102
102
  q = new TwinJournalQuery()
103
103
  ctx = { state: { domain: { id: domainId } } }
104
104
 
105
- await TwinEngine.ingestMaster(domainId, mesMaster())
106
- await TwinEngine.ingestMaster(domainId, wmsMaster())
105
+ await TwinEngine.ingestMaster(domainId, mesMaster(), 'reset')
106
+ await TwinEngine.ingestMaster(domainId, wmsMaster(), 'reset')
107
107
  })
108
108
 
109
109
  after(async () => {
@@ -122,7 +122,7 @@ test('실행 중이 아니면 그 사실을 알린다 — "없는 것" 과 구
122
122
  })
123
123
 
124
124
  test('돌고 있으면 구간의 양을 종류로 갈라 낸다 — 표준 등식이 성립한다', async () => {
125
- await TwinEngine.startFromRegistry(domainId, MES, 'sim-experiment')
125
+ await TwinEngine.startFromRegistry(domainId, MES, 'reset')
126
126
  const rows = await ask(MES)
127
127
  assert.equal(rows[0].reason, undefined)
128
128
 
@@ -138,7 +138,7 @@ test('돌고 있으면 구간의 양을 종류로 갈라 낸다 — 표준 등
138
138
  })
139
139
 
140
140
  test('공정을 선언하지 않은 트윈은 판정 대상이 아니다 — 0 으로 채우면 없는 병목이 생긴다', async () => {
141
- await TwinEngine.startFromRegistry(domainId, WMS, 'sim-experiment')
141
+ await TwinEngine.startFromRegistry(domainId, WMS, 'reset')
142
142
  const rows = await q.twinOperationsCapability(WMS, undefined, 24, WINDOW.fromTime, WINDOW.toTime, WINDOW.fromTime, ctx)
143
143
  assert.equal(rows[0].reason, 'no-operations')
144
144
  assert.equal(rows[0].report, undefined)
@@ -81,7 +81,7 @@ before(async () => {
81
81
  domain: { id: domainId },
82
82
  instanceId: INSTANCE,
83
83
  kind: 'mes',
84
- realityMode: 'sim-experiment',
84
+ restartPolicy: 'reset',
85
85
  purpose: 'operational',
86
86
  status: 'stopped',
87
87
  model: {}
@@ -184,7 +184,7 @@ before(async () => {
184
184
  q = new TwinModelItemQuery()
185
185
  ctx = { state: { domain: { id: domainId } } }
186
186
 
187
- await TwinEngine.ingestMaster(domainId, master())
187
+ await TwinEngine.ingestMaster(domainId, master(), 'reset')
188
188
  })
189
189
 
190
190
  after(async () => {
@@ -94,3 +94,31 @@ test('공정 시간이 등록돼 있고, 그 값을 실어 보내는 자리를
94
94
  assert.match(layer, /isoDurationOf/, '커널이 읽는 표기로 바꾼다')
95
95
  assert.match(hostSrc('engine/twin-engine.ts'), /declareDurations\(declared\)/, '기동·구조전환 때 커널에 싣는다')
96
96
  })
97
+
98
+ /*
99
+ * 모수도 같은 배선을 지난다 (2026-08-19).
100
+ *
101
+ * 이 시험이 소스를 보는 이유: 모수를 읽는 순간은 **작업이 완료될 때**라서 실 앱에서 관측하려면 라인이
102
+ * 한 바퀴 돌아야 한다(복원된 25분 작업이 쌓인 트윈에서는 그 관측이 오래 걸린다 — 실제로 그랬다).
103
+ * 값이 커널까지 가는 **경로**는 여기서 지키고, 커널이 그 값을 쓰는지는 커널 시험이 값으로 지킨다.
104
+ */
105
+ test('수율·셋업이 커널 창구까지 배선돼 있다 — 등록만 하고 나르지 않으면 화면이 거짓을 말한다', () => {
106
+ const layer = hostSrc('engine/local-declarations.ts')
107
+ assert.match(layer, /OPERATION_PROPERTY\.yield/, '선언 겹이 수율을 안다')
108
+ assert.match(layer, /OPERATION_PROPERTY\.setupDuration/, '선언 겹이 셋업을 안다')
109
+ /* 퍼센트 → 비율 환산은 **한 곳**이어야 한다(두 곳이면 100 이 두 번 걸린다). */
110
+ const divisions = layer.match(/pct \/ 100/g) ?? []
111
+ assert.equal(divisions.length, 1, `퍼센트 환산은 한 자리에서만 (발견 ${divisions.length})`)
112
+ assert.match(hostSrc('engine/twin-engine.ts'), /declareParameters\(params\)/, '기동·구조전환 때 커널에 싣는다')
113
+ /* 커널이 그 창구를 갖지 않은 버전이면 **말한다** — 조용히 상수로 돌지 않게. */
114
+ assert.match(hostSrc('engine/twin-engine.ts'), /declareParameters missing/, '버전 어긋남을 경고로 드러낸다')
115
+ })
116
+
117
+ test('공정 모수가 등록부에 있고 읽는 자리를 가리킨다', () => {
118
+ for (const id of ['operation.yield', 'operation.setupDuration']) {
119
+ const e = PROPERTY_EFFECTS.find(x => x.id === id)!
120
+ assert.ok(e, `${id} 가 등록부에 있어야 한다`)
121
+ assert.deepEqual(e.axes, ['operations'])
122
+ assert.match(e.readBy, /declareParameters/, '어느 창구로 가는지 말한다')
123
+ }
124
+ })
@@ -0,0 +1,145 @@
1
+ /*
2
+ * ★ 「원본에 닿지 못한다」와 「원본이 조용하다」는 **다른 사실**이다 — 상태가 그것을 가려야 한다.
3
+ *
4
+ * ── 무엇이 조용했나 (2026-08-23 실측) ───────────────────────────────────────
5
+ * 실 원본이 끊겼다(`jejoup.hatiolab.com:4000` 접속 시간 초과, HTTP 000 / 8초). 그런데 트윈의
6
+ * **조회 가능한 상태 어디에도 그 사실이 없었다.**
7
+ *
8
+ * recordIngest `onRecords` 가 불릴 때만 도달한다 — 읽기 실패는 그 문에 오지 않는다
9
+ * TwinReference.lastError 마스터 동기·접속 시험에만 적힌다 — 라이브 피드는 적지 않았다
10
+ * 계약 `openLiveFeed` 에 「읽지 못했다」를 말할 통로가 없었다
11
+ *
12
+ * 어댑터는 **로그로는 말하고 있었다**(재시도 경고). 그러나 로그는 사람이 볼 때만 값이 있다 — 화면이
13
+ * 말하려면 상태에 있어야 한다. 실증 데모 중에 원본이 끊기면 사용자가 원인을 찾을 수 없다.
14
+ *
15
+ * ── 이 시험이 지키는 것 ─────────────────────────────────────────────────────
16
+ * ① 실패가 상태에 남는다 ② 「몇 번」과 「언제부터」를 함께 든다(한 번의 흔들림과 20분째 단절은 다르다)
17
+ * ③ **빈 읽기는 성공이다** — 조용한 원본을 끊긴 원본으로 만들지 않는다
18
+ * ④ 조회 뷰로 나간다 — 상태에만 있고 안 나가면 화면이 못 본다
19
+ */
20
+ import { test } from 'node:test'
21
+ import assert from 'node:assert/strict'
22
+ import { readFileSync } from 'node:fs'
23
+ import { join } from 'node:path'
24
+ import {
25
+ clearReadFailure,
26
+ ingestHealth,
27
+ newIngestLedger,
28
+ recordIngest,
29
+ recordReadFailure
30
+ } from '../server/engine/ingest-health.ts'
31
+
32
+ const T0 = Date.parse('2026-08-23T06:00:00.000Z')
33
+
34
+ test('실패가 상태에 남는다 — 사유와 흐름까지', () => {
35
+ const l = newIngestLedger()
36
+ assert.equal(l.readFailure, undefined, '처음에는 없다(없음이 「닿는다」는 뜻이다)')
37
+ recordReadFailure(l, 'connect ETIMEDOUT', T0, 'lot')
38
+ assert.equal(l.readFailure?.consecutive, 1)
39
+ assert.equal(l.readFailure?.reason, 'connect ETIMEDOUT')
40
+ assert.equal(l.readFailure?.stream, 'lot', '어느 표가 막혔나가 원인 찾기의 절반이다')
41
+ })
42
+
43
+ test('한 번의 흔들림과 20분째 단절을 가른다 — 시작 시각을 지킨다', () => {
44
+ /*
45
+ * 매번 새로 적으면 화면이 늘 「방금 실패했다」만 보게 되고, 계속된 단절을 대수롭지 않게 말한다.
46
+ */
47
+ const l = newIngestLedger()
48
+ recordReadFailure(l, 'ETIMEDOUT', T0)
49
+ recordReadFailure(l, 'ETIMEDOUT', T0 + 600_000)
50
+ recordReadFailure(l, 'ETIMEDOUT', T0 + 1_200_000)
51
+ assert.equal(l.readFailure?.consecutive, 3)
52
+ assert.equal(l.readFailure?.sinceMs, T0, '처음 실패한 시각을 지킨다')
53
+ assert.equal(l.readFailure?.lastAtMs, T0 + 1_200_000, '마지막 실패는 갱신된다')
54
+ })
55
+
56
+ test('빈 읽기는 성공이다 — 조용한 원본을 끊긴 원본으로 만들지 않는다', () => {
57
+ /*
58
+ * ★ 이 단정이 고치려던 것의 **반대 방향으로 틀리는 것**을 막는다. 원본이 「새 것이 없다」고 답한 것은
59
+ * 닿았다는 뜻이다.
60
+ */
61
+ const l = newIngestLedger()
62
+ recordReadFailure(l, 'ETIMEDOUT', T0)
63
+ clearReadFailure(l)
64
+ assert.equal(l.readFailure, undefined, '읽었으면 단절 기록이 사라진다')
65
+ /* 그리고 그 뒤 다시 실패하면 **새 단절**이다 — 옛 시작 시각을 물려받지 않는다. */
66
+ recordReadFailure(l, 'ETIMEDOUT', T0 + 3_600_000)
67
+ assert.equal(l.readFailure?.consecutive, 1, '연속이 끊겼으므로 1 부터')
68
+ assert.equal(l.readFailure?.sinceMs, T0 + 3_600_000, '새 단절의 시작이다')
69
+ })
70
+
71
+ test('조회 뷰로 나간다 — 상태에만 있으면 화면이 못 본다', () => {
72
+ const l = newIngestLedger()
73
+ recordReadFailure(l, 'connect ETIMEDOUT', T0, 'inventory')
74
+ const v = ingestHealth(l, 'attached', undefined, T0 + 60_000)
75
+ assert.ok(v.readFailure, '뷰에 실려야 한다')
76
+ assert.equal(v.readFailure?.consecutive, 1)
77
+ assert.equal(v.readFailure?.since, new Date(T0).toISOString())
78
+ assert.equal(v.readFailure?.reason, 'connect ETIMEDOUT')
79
+ assert.equal(v.readFailure?.stream, 'inventory')
80
+ })
81
+
82
+ test('닿고 있으면 그 필드를 만들지 않는다 — 빈 값이 「닿는다」로 읽히지 않게', () => {
83
+ const l = newIngestLedger()
84
+ recordIngest(l, 3, [], T0)
85
+ const v = ingestHealth(l, 'attached', undefined, T0 + 1000)
86
+ assert.equal(v.readFailure, undefined)
87
+ /* 가드가 스스로를 증명한다 — 이 장부가 실제로 무엇을 받았다. */
88
+ assert.equal(v.total.offered, 3, '빈 비교로 통과하지 않는다')
89
+ })
90
+
91
+ test('「유입 없음」과 「닿지 못함」은 다른 판정이다', () => {
92
+ /*
93
+ * 유입 없음은 원본이 조용한 것일 수 있고, 닿지 못함은 우리가 묻지도 못한 것이다. 두 상태가 같은
94
+ * 값으로 뭉개지면 화면이 두 문장을 다르게 말할 수 없다.
95
+ */
96
+ const quiet = newIngestLedger()
97
+ const dark = newIngestLedger()
98
+ recordReadFailure(dark, 'ETIMEDOUT', T0)
99
+
100
+ const vq = ingestHealth(quiet, 'attached', undefined, T0)
101
+ const vd = ingestHealth(dark, 'attached', undefined, T0)
102
+ assert.equal(vq.lastAt, null, '조용한 쪽도 받은 것이 없다')
103
+ assert.equal(vd.lastAt, null, '끊긴 쪽도 받은 것이 없다 — 그 값만으로는 갈리지 않는다')
104
+ assert.notEqual(Boolean(vq.readFailure), Boolean(vd.readFailure), '이 축이 그 둘을 가른다')
105
+ })
106
+
107
+ /*
108
+ * ── 읽기 실패가 **피드를 끄면 안 된다** (2026-08-23, 되돌린 자리) ────────────
109
+ *
110
+ * 처음 구현에서 읽기 실패가 레퍼런스 행의 `status` 를 `'error'` 로 적었다. 그것이 **피드를 영구히
111
+ * 껐다**: 부팅 재부착은 `status: 'connected'` 인 레퍼런스만 되살리므로, 원본이 **잠깐** 끊긴 것이
112
+ * 다음 재기동에서 그 트윈을 아예 굶겼다. 실측으로 그렇게 났다 — 그 판의 로그에 `[twin-live]` 줄이
113
+ * 한 줄도 없었다.
114
+ *
115
+ * 두 축을 한 칸에 섞은 것이 원인이다.
116
+ *
117
+ * status **연결 설정이 성립하나** — 접속 시험·마스터 동기가 정한다. 재부착의 **자격**이다
118
+ * 읽기 실패 **지금 닿나** — 오갈 수 있는 사실이고 유입 장부가 그 축을 갖는다
119
+ *
120
+ * 「사실을 말하게」 하려던 것이 「사실을 못 듣게」 만들었다. 그 방향의 실패는 조용하다 — 화면은
121
+ * 「원본에 닿지 못한다」를 정직하게 말하면서, 정작 다시 닿을 기회를 잃는다.
122
+ */
123
+ test('읽기 실패가 레퍼런스의 status 를 건드리지 않는다 — 그것이 재부착의 자격이다', () => {
124
+ const src = readFileSync(join(import.meta.dirname, '..', 'server', 'service', 'reference', 'reference-live.ts'), 'utf8')
125
+ const code = src.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/(^|[^:])\/\/.*$/gm, '$1')
126
+
127
+ const at = code.indexOf('onReadFailure:')
128
+ assert.ok(at > 0, 'onReadFailure 배선이 있어야 한다')
129
+ const body = code.slice(at, at + 600)
130
+
131
+ assert.doesNotMatch(body, /status:/, '읽기 실패는 status 를 적지 않는다 — 적으면 다음 부팅이 그 피드를 되살리지 않는다')
132
+ /* 대신 이유는 남긴다 — 사람이 목록에서 볼 수 있게. */
133
+ assert.match(body, /lastError/, '이유는 레퍼런스 행에 남긴다')
134
+ /* 그리고 「지금 닿나」는 장부가 답한다. */
135
+ assert.match(body, /recordIngestReadFailure/, '유입 장부가 그 축을 든다')
136
+ })
137
+
138
+ test('재부착의 자격은 connected 뿐이다 — 그래서 그 값을 함부로 바꾸면 안 된다', () => {
139
+ /*
140
+ * 이 단정이 위 가드의 **이유**를 못박는다. 재부착 조건이 넓어지면 위 규율의 근거가 약해지므로,
141
+ * 둘이 함께 움직여야 한다.
142
+ */
143
+ const src = readFileSync(join(import.meta.dirname, '..', 'server', 'service', 'reference', 'reference-live.ts'), 'utf8')
144
+ assert.match(src, /find\(\{ where: \{ status: 'connected' \}/, '부팅 재부착은 connected 만 되살린다')
145
+ })
@@ -0,0 +1,70 @@
1
+ /*
2
+ * ★ 커넥터의 **근거 수준** — 목으로 확인한 것을 확인으로 부르지 않는다.
3
+ *
4
+ * ── 왜 이 축이 생겼나 (2026-08-23, 토대 릴리즈 준비) ────────────────────────
5
+ * 등록된 커넥터 열넷 중 실 시스템에 붙어 본 것은 **하나**다(첫 실 연동). 나머지는 목에 대고 만들었다.
6
+ * 그런데 화면의 커넥터 목록에 그 구분이 없어서 **목으로 확인한 것이 확인으로 보인다.**
7
+ *
8
+ * 이 저장소는 같은 모양을 능력 축에서 이미 거절했다 — 「선언만 있는 능력은 사용자에게 거짓말이 된다」
9
+ * (§`capabilitiesOf`). 그 규율을 근거에도 세운다.
10
+ *
11
+ * ── 무엇을 검증할 수 있고 무엇을 못 하나 ────────────────────────────────────
12
+ * 능력은 구현이 있는지 보아 선언을 검증할 수 있다. **근거는 볼 수 없다** — 사람이 아는 사실이다.
13
+ * 그래서 검증할 수 있는 하나만 검증한다: **실 시스템을 주장하면 무엇에 대고인지 말해야 한다.**
14
+ * 가리키는 대상이 없는 주장은 주장이 아니고, 그런 주장은 낡아도 아무도 알 수 없다.
15
+ */
16
+ import { test } from 'node:test'
17
+ import assert from 'node:assert/strict'
18
+ import { groundingOf, type ReferenceAdapter } from '../server/service/reference/reference-adapter.ts'
19
+
20
+ const adapter = (grounding?: any) => ({ type: 't', grounding } as unknown as ReferenceAdapter)
21
+
22
+ test('선언이 없으면 「모른다」다 — 「목이다」가 아니다', () => {
23
+ /*
24
+ * 둘을 같게 두면 아직 선언하지 않은 새 커넥터가 목으로 낙인찍힌다 — 그것도 없는 사실이다.
25
+ * 화면은 `unknown` 을 「아직 밝히지 않았다」로 말해야 한다.
26
+ */
27
+ assert.deepEqual(groundingOf(adapter()), { level: 'unknown' })
28
+ assert.deepEqual(groundingOf(undefined), { level: 'unknown' })
29
+ })
30
+
31
+ test('실 시스템을 주장하면 무엇에 대고인지 말해야 한다 — 아니면 주장이 내려간다', () => {
32
+ /* ★ 이 가드의 핵심. 대상 없는 주장은 낡아도 아무도 모른다. */
33
+ for (const level of ['instance', 'product'] as const) {
34
+ const g = groundingOf(adapter({ level }))
35
+ assert.equal(g.level, 'unknown', `${level} 주장이 대상 없이 통과하면 안 된다`)
36
+ assert.match(g.downgraded ?? '', /does not say what it was verified against/)
37
+ assert.match(g.downgraded ?? '', new RegExp(level), '무엇을 주장했는지 함께 말한다')
38
+ }
39
+ /* 여백만 있는 것도 비어 있는 것이다 — 형식을 채워 통과하지 못하게. */
40
+ assert.equal(groundingOf(adapter({ level: 'instance', verifiedAgainst: ' ' })).level, 'unknown')
41
+ })
42
+
43
+ test('대상을 말하면 그 주장이 선다', () => {
44
+ const g = groundingOf(adapter({ level: 'instance', verifiedAgainst: 'chef v1 · jejoup / pocheon' }))
45
+ assert.equal(g.level, 'instance')
46
+ assert.equal(g.verifiedAgainst, 'chef v1 · jejoup / pocheon')
47
+ assert.equal(g.downgraded, undefined)
48
+ })
49
+
50
+ test('목과 가상은 대상 없이도 선다 — 그 자체가 사실이다', () => {
51
+ /*
52
+ * `mock` 은 「실 시스템에 붙어 본 적이 없다」는 사실이고 그것을 말하는 데 대상이 필요하지 않다.
53
+ * `synthetic` 은 한계가 아니라 **성질**이다 — 가상 원본은 스스로 만들어 낸다.
54
+ */
55
+ assert.equal(groundingOf(adapter({ level: 'mock' })).level, 'mock')
56
+ assert.equal(groundingOf(adapter({ level: 'synthetic' })).level, 'synthetic')
57
+ })
58
+
59
+ test('instance 와 product 를 가른다 — 그 구분이 이 어휘의 요점이다', () => {
60
+ /*
61
+ * 첫 실 연동에서 그 배포만의 성질이 넷 드러났는데(관계 리솔버가 다른 테넌트의 행을 답한다 · 모르는
62
+ * 필터를 조용히 무시한다 · 지워진 품목의 BOM 이 남아 있다 · 사실상 무한을 뜻하는 표식) **그것이
63
+ * 제품의 성질인지 그 인스턴스의 성질인지 아무도 모른다.** 두 번째 배포가 붙는 날 갈린다.
64
+ *
65
+ * 그래서 하나로 확인한 것을 `product` 라 부르면 안 되고, 어휘가 그 구분을 담는다.
66
+ */
67
+ const one = groundingOf(adapter({ level: 'instance', verifiedAgainst: '한 배포' }))
68
+ const many = groundingOf(adapter({ level: 'product', verifiedAgainst: '세 배포' }))
69
+ assert.notEqual(one.level, many.level, '두 수준이 같은 값으로 뭉개지지 않는다')
70
+ })