@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,52 @@
1
+ /*
2
+ * ★ 소스 시험이 형제 모듈을 부를 수 있게 한다 — **`operato-twin` 의 같은 규칙을 이 패키지에도 둔다** — **`.js` 로 적힌 상대 경로를 `.ts` 로 되짚는다.**
3
+ *
4
+ * ── 무엇이 서고 있었나 (2026-08-22) ─────────────────────────────────────────
5
+ * 이 패키지의 `npm test` 는 `node --test test/*.test.ts` 로 **소스를 그대로 실행**한다. 그런데 서버
6
+ * 소스는 tsc 로 빌드되는 코드이므로 상대 import 를 `'./x.js'` 로 적는다(그것이 빌드 산출물에서 맞는
7
+ * 경로다). 타입만 지우는 실행은 그 `.js` 를 `.ts` 로 바꿔 주지 않는다.
8
+ *
9
+ * 그래서 **소스 모듈이 형제 모듈을 값으로 참조하는 순간** 그 모듈 그래프를 부르는 시험이 전부 선다.
10
+ * 실측으로 하루에 네 종류가 이 벽에 걸렸다: `identity/namespace.js`(정체성 이름공간) ·
11
+ * `engine/log.js`(로그 문) · `isa95-coverage.js`(적합성 판정) · `journal-count.js`(저널 세기). 늘 같은 오류다.
12
+ *
13
+ * Cannot find module '…/server/identity/namespace.js' imported from '…/templates/wms-unit-load.ts'
14
+ *
15
+ * ── 왜 이 방식인가 ──────────────────────────────────────────────────────────
16
+ * 고르던 길이 둘 있었다.
17
+ *
18
+ * ① 그 시험들을 **빌드 산출물**(`dist-server`)로 돌린다 — 이 패키지에 전례가 있다. 그러나 시험마다
19
+ * 그 배선을 적어야 하고, 걸리는 시험이 늘 때마다 같은 일을 다시 한다. 그리고 산출물이 낡으면
20
+ * **고친 코드가 아닌 것이 돌면서 초록으로 보인다.**
21
+ * ② 상수를 쓰는 파일 안으로 옮긴다 — 값 참조가 사라지지만 **정본이 흩어진다.** 그것이 이 상수를 한
22
+ * 자리에 모은 이유와 정면으로 어긋난다.
23
+ *
24
+ * 둘 다 증상을 피하는 것이고, 원인은 **해석 규칙 하나**다. 그래서 규칙을 고친다: 상대 경로 `.js` 가
25
+ * 없고 같은 이름의 `.ts` 가 있으면 그것을 쓴다. 한 자리에 두므로 걸리는 시험이 늘어도 할 일이 없다.
26
+ *
27
+ * ── 무엇을 하지 않는가 ─────────────────────────────────────────────────────
28
+ * **패키지 이름(`@things-factory/…`)은 손대지 않는다.** 그것은 의존 패키지의 산출물을 가리키는
29
+ * 것이고 실제로 `.js` 가 있다. 되짚는 것은 **상대 경로**뿐이고, 그것도 `.ts` 가 실제로 있을 때만이다 —
30
+ * 없으면 원래 오류가 그대로 난다(없는 것을 지어내지 않는다).
31
+ */
32
+ import { registerHooks } from 'node:module'
33
+ import { existsSync } from 'node:fs'
34
+ import { fileURLToPath, pathToFileURL } from 'node:url'
35
+
36
+ registerHooks({
37
+ resolve(specifier, context, nextResolve) {
38
+ /* 상대 경로의 `.js` 만 본다 — 패키지 이름과 그 밖의 확장자는 그대로 둔다. */
39
+ if (!specifier.startsWith('.') || !specifier.endsWith('.js')) return nextResolve(specifier, context)
40
+
41
+ try {
42
+ return nextResolve(specifier, context)
43
+ } catch (err) {
44
+ /* 해석이 실패한 그때만 `.ts` 를 본다 — 성공하는 경로의 뜻을 바꾸지 않는다. */
45
+ const parent = context?.parentURL
46
+ if (!parent) throw err
47
+ const asTs = new URL(specifier.slice(0, -3) + '.ts', parent)
48
+ if (!existsSync(fileURLToPath(asTs))) throw err
49
+ return { url: pathToFileURL(fileURLToPath(asTs)).href, shortCircuit: true, format: 'module-typescript' }
50
+ }
51
+ }
52
+ })
@@ -0,0 +1,111 @@
1
+ /*
2
+ * **재기동 정책** — 개명(ADR-0029 §2)과 그 규율 둘.
3
+ *
4
+ * ── 무엇을 바꿨나 ───────────────────────────────────────────────────────────
5
+ * `realityMode`(`mirror`·`sim-world`·`sim-experiment`) → `restartPolicy`(`resync`·`resume`·`reset`).
6
+ * 「현실이 어디서 오나」는 ADR-0029 가 **원본의 종류**로 옮긴 물음이고, 코드가 이 축으로 실제로 가른 것은
7
+ * **재기동 거동 하나**였다 — 그것은 원본의 성질이 아니라 트윈과 자기 과거의 관계다. 그래서 축은 남고
8
+ * 이름이 바뀌었다.
9
+ *
10
+ * ── 지키는 규율 둘 (계획서 §6) ──────────────────────────────────────────────
11
+ * ① **모르는 값은 오류를 낸다.** 예전에는 미선언이 조용히 `sim-experiment`(=저널 초기화)로 떨어졌다 — 값이
12
+ * 하나 어긋나는 순간 이력을 지우는 길이다. 기본값을 없앴다.
13
+ * ② **옛 어휘를 번역하지 않는다.** 번역을 두면 옛 값이 살아남고 다시 저장된다. 저장된 값은 한 번
14
+ * 옮기고(데이터 이관), 코드는 새 어휘만 안다 — 다만 무엇으로 옮겨야 하는지는 오류 문장이 말한다.
15
+ */
16
+ import { test } from 'node:test'
17
+ import assert from 'node:assert/strict'
18
+ import { readdirSync, readFileSync, statSync } from 'node:fs'
19
+ import { fileURLToPath } from 'node:url'
20
+ import { join } from 'node:path'
21
+
22
+ import { readRestartPolicy, isRestartPolicy, RESTART_POLICIES } from '../server/engine/restart-policy.ts'
23
+
24
+ test('셋뿐이다 — 그리고 그 셋을 읽는다', () => {
25
+ assert.deepEqual([...RESTART_POLICIES], ['resync', 'resume', 'reset'])
26
+ for (const p of RESTART_POLICIES) assert.equal(readRestartPolicy(p, 'test'), p)
27
+ assert.equal(readRestartPolicy(' resume ', 'test'), 'resume', '공백은 다듬는다')
28
+ })
29
+
30
+ test('미선언은 오류를 낸다 — 기본값으로 메우면 저널이 지워지는 길이 열린다', () => {
31
+ for (const raw of [undefined, null, '', ' ', 42, {}]) {
32
+ assert.throws(() => readRestartPolicy(raw, 'twin "x"'), /twin "x": restart policy/)
33
+ }
34
+ /* 무엇을 해야 하는지 문장이 말한다 — 코드를 그대로 오류를 내지 않는다. */
35
+ try {
36
+ readRestartPolicy(undefined, 'twin "x"')
37
+ assert.fail('오류를 내어야 한다')
38
+ } catch (e: any) {
39
+ assert.match(e.message, /resync \| resume \| reset/)
40
+ assert.match(e.message, /no default/)
41
+ }
42
+ })
43
+
44
+ test('옛 어휘도 오류를 낸다 — 다만 무엇으로 옮길지 말한다(번역해 주지 않는다)', () => {
45
+ const cases: [string, string][] = [
46
+ ['mirror', 'resync'],
47
+ ['sim-world', 'resume'],
48
+ ['sim-experiment', 'reset']
49
+ ]
50
+ for (const [old, next] of cases) {
51
+ try {
52
+ readRestartPolicy(old, 'twin "x"')
53
+ assert.fail(`${old} 은 오류를 내어야 한다`)
54
+ } catch (e: any) {
55
+ assert.match(e.message, /old vocabulary/)
56
+ assert.match(e.message, new RegExp(`"${next}"`), '옮길 값을 말한다')
57
+ }
58
+ }
59
+ })
60
+
61
+ test('묻는 자리도 있다 — 화면·검사는 오류를 내지 않고 판별한다', () => {
62
+ assert.equal(isRestartPolicy('resume'), true)
63
+ assert.equal(isRestartPolicy('sim-world'), false)
64
+ assert.equal(isRestartPolicy(undefined), false)
65
+ })
66
+
67
+ /*
68
+ * ── 옛 어휘가 코드로 되돌아오지 않게 (계획서 §6 의 검사) ────────────────────
69
+ * ADR-0034 의 `local-declarations.test.ts` 가 `site*` 에 대해 하는 것과 같은 방식이다. 개명은 저장값·타입·
70
+ * 화면에 걸쳐 있어서, 한 곳만 되돌아와도 그 자리가 조용히 다르게 재기동한다.
71
+ */
72
+ const OLD_TOKENS = [/\brealityMode\b/, /\bRealityMode\b/, /'sim-world'/, /'sim-experiment'/, /DEFAULT_REALITY_MODE/]
73
+
74
+ function sources(root: string): { file: string; src: string }[] {
75
+ const base = fileURLToPath(new URL(root, import.meta.url))
76
+ const out: { file: string; src: string }[] = []
77
+ const walk = (dir: string) => {
78
+ for (const name of readdirSync(dir)) {
79
+ if (name === 'node_modules' || name.startsWith('dist')) continue
80
+ const path = join(dir, name)
81
+ if (statSync(path).isDirectory()) { walk(path); continue }
82
+ if (name.endsWith('.ts')) out.push({ file: path.slice(base.length + 1), src: readFileSync(path, 'utf-8') })
83
+ }
84
+ }
85
+ walk(base)
86
+ return out
87
+ }
88
+
89
+ test('옛 어휘가 코드에 없다 — 이력을 설명하는 주석만 예외다', () => {
90
+ const offenders: string[] = []
91
+ for (const { file, src } of [...sources('../server'), ...sources('../test')]) {
92
+ /* 이력 설명(「예전에는 …」)과 이관 안내(restart-policy.ts)는 남아야 한다 — 그것이 개명의 근거다. */
93
+ if (file.endsWith('restart-policy.ts') || file.endsWith('restart-policy.test.ts')) continue
94
+ for (const line of src.split('\n')) {
95
+ const isComment = /^\s*(\*|\/\/|\/\*)/.test(line)
96
+ if (isComment) continue
97
+ if (OLD_TOKENS.some(re => re.test(line))) offenders.push(`${file}: ${line.trim().slice(0, 80)}`)
98
+ }
99
+ }
100
+ assert.deepEqual(offenders, [], `옛 어휘가 코드로 돌아왔다:\n${offenders.join('\n')}`)
101
+ })
102
+
103
+ test('기본값 상수가 없다 — 미선언을 우리가 고르지 않는다', () => {
104
+ const engine = readFileSync(fileURLToPath(new URL('../server/engine/twin-engine.ts', import.meta.url)), 'utf-8')
105
+ const code = engine
106
+ .split('\n')
107
+ .filter(line => !/^\s*(\*|\/\/|\/\*)/.test(line))
108
+ .join('\n')
109
+ assert.equal(/DEFAULT_RESTART_POLICY/.test(code), false, '기본값 상수가 되살아나면 미선언이 다시 조용해진다')
110
+ assert.match(code, /readRestartPolicy\(/, '읽는 자리는 엄격한 리더를 지난다')
111
+ })
@@ -73,5 +73,11 @@ test('프로비저닝이 고른 현장을 origin 에 남기고, 다시 읽기가
73
73
  .join('\n')
74
74
  assert.match(code, /kind: 'template', templateId, params: effective, \.\.\.\(spaceId \? \{ spaceId \} : \{\}\)/, '템플릿 트윈이 현장을 기억한다')
75
75
  assert.match(code, /const keepSpace = origin\.spaceId \|\| inst\.spaceId/, '기억이 없으면 지금 있는 현장을 지킨다')
76
- assert.match(code, /ingestMaster\(domainId, master, keepSpace \? \{ spaceId: keepSpace \} : undefined/, '다시 읽기가 그 현장으로 넣는다')
76
+ /* 인자 사이에 재기동 정책이 끼었다(ADR-0029 §2) 현장을 넘기는 인자는 그대로여야 한다. */
77
+ assert.match(code, /keepSpace \? \{ spaceId: keepSpace \} : undefined/, '다시 읽기가 그 현장으로 넣는다')
78
+ assert.match(
79
+ code,
80
+ /readRestartPolicy\(inst\.restartPolicy, `resync of twin/,
81
+ '다시 읽기는 그 트윈의 정책을 그대로 지킨다(구조만 새로 그린다)'
82
+ )
77
83
  })
@@ -0,0 +1,93 @@
1
+ /*
2
+ * ★ 리비전은 **두 셈**이고, 둘이 같은 것을 세지 않는다 — 그 사실을 코드가 지키는지 본다.
3
+ *
4
+ * ── 무엇이 어긋나 있었나 (2026-08-23 실측) ──────────────────────────────────
5
+ * 포천 미러의 체크포인트가 뜻이 다른 두 수를 나란히 적었다.
6
+ *
7
+ * 바깥 revision 12,972 저널에 적힌 줄 번호
8
+ * state.revision 6,478 커널이 처리한 봉투 수
9
+ *
10
+ * 차이 6,494 는 그 트윈을 **다시 세운 시각**(09:11)에 저널이 이미 갖고 있던 줄 수와 정확히 같았다
11
+ * (줄 6494 = 08:23:59 · 줄 6495 = 09:11:13). 산수가 맞았으니 결함은 「수가 틀렸다」가 아니다.
12
+ *
13
+ * 결함은 **비대칭**이었다. 재기동 경로(`start` → `resumeRevision`)는 커널에 저널 머리를 알려 주는데
14
+ * 미러를 세우는 경로(`startLive`)는 알려 주지 않았다. 그래서 같은 저장물을 읽는 사람이 같은 이름의 두
15
+ * 수를 같은 축으로 견주게 되고, 실제로 그렇게 읽혔다(반나절이 그 오독에서 나갔다).
16
+ *
17
+ * ── 왜 알려 주어야 하나 ─────────────────────────────────────────────────────
18
+ * 커널의 계약이 그 이유를 적어 두었다(`resumeRevision`): 「저널이 비어 있지 않은데 0부터 다시 세면 같은
19
+ * 트윈에 같은 리비전이 둘 생기고, 재생 순서가 뒤섞이며, 시간여행이 엉뚱한 시점을 답한다 — 오류는 안 난다.」
20
+ * 조용히 깨지는 종류이므로 시험이 지킨다.
21
+ *
22
+ * ── 이 시험이 무엇을 재는가 ─────────────────────────────────────────────────
23
+ * DB 도 커널도 띄우지 않는다. 이 규율은 **어느 경로가 무엇을 부르나**에 있고 그것은 소스에서 확인된다.
24
+ */
25
+ import { test } from 'node:test'
26
+ import assert from 'node:assert/strict'
27
+ import { readFileSync } from 'node:fs'
28
+ import { join } from 'node:path'
29
+
30
+ const ENGINE = join(import.meta.dirname, '..', 'server', 'engine', 'twin-engine.ts')
31
+ const src = readFileSync(ENGINE, 'utf8')
32
+
33
+ /**
34
+ * 그 함수 본문만 — 다른 경로의 호출이 섞여 판정이 흐려지지 않게.
35
+ *
36
+ * 끝은 **다음 멤버 선언**으로 자른다. 글자 수로 자르면 주석이 늘어난 날 조용히 짧아져서, 있는 것을
37
+ * 없다고 말한다(거짓 빨강을 내는 가드는 없는 것보다 나쁘다).
38
+ */
39
+ function bodyOf(signature: string): string {
40
+ const at = src.indexOf(signature)
41
+ assert.ok(at > 0, `${signature} 이 있어야 한다`)
42
+ const rest = src.slice(at + signature.length)
43
+ const end = rest.search(/\n (?:static|private static|\/\*\*)/)
44
+ const body = end === -1 ? rest : rest.slice(0, end)
45
+ /* 잘라낸 본문이 비면 판정이 성립하지 않는다 — 그것을 통과로 읽지 않는다. */
46
+ assert.ok(body.length > 200, `${signature} 본문을 잘라내지 못했다(${body.length}자)`)
47
+ return body
48
+ }
49
+
50
+ test('미러를 세우는 경로도 커널에 저널 머리를 알려 준다', () => {
51
+ const body = bodyOf('static startLive(')
52
+ /* 저널 머리를 읽는다 — 이 조회는 예전부터 있었다(inst.revision 시드용). */
53
+ assert.match(body, /order: \{ revision: 'DESC' \}/, '저널 머리를 읽어야 한다')
54
+ /* 그리고 **커널에도** 알려 준다 — 이 한 줄이 없어서 두 수가 갈렸다. */
55
+ assert.match(body, /resumeRevision\(head\)/, '커널에도 그 번호를 알려 준다')
56
+ })
57
+
58
+ test('재기동 경로는 예전부터 알려 주고 있었다 — 두 경로가 같아졌는지 본다', () => {
59
+ /*
60
+ * 이 단정은 「고쳤다」가 아니라 **두 경로가 대칭인지**를 지킨다. 한쪽만 남으면 같은 결함이 다른
61
+ * 문으로 되살아난다.
62
+ */
63
+ const body = bodyOf('static start(')
64
+ assert.match(body, /resumeRevision\(resumeFrom\)/, '재기동 경로도 알려 준다')
65
+ })
66
+
67
+ test('저널이 비어 있으면 알려 주지 않는다 — 새 트윈은 0 부터 세는 것이 맞다', () => {
68
+ const body = bodyOf('static startLive(')
69
+ assert.match(body, /if \(!head \|\| typeof/, '머리가 0 이면 아무 일도 하지 않는다')
70
+ })
71
+
72
+ test('뒤로 되돌리려는 거절을 삼키지 않는다', () => {
73
+ /*
74
+ * 조회가 비동기라 그 사이에 이벤트가 들어와 있을 수 있다. 그때 커널은 뒤로 가기를 거절한다(겹치는
75
+ * 번호를 막는 그 판정이다). 그 거절을 조용히 버리면 「왜 두 수가 아직 다르지」를 답할 계기가 없다.
76
+ */
77
+ const body = bodyOf('static startLive(')
78
+ assert.match(body, /catch \(err[^)]*\)[\s\S]{0,200}twinWarn/, '거절을 로그에 남긴다')
79
+ assert.match(body, /is behind the kernel/, '무엇이 어긋났는지 말한다')
80
+ })
81
+
82
+ test('체크포인트는 두 수를 나란히 적는다 — 그 사실 자체는 옳다', () => {
83
+ /*
84
+ * 바깥은 저널 줄 번호(이어 쓸 자리), 안쪽은 커널이 접은 봉투 수다. **둘을 하나로 합치지 않는다** —
85
+ * 합치면 「어디까지 적었나」와 「어디까지 반영했나」를 구별할 수 없고, 그 구별이 미러의 정직성이다.
86
+ * 두 경로가 대칭이면 값이 같아지지만, **자리는 둘이어야 한다.**
87
+ */
88
+ const at = src.indexOf('static async persistSnapshot')
89
+ assert.ok(at > 0, 'persistSnapshot 이 있어야 한다')
90
+ const body = src.slice(at, at + 1200)
91
+ assert.match(body, /inst\.revision \?\? state\.revision/, '바깥은 저널 줄 번호를 먼저 쓴다')
92
+ assert.match(body, /\{ revision, state/, '두 수를 나란히 싣는다')
93
+ })
@@ -39,7 +39,7 @@ test('인스턴스 id 안의 구분자를 보존한다 — 첫 구분자에서
39
39
  assert.equal(instanceId, weird)
40
40
  })
41
41
 
42
- test('도메인 id 에 구분자가 있으면 던진다 — 조용히 모호한 키를 만들지 않는다', () => {
42
+ test('도메인 id 에 구분자가 있으면 오류를 낸다 — 조용히 모호한 키를 만들지 않는다', () => {
43
43
  /* 조용히 만들면 그게 곧 이 결함의 재발이다(어느 테넌트인지 알 수 없는 키). */
44
44
  assert.throws(() => runtimeKey(`a${RUNTIME_KEY_SEP}b`, 'x'), /ambiguous/)
45
45
  })
@@ -49,7 +49,7 @@ test('빈 값은 키가 될 수 없다 — 테넌트 없는 런타임은 없다'
49
49
  assert.throws(() => runtimeKey(D1, ''), /instanceId is required/)
50
50
  })
51
51
 
52
- test('키가 아닌 것을 되돌리려 하면 던진다', () => {
52
+ test('키가 아닌 것을 되돌리려 하면 오류를 낸다', () => {
53
53
  for (const bad of ['', 'no-separator', `${RUNTIME_KEY_SEP}leading`, `trailing${RUNTIME_KEY_SEP}`]) {
54
54
  assert.throws(() => parseRuntimeKey(bad), /is not a runtime key/, `입력: "${bad}"`)
55
55
  }
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * 스케일 테스트 트윈 벤치 — 미국 공장(WMS+MES+YMS) 형태를 대규모로 스케일업, 5종 동기 엔티티 전부 부하.
3
3
  * 동기 엔티티 = locations·items(재고)·equipment·tasks·orders (노드/무버뿐이 아님). area 는 계층(건물>층>존).
4
- * 측정(가정 아닌 실측): masterToTwin(구조 생성)·apply(혼합 이벤트 처리량)·snapshot(전체 투영)·buildEntityDeltas(방송, 't1')·heap.
4
+ * 측정(가정 아닌 실측): masterToTwin(구조 생성)·apply(혼합 이벤트 처리량)·snapshot(전체 투영)·buildEntityDeltas(브로드캐스팅, 't1')·heap.
5
5
  * 순수 커널+호스트 매핑(shell·DB 무관) → node:test. area 는 공간층(projector State 밖)이라 처리량엔 무영향 — 구조 생성 비용에만.
6
6
  */
7
7
  import { test } from 'node:test'
@@ -87,7 +87,7 @@ for (const [name, tier] of Object.entries(TIERS)) {
87
87
  const tSnap = (performance.now() - t) / iters
88
88
  console.log(` [${name}] snapshot: ${tSnap.toFixed(2)}ms/call (items ${s.items.length.toLocaleString()}·locations ${s.locations.length.toLocaleString()}·equipment ${s.equipment.length}·tasks ${s.tasks.length}·orders ${s.orders.length}) → 최대 ${Math.round(1000 / tSnap).toLocaleString()} snap/s`)
89
89
 
90
- // 4) 방송 payload(buildEntityDeltas)
90
+ // 4) 브로드캐스팅 payload(buildEntityDeltas)
91
91
  t = performance.now()
92
92
  for (let k = 0; k < iters; k++) buildEntityDeltas(s, 't1')
93
93
  const tDelta = (performance.now() - t) / iters
@@ -0,0 +1,104 @@
1
+ /*
2
+ * **허락과 일어남 사이를 같은 행에 닫는다** — ADR-0029 §1 결정 C (2026-08-19).
3
+ *
4
+ * ── 무엇이 열려 있었나 ──────────────────────────────────────────────────────
5
+ * 명령이 원본으로 가면 감사는 「허락했다」까지만 증명한다: 어댑터가 조용히 실패하면 원장에는 승인만 남는다.
6
+ * 그리고 구동 문(`controlTwinSource`)은 감사를 **아예 남기지 않았다** — 자극을 바꾸고 배속을 올리는 것은
7
+ * 그 트윈의 사실을 바꾸는 행위인데, 나중에 「무엇이 이 공장을 흔들었나」에 답할 수 없었다.
8
+ *
9
+ * ── 세 값을 구별한다 ────────────────────────────────────────────────────────
10
+ * · 칸이 **없음** — 원본에 묻지 않았다(커널이 받은 명령).
11
+ * · `unknown` — 물었는데 답이 없다. **성공으로 적지 않는다.**
12
+ * · `applied`·`refused` — 원본이 답했다(둘 다 사실이다).
13
+ *
14
+ * 셋을 뭉개면 둘 중 하나가 된다: 일어난 일을 없던 일로 만들거나, 없던 일을 일어난 일로 만든다.
15
+ */
16
+ import { test } from 'node:test'
17
+ import assert from 'node:assert/strict'
18
+ import { readFileSync } from 'node:fs'
19
+ import { fileURLToPath } from 'node:url'
20
+
21
+ import { buildCommandAudit, sourceOutcomeOf } from '../server/service/twin-audit/command-audit.ts'
22
+
23
+ const at = new Date('2026-08-19T05:00:00.000Z')
24
+
25
+ test('묻지 않았으면 칸을 만들지 않는다 — 커널이 받은 명령이 그렇다', () => {
26
+ const row = buildCommandAudit({ command: { type: 'order.hold', origin: 'user' }, ack: { accepted: true }, at })
27
+ assert.equal('sourceOutcome' in row, false, '없음은 「모른다」가 아니라 「묻지 않았다」다')
28
+ assert.equal('sourceCode' in row, false)
29
+ })
30
+
31
+ test('물었는데 답이 없으면 unknown — 성공으로 적지 않는다', () => {
32
+ const row = buildCommandAudit({
33
+ command: { type: 'source.stimulus', origin: 'user' },
34
+ ack: { accepted: true },
35
+ source: { answered: false },
36
+ at
37
+ })
38
+ assert.equal(row.sourceOutcome, 'unknown')
39
+ })
40
+
41
+ test('답이 있으면 그대로 적는다 — 거절도 사실이다', () => {
42
+ const applied = buildCommandAudit({ command: {}, ack: { accepted: true }, source: { answered: true, ok: true }, at })
43
+ assert.equal(applied.sourceOutcome, 'applied')
44
+ const refused = buildCommandAudit({
45
+ command: {},
46
+ ack: { accepted: true },
47
+ source: { answered: true, ok: false, code: 'not-declared' },
48
+ at
49
+ })
50
+ assert.equal(refused.sourceOutcome, 'refused')
51
+ assert.equal(refused.sourceCode, 'not-declared', '원본이 준 사유를 그대로 적는다')
52
+ })
53
+
54
+ test('사유가 없으면 사유 칸도 없다 — 지어내지 않는다', () => {
55
+ const row = buildCommandAudit({ command: {}, ack: { accepted: true }, source: { answered: true, ok: true }, at })
56
+ assert.equal('sourceCode' in row, false)
57
+ })
58
+
59
+ test('판정 함수가 세 값만 낸다 — ok 가 참/거짓이 아니면 모른다', () => {
60
+ assert.equal(sourceOutcomeOf({ answered: true, ok: true }), 'applied')
61
+ assert.equal(sourceOutcomeOf({ answered: true, ok: false }), 'refused')
62
+ assert.equal(sourceOutcomeOf({ answered: false }), 'unknown')
63
+ assert.equal(sourceOutcomeOf({ answered: true }), 'unknown', '답했다고 하면서 값이 없으면 모르는 것이다')
64
+ assert.equal(sourceOutcomeOf({ answered: true, ok: undefined as any }), 'unknown')
65
+ })
66
+
67
+ test('신원은 여전히 서버만 채운다 — 원본의 답이 그 규율을 흔들지 않는다', () => {
68
+ const row = buildCommandAudit({
69
+ command: { type: 'source.pause', origin: 'user', actorId: 'forged', actorEmail: 'forged@x' },
70
+ ack: { accepted: true },
71
+ source: { answered: true, ok: true },
72
+ at,
73
+ actor: { id: 'u1', email: 'real@x' }
74
+ })
75
+ assert.equal(row.actorId, 'u1')
76
+ assert.equal(row.actorEmail, 'real@x')
77
+ })
78
+
79
+ /*
80
+ * ── 구동 문이 실제로 남기는지 (소스로 지킨다) ───────────────────────────────
81
+ * 순수 함수만 시험하면 「기록을 만들 수 있다」까지만 지킨다. 남기지 않으면 그 함수는 아무 일도 하지 않는다.
82
+ */
83
+ test('구동 문이 게이트 뒤에서 원본의 답까지 남긴다', () => {
84
+ const src = readFileSync(fileURLToPath(new URL('../server/service/twin-control/twin-control-mutation.ts', import.meta.url)), 'utf-8')
85
+ const code = src
86
+ .split('\n')
87
+ .filter(line => !/^\s*(\*|\/\/|\/\*)/.test(line))
88
+ .join('\n')
89
+ /* 게이트는 남는다(결정 C 의 절반). */
90
+ const gates = code.match(/@privilege\(category: "twin", privilege: "mutation"/g) ?? []
91
+ assert.ok(gates.length >= 2, `구동 문에도 게이트가 있어야 한다(발견 ${gates.length})`)
92
+ /* 원본의 답을 같은 행에 닫는다. */
93
+ assert.match(code, /source: \{ answered: typeof result\?\.ok === 'boolean', ok: result\?\.ok, code: result\?\.code \}/)
94
+ assert.match(code, /type: `source\.\$\{action\}`/, '커맨드 채널과 같은 어휘로 적는다')
95
+ /* 기록 실패를 삼키지 않는다. */
96
+ assert.match(code, /source control audit not recorded/)
97
+ })
98
+
99
+ test('엔티티가 그 두 칸을 든다 — 비어 있음이 뜻을 갖는다', () => {
100
+ const src = readFileSync(fileURLToPath(new URL('../server/service/twin-audit/twin-audit-event.ts', import.meta.url)), 'utf-8')
101
+ assert.match(src, /sourceOutcome\?: string/)
102
+ assert.match(src, /sourceCode\?: string/)
103
+ assert.match(src, /the source was not asked/, '비어 있음의 뜻을 필드 설명이 말한다')
104
+ })
@@ -61,7 +61,7 @@ test('커널이 보고 API 를 갖지 않으면 undefined — "전부 기본값"
61
61
  assert.equal(readSpecCoverage(undefined), undefined)
62
62
  })
63
63
 
64
- test('커널 보고가 던져도 예측을 깨지 않는다', () => {
64
+ test('커널 보고가 오류를 내어도 예측을 깨지 않는다', () => {
65
65
  assert.equal(readSpecCoverage({ specCoverage: () => { throw new Error('boom') } }), undefined)
66
66
  })
67
67
 
@@ -0,0 +1,95 @@
1
+ /*
2
+ * ★ 단계 판정 — **체크리스트가 아니라 증거로 센다.**
3
+ *
4
+ * 이 테스트가 지키는 것:
5
+ * ① **「모른다」는 달성이 아니다.** 계측되지 않은 신호(`null`)를 0 으로 대체하면 「반출 0건」과
6
+ * 「반출을 세지 않았다」가 같아진다. 배선하지 않은 계측으로 단계를 주장할 길을 남기지 않는다.
7
+ * ② **반출을 세었는데 없으면 라이브가 아니다.** 되풀어 읽는 스냅샷은 원리적으로 반출을 못 낸다 —
8
+ * 이 판별식이 폴링을 「실시간」이라 부르는 것을 막는다.
9
+ * ③ **선언이 비면 예측이 아니다.** 커널 기본값 위에 선 숫자도 화면에서는 예측처럼 보인다.
10
+ * ④ **앞을 건너뛸 수 없다.** 뒤 단계는 자기 조건을 따지지 않고 `prerequisite` 으로 막는다.
11
+ */
12
+ import { test } from 'node:test'
13
+ import assert from 'node:assert/strict'
14
+
15
+ import { assessStagePath, forbiddenClaims, type StageEvidence } from '../server/engine/stage-path.ts'
16
+
17
+ /** 전부 달성한 증거 — 각 시험은 여기서 한 축만 무너뜨린다(무엇이 판정을 바꿨는지 분명해진다). */
18
+ const full = (): StageEvidence => ({
19
+ locationCount: 12,
20
+ itemCount: 340,
21
+ masterWarnings: 3,
22
+ liveIngestTotal: 1200,
23
+ removalsObserved: 17,
24
+ forecastRan: true,
25
+ specCoverage: { declared: 6, total: 8 },
26
+ actuateCapability: true,
27
+ correlatedEffectObserved: 2
28
+ })
29
+
30
+ test('전부 증거가 있으면 폐루프까지 도달하고 금지 표현이 없다', () => {
31
+ const p = assessStagePath(full())
32
+ assert.deepEqual(p.stages.map(s => s.achieved), [true, true, true, true])
33
+ assert.equal(p.reached, 'closed-loop')
34
+ assert.equal(p.next, null)
35
+ assert.deepEqual(forbiddenClaims(p), [])
36
+ })
37
+
38
+ test('못 옮긴 값을 세지 않았으면 구조도 달성이 아니다 — 0 이라고 우기지 않는다', () => {
39
+ const p = assessStagePath({ ...full(), masterWarnings: null })
40
+ const s1 = p.stages[0]
41
+ assert.equal(s1.achieved, false)
42
+ assert.deepEqual(s1.unmeasured, ['master-warnings'])
43
+ assert.deepEqual(s1.blockedBy, [], '자리와 물품은 있으므로 막힌 것이 아니라 모르는 것이다')
44
+ assert.equal(p.reached, null, '첫 칸을 못 밟았으면 서 있는 칸이 없다')
45
+ })
46
+
47
+ test('못 옮긴 값이 3건이어도 구조는 달성이다 — 요구는 「없다」가 아니라 「말할 수 있다」', () => {
48
+ assert.equal(assessStagePath({ ...full(), masterWarnings: 3 }).stages[0].achieved, true)
49
+ assert.equal(assessStagePath({ ...full(), masterWarnings: 0 }).stages[0].achieved, true)
50
+ })
51
+
52
+ test('반출을 세었는데 0 이면 라이브가 아니다 — 스냅샷 되풀어 읽기의 징표', () => {
53
+ const p = assessStagePath({ ...full(), removalsObserved: 0 })
54
+ assert.equal(p.stages[1].achieved, false)
55
+ assert.deepEqual(p.stages[1].blockedBy, ['no-removal-observed'])
56
+ assert.equal(p.reached, 'structure')
57
+ assert.ok(forbiddenClaims(p).includes('claim.realtime'), '실시간이라고 말할 수 없다')
58
+ })
59
+
60
+ test('반출이 계측되지 않으면 「없다」가 아니라 「모른다」로 막힌다', () => {
61
+ const p = assessStagePath({ ...full(), removalsObserved: null })
62
+ assert.deepEqual(p.stages[1].unmeasured, ['removals'])
63
+ assert.deepEqual(p.stages[1].blockedBy, [], '인제스트는 있으므로 막힌 것이 아니다')
64
+ })
65
+
66
+ test('선언이 비면 예측이 아니다 — 기본값 위에 선 숫자도 숫자로 보인다', () => {
67
+ const p = assessStagePath({ ...full(), specCoverage: { declared: 0, total: 8 } })
68
+ assert.equal(p.stages[2].achieved, false)
69
+ assert.deepEqual(p.stages[2].blockedBy, ['declarations-empty'])
70
+ assert.equal(p.reached, 'live')
71
+ assert.ok(forbiddenClaims(p).includes('claim.prediction'))
72
+ })
73
+
74
+ test('접수만으로는 폐루프가 아니다 — 효과가 관측으로 돌아와야 닫힌다', () => {
75
+ const p = assessStagePath({ ...full(), correlatedEffectObserved: 0 })
76
+ assert.deepEqual(p.stages[3].blockedBy, ['effect-not-observed'])
77
+ assert.equal(p.reached, 'forecast')
78
+ })
79
+
80
+ test('앞이 막히면 뒤는 자기 조건을 따지지 않는다 — 순서를 건너뛸 수 없다', () => {
81
+ const p = assessStagePath({ ...full(), locationCount: 0, itemCount: 0 })
82
+ assert.deepEqual(p.stages[0].blockedBy, ['no-locations', 'no-items'])
83
+ for (const later of p.stages.slice(1)) assert.deepEqual(later.blockedBy, ['prerequisite'])
84
+ assert.deepEqual(p.next, { stage: 'structure', blockedBy: ['no-locations', 'no-items'], unmeasured: [] })
85
+ })
86
+
87
+ test('다음 걸음은 목록이 아니라 하나다', () => {
88
+ const p = assessStagePath({ ...full(), removalsObserved: 0, specCoverage: null, actuateCapability: false })
89
+ assert.equal(p.next?.stage, 'live', '뒤쪽 결손을 함께 들이밀지 않는다')
90
+ })
91
+
92
+ test('비직렬 수량 물품만 있어도 구조는 달성이다 — 식품·화학은 개수로 오지 않는다', () => {
93
+ /* itemCount 는 「자리에 놓인 물품 수」이고, 비직렬 수량 물품(subLot)도 한 줄로 센다. */
94
+ assert.equal(assessStagePath({ ...full(), itemCount: 1 }).stages[0].achieved, true)
95
+ })
@@ -11,9 +11,21 @@
11
11
  import { test } from 'node:test'
12
12
  import assert from 'node:assert/strict'
13
13
 
14
- import { standardsForKind } from '../server/service/twin-model/standard-coverage.ts'
15
- import { iec61850Coverage } from '../server/service/twin-model/iec61850-coverage.ts'
16
- import { epcisCoverage } from '../server/service/twin-model/epcis-coverage.ts'
14
+ /*
15
+ * **빌드 산출물을 부른다** — 소스를 직접 실행하지 않는다.
16
+ *
17
+ * 이 모듈들은 서로를 값으로 참조한다(축 어긋남 판정을 한 곳에 두었다). 그 참조는 계약대로 `.js` 로
18
+ * 적히고, 타입만 지우고 소스를 실행하는 방식은 그 경로를 `.ts` 로 바꿔 주지 않는다. 그래서 소스를
19
+ * 부르면 「모듈을 찾을 수 없다」로 선다.
20
+ *
21
+ * **소스를 고친 뒤에는 `npm run build:server` 를 먼저 돌린다.**
22
+ */
23
+ import { createRequire } from 'node:module'
24
+
25
+ const req = createRequire(import.meta.url)
26
+ const { standardsForKind } = req('../dist-server/service/twin-model/standard-coverage.js')
27
+ const { iec61850Coverage } = req('../dist-server/service/twin-model/iec61850-coverage.js')
28
+ const { epcisCoverage } = req('../dist-server/service/twin-model/epcis-coverage.js')
17
29
  import { readFileSync } from 'node:fs'
18
30
  import { fileURLToPath } from 'node:url'
19
31
 
@@ -0,0 +1,55 @@
1
+ /*
2
+ * 상태별 내림 — **수 하나가 뜻을 바꾸지 않게** (2026-08-23).
3
+ *
4
+ * 커널이 관측 흡수에서 완료된 작업을 떨어뜨리지 않게 고쳐진 뒤, 개념 지도의 작업 카드가 0 에서 2,820 이
5
+ * 되었다. 카드는 상태의 인구조사이므로 그 수가 맞지만, 사람은 「지금 할 일 2,820건」으로 읽는다.
6
+ * 수를 줄여 맞는 척하게 만들지 않고, 무엇으로 이루어진 수인지 함께 낸다.
7
+ *
8
+ * 이 시험이 지키는 것:
9
+ * ① 상태별로 갈라 세고, **많은 것부터** 낸다(카드가 한 줄로 보이므로 순서가 뜻을 만든다).
10
+ * ② 낱말은 커널의 것 그대로 — 여기서 묶거나 고쳐 부르지 않는다.
11
+ * ③ 상태를 말하지 않는 축은 **필드를 만들지 않는다**(빈 표는 「상태 없음」과 「못 읽음」을 뭉갠다).
12
+ */
13
+ import { test } from 'node:test'
14
+ import assert from 'node:assert/strict'
15
+
16
+ import { statusTally } from '../server/service/twin-model/status-tally.ts'
17
+
18
+ test('상태별로 갈라 세고 많은 것부터 낸다', () => {
19
+ const r = statusTally([
20
+ { id: 'a', status: 'completed' },
21
+ { id: 'b', status: 'in-progress' },
22
+ { id: 'c', status: 'completed' },
23
+ { id: 'd', status: 'completed' }
24
+ ])
25
+ assert.deepEqual(r.statusCounts, [
26
+ { status: 'completed', count: 3 },
27
+ { status: 'in-progress', count: 1 }
28
+ ])
29
+ })
30
+
31
+ test('낱말은 커널의 것 그대로 — 묶거나 고쳐 부르지 않는다', () => {
32
+ const r = statusTally([{ status: 'assigned' }, { status: 'created' }])
33
+ assert.deepEqual(
34
+ r.statusCounts?.map(s => s.status).sort(),
35
+ ['assigned', 'created'],
36
+ '「대기」처럼 앱의 낱말로 바꾸면 커널이 늘린 낱말을 화면이 조용히 잘못 부른다'
37
+ )
38
+ })
39
+
40
+ test('상태를 말하지 않는 축은 필드를 만들지 않는다', () => {
41
+ assert.deepEqual(statusTally([{ id: 'loc-1' }, { id: 'loc-2' }]), {}, '빈 표를 내지 않는다')
42
+ assert.deepEqual(statusTally([]), {})
43
+ assert.deepEqual(statusTally(null), {})
44
+ assert.deepEqual(statusTally({ id: 'not-a-list' }), {}, '목록이 아니면 셀 것이 없다')
45
+ })
46
+
47
+ test('상태를 말하는 것만 센다 — 없는 것을 「모름」이라는 갈래로 만들지 않는다', () => {
48
+ const r = statusTally([{ status: 'completed' }, { id: 'x' }, { status: '' }, { status: 7 }])
49
+ assert.deepEqual(r.statusCounts, [{ status: 'completed', count: 1 }])
50
+ })
51
+
52
+ test('한 갈래뿐이면 그대로 한 갈래다 — 화면이 그때는 내림을 감춘다', () => {
53
+ const r = statusTally([{ status: 'completed' }, { status: 'completed' }])
54
+ assert.deepEqual(r.statusCounts, [{ status: 'completed', count: 2 }], '판단은 화면이 하고, 사실은 그대로 낸다')
55
+ })