@things-factory/headless-twin 10.0.12 → 10.0.14

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 (315) hide show
  1. package/dist-server/engine/canonical-ingest.d.ts +57 -2
  2. package/dist-server/engine/canonical-ingest.js +43 -2
  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/index.d.ts +3 -0
  7. package/dist-server/engine/index.js +4 -0
  8. package/dist-server/engine/index.js.map +1 -1
  9. package/dist-server/engine/ingest-health.d.ts +376 -0
  10. package/dist-server/engine/ingest-health.js +482 -0
  11. package/dist-server/engine/ingest-health.js.map +1 -0
  12. package/dist-server/engine/integration-coverage.d.ts +76 -0
  13. package/dist-server/engine/integration-coverage.js +73 -0
  14. package/dist-server/engine/integration-coverage.js.map +1 -0
  15. package/dist-server/engine/integration-probes.d.ts +65 -0
  16. package/dist-server/engine/integration-probes.js +100 -0
  17. package/dist-server/engine/integration-probes.js.map +1 -0
  18. package/dist-server/engine/integration-runner.d.ts +45 -0
  19. package/dist-server/engine/integration-runner.js +59 -0
  20. package/dist-server/engine/integration-runner.js.map +1 -0
  21. package/dist-server/engine/integration-target-profile.d.ts +57 -0
  22. package/dist-server/engine/integration-target-profile.js +79 -0
  23. package/dist-server/engine/integration-target-profile.js.map +1 -0
  24. package/dist-server/engine/kpi-fold.d.ts +27 -0
  25. package/dist-server/engine/kpi-fold.js +47 -4
  26. package/dist-server/engine/kpi-fold.js.map +1 -1
  27. package/dist-server/engine/kpi-query.js +128 -14
  28. package/dist-server/engine/kpi-query.js.map +1 -1
  29. package/dist-server/engine/live-feed-registry.js +2 -16
  30. package/dist-server/engine/live-feed-registry.js.map +1 -1
  31. package/dist-server/engine/load-meter.d.ts +1 -1
  32. package/dist-server/engine/load-meter.js +12 -3
  33. package/dist-server/engine/load-meter.js.map +1 -1
  34. package/dist-server/engine/log.d.ts +18 -0
  35. package/dist-server/engine/log.js +80 -0
  36. package/dist-server/engine/log.js.map +1 -0
  37. package/dist-server/engine/loop-lag.d.ts +54 -0
  38. package/dist-server/engine/loop-lag.js +87 -0
  39. package/dist-server/engine/loop-lag.js.map +1 -0
  40. package/dist-server/engine/model-gap.d.ts +108 -0
  41. package/dist-server/engine/model-gap.js +95 -0
  42. package/dist-server/engine/model-gap.js.map +1 -0
  43. package/dist-server/engine/restart-policy.d.ts +3 -3
  44. package/dist-server/engine/restart-policy.js +4 -4
  45. package/dist-server/engine/restart-policy.js.map +1 -1
  46. package/dist-server/engine/runtime-key.js +1 -1
  47. package/dist-server/engine/runtime-key.js.map +1 -1
  48. package/dist-server/engine/stage-path.d.ts +83 -0
  49. package/dist-server/engine/stage-path.js +118 -0
  50. package/dist-server/engine/stage-path.js.map +1 -0
  51. package/dist-server/engine/twin-engine.d.ts +386 -16
  52. package/dist-server/engine/twin-engine.js +1217 -172
  53. package/dist-server/engine/twin-engine.js.map +1 -1
  54. package/dist-server/index.js +34 -2
  55. package/dist-server/index.js.map +1 -1
  56. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.d.ts +5 -0
  57. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js +58 -0
  58. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js.map +1 -0
  59. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.d.ts +5 -0
  60. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js +55 -0
  61. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js.map +1 -0
  62. package/dist-server/migrations/index.js +7 -1
  63. package/dist-server/migrations/index.js.map +1 -1
  64. package/dist-server/service/index.d.ts +5 -2
  65. package/dist-server/service/index.js +18 -7
  66. package/dist-server/service/index.js.map +1 -1
  67. package/dist-server/service/reference/discovery-result.d.ts +1 -1
  68. package/dist-server/service/reference/discovery-result.js +1 -1
  69. package/dist-server/service/reference/discovery-result.js.map +1 -1
  70. package/dist-server/service/reference/index.d.ts +4 -1
  71. package/dist-server/service/reference/index.js +6 -1
  72. package/dist-server/service/reference/index.js.map +1 -1
  73. package/dist-server/service/reference/reference-adapter.d.ts +210 -3
  74. package/dist-server/service/reference/reference-adapter.js +28 -4
  75. package/dist-server/service/reference/reference-adapter.js.map +1 -1
  76. package/dist-server/service/reference/reference-assessment.d.ts +78 -0
  77. package/dist-server/service/reference/reference-assessment.js +156 -0
  78. package/dist-server/service/reference/reference-assessment.js.map +1 -0
  79. package/dist-server/service/reference/reference-live.d.ts +12 -1
  80. package/dist-server/service/reference/reference-live.js +134 -11
  81. package/dist-server/service/reference/reference-live.js.map +1 -1
  82. package/dist-server/service/reference/reference-master.d.ts +59 -3
  83. package/dist-server/service/reference/reference-master.js +34 -4
  84. package/dist-server/service/reference/reference-master.js.map +1 -1
  85. package/dist-server/service/reference/reference-probe.d.ts +92 -0
  86. package/dist-server/service/reference/reference-probe.js +186 -0
  87. package/dist-server/service/reference/reference-probe.js.map +1 -0
  88. package/dist-server/service/reference/reference-progress-subscription.d.ts +17 -0
  89. package/dist-server/service/reference/reference-progress-subscription.js +94 -0
  90. package/dist-server/service/reference/reference-progress-subscription.js.map +1 -0
  91. package/dist-server/service/reference/reference-progress.d.ts +38 -0
  92. package/dist-server/service/reference/reference-progress.js +71 -0
  93. package/dist-server/service/reference/reference-progress.js.map +1 -0
  94. package/dist-server/service/reference/reference-resolver.js +210 -43
  95. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  96. package/dist-server/service/reference/twin-reference.d.ts +32 -0
  97. package/dist-server/service/reference/twin-reference.js +10 -0
  98. package/dist-server/service/reference/twin-reference.js.map +1 -1
  99. package/dist-server/service/twin-control/twin-control-mutation.js +4 -3
  100. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  101. package/dist-server/service/twin-event/journal-count.d.ts +21 -0
  102. package/dist-server/service/twin-event/journal-count.js +26 -0
  103. package/dist-server/service/twin-event/journal-count.js.map +1 -0
  104. package/dist-server/service/twin-event/twin-event-keys.d.ts +9 -0
  105. package/dist-server/service/twin-event/twin-event-keys.js +15 -18
  106. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  107. package/dist-server/service/twin-event/twin-event-type.d.ts +3 -1
  108. package/dist-server/service/twin-event/twin-event-type.js +18 -1
  109. package/dist-server/service/twin-event/twin-event-type.js.map +1 -1
  110. package/dist-server/service/twin-event/twin-event.d.ts +1 -0
  111. package/dist-server/service/twin-event/twin-event.js +9 -1
  112. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  113. package/dist-server/service/twin-forecast/twin-forecast-query.js +26 -2
  114. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  115. package/dist-server/service/twin-ingest-window/index.d.ts +5 -0
  116. package/dist-server/service/twin-ingest-window/index.js +10 -0
  117. package/dist-server/service/twin-ingest-window/index.js.map +1 -0
  118. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.d.ts +12 -0
  119. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js +123 -0
  120. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js.map +1 -0
  121. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.d.ts +1 -0
  122. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js +51 -0
  123. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js.map +1 -0
  124. package/dist-server/service/twin-ingest-window/twin-ingest-window.d.ts +16 -0
  125. package/dist-server/service/twin-ingest-window/twin-ingest-window.js +108 -0
  126. package/dist-server/service/twin-ingest-window/twin-ingest-window.js.map +1 -0
  127. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  128. package/dist-server/service/twin-journal/twin-journal-query.js +58 -5
  129. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  130. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +14 -0
  131. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +72 -2
  132. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  133. package/dist-server/service/twin-metrics/twin-metrics-query.d.ts +1 -0
  134. package/dist-server/service/twin-metrics/twin-metrics-query.js +78 -3
  135. package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
  136. package/dist-server/service/twin-model/axis-journal-evidence.d.ts +2 -0
  137. package/dist-server/service/twin-model/axis-journal-evidence.js +89 -0
  138. package/dist-server/service/twin-model/axis-journal-evidence.js.map +1 -0
  139. package/dist-server/service/twin-model/epcis-coverage.d.ts +1 -1
  140. package/dist-server/service/twin-model/epcis-coverage.js +46 -7
  141. package/dist-server/service/twin-model/epcis-coverage.js.map +1 -1
  142. package/dist-server/service/twin-model/iec61850-coverage.d.ts +1 -1
  143. package/dist-server/service/twin-model/iec61850-coverage.js +18 -7
  144. package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -1
  145. package/dist-server/service/twin-model/isa95-coverage.d.ts +14 -0
  146. package/dist-server/service/twin-model/isa95-coverage.js +108 -14
  147. package/dist-server/service/twin-model/isa95-coverage.js.map +1 -1
  148. package/dist-server/service/twin-model/item-ref.d.ts +24 -0
  149. package/dist-server/service/twin-model/item-ref.js +88 -0
  150. package/dist-server/service/twin-model/item-ref.js.map +1 -0
  151. package/dist-server/service/twin-model/name-index.d.ts +36 -0
  152. package/dist-server/service/twin-model/name-index.js +116 -0
  153. package/dist-server/service/twin-model/name-index.js.map +1 -0
  154. package/dist-server/service/twin-model/project-structure.js +1 -1
  155. package/dist-server/service/twin-model/project-structure.js.map +1 -1
  156. package/dist-server/service/twin-model/status-tally.d.ts +9 -0
  157. package/dist-server/service/twin-model/status-tally.js +37 -0
  158. package/dist-server/service/twin-model/status-tally.js.map +1 -0
  159. package/dist-server/service/twin-model/twin-lineage-query.js +40 -12
  160. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
  161. package/dist-server/service/twin-model/twin-model-item-query.js +38 -39
  162. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
  163. package/dist-server/service/twin-model/twin-model-query.js +156 -4
  164. package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
  165. package/dist-server/service/twin-model/twin-model-tree-query.js +7 -0
  166. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
  167. package/dist-server/service/twin-readiness/index.d.ts +2 -0
  168. package/dist-server/service/twin-readiness/index.js +6 -0
  169. package/dist-server/service/twin-readiness/index.js.map +1 -0
  170. package/dist-server/service/twin-readiness/twin-readiness-query.d.ts +3 -0
  171. package/dist-server/service/twin-readiness/twin-readiness-query.js +103 -0
  172. package/dist-server/service/twin-readiness/twin-readiness-query.js.map +1 -0
  173. package/dist-server/service/twin-space/twin-space-resolver.d.ts +2 -2
  174. package/dist-server/service/twin-space/twin-space-resolver.js +5 -4
  175. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  176. package/dist-shared/entity-delta.d.ts +23 -3
  177. package/dist-shared/entity-delta.js +12 -8
  178. package/dist-shared/entity-delta.js.map +1 -1
  179. package/dist-shared/kpi-broadcast.js +1 -1
  180. package/dist-shared/kpi-broadcast.js.map +1 -1
  181. package/dist-shared/touched-items.d.ts +7 -0
  182. package/dist-shared/touched-items.js +80 -0
  183. package/dist-shared/touched-items.js.map +1 -0
  184. package/package.json +7 -7
  185. package/server/engine/canonical-ingest.ts +92 -7
  186. package/server/engine/command-routing.ts +1 -1
  187. package/server/engine/index.ts +4 -0
  188. package/server/engine/ingest-health.ts +782 -0
  189. package/server/engine/integration-coverage.ts +147 -0
  190. package/server/engine/integration-probes.ts +144 -0
  191. package/server/engine/integration-runner.ts +95 -0
  192. package/server/engine/integration-target-profile.ts +103 -0
  193. package/server/engine/kpi-fold.ts +72 -4
  194. package/server/engine/kpi-query.ts +129 -15
  195. package/server/engine/live-feed-registry.ts +2 -1
  196. package/server/engine/load-meter.ts +12 -3
  197. package/server/engine/log.ts +72 -0
  198. package/server/engine/loop-lag.ts +120 -0
  199. package/server/engine/model-gap.ts +168 -0
  200. package/server/engine/restart-policy.ts +4 -4
  201. package/server/engine/runtime-key.ts +1 -1
  202. package/server/engine/stage-path.ts +172 -0
  203. package/server/engine/twin-engine.ts +1334 -168
  204. package/server/index.ts +35 -2
  205. package/server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.ts +54 -0
  206. package/server/migrations/1786200000000-CarryLiveFeedCursor.ts +53 -0
  207. package/server/migrations/index.ts +7 -1
  208. package/server/service/index.ts +11 -0
  209. package/server/service/reference/discovery-result.ts +1 -1
  210. package/server/service/reference/index.ts +6 -1
  211. package/server/service/reference/reference-adapter.ts +220 -5
  212. package/server/service/reference/reference-assessment.ts +250 -0
  213. package/server/service/reference/reference-live.ts +143 -11
  214. package/server/service/reference/reference-master.ts +76 -6
  215. package/server/service/reference/reference-probe.ts +264 -0
  216. package/server/service/reference/reference-progress-subscription.ts +73 -0
  217. package/server/service/reference/reference-progress.ts +95 -0
  218. package/server/service/reference/reference-resolver.ts +200 -12
  219. package/server/service/reference/twin-reference.ts +39 -1
  220. package/server/service/twin-control/twin-control-mutation.ts +4 -3
  221. package/server/service/twin-event/journal-count.ts +53 -0
  222. package/server/service/twin-event/twin-event-keys.ts +16 -1
  223. package/server/service/twin-event/twin-event-type.ts +26 -2
  224. package/server/service/twin-event/twin-event.ts +7 -0
  225. package/server/service/twin-forecast/twin-forecast-query.ts +29 -2
  226. package/server/service/twin-ingest-window/index.ts +7 -0
  227. package/server/service/twin-ingest-window/twin-ingest-window-query.ts +125 -0
  228. package/server/service/twin-ingest-window/twin-ingest-window-writer.ts +57 -0
  229. package/server/service/twin-ingest-window/twin-ingest-window.ts +113 -0
  230. package/server/service/twin-instance/twin-instance.ts +1 -1
  231. package/server/service/twin-journal/twin-journal-query.ts +59 -5
  232. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +77 -3
  233. package/server/service/twin-metrics/twin-metrics-query.ts +81 -3
  234. package/server/service/twin-model/axis-journal-evidence.ts +57 -0
  235. package/server/service/twin-model/epcis-coverage.ts +29 -8
  236. package/server/service/twin-model/iec61850-coverage.ts +2 -8
  237. package/server/service/twin-model/isa95-coverage.ts +107 -14
  238. package/server/service/twin-model/item-ref.ts +80 -0
  239. package/server/service/twin-model/name-index.ts +94 -0
  240. package/server/service/twin-model/project-structure.ts +1 -1
  241. package/server/service/twin-model/status-tally.ts +37 -0
  242. package/server/service/twin-model/twin-lineage-query.ts +38 -9
  243. package/server/service/twin-model/twin-model-item-query.ts +26 -27
  244. package/server/service/twin-model/twin-model-query.ts +155 -4
  245. package/server/service/twin-model/twin-model-tree-query.ts +7 -0
  246. package/server/service/twin-readiness/index.ts +3 -0
  247. package/server/service/twin-readiness/twin-readiness-query.ts +96 -0
  248. package/server/service/twin-space/twin-space-resolver.ts +5 -4
  249. package/shared/entity-delta.ts +31 -7
  250. package/shared/kpi-broadcast.ts +1 -1
  251. package/shared/touched-items.ts +73 -0
  252. package/test/axis-journal-evidence.test.ts +71 -0
  253. package/test/axis-read.test.ts +101 -1
  254. package/test/boot-resume.test.ts +92 -26
  255. package/test/broadcast-cost-baseline.test.ts +200 -0
  256. package/test/broadcast-period.test.ts +58 -0
  257. package/test/canonical-ingest-vocabularies.test.ts +36 -1
  258. package/test/canonical-quantity-door.test.ts +61 -0
  259. package/test/checkpoint-refuses-empty.test.ts +103 -0
  260. package/test/command-routing.test.ts +1 -1
  261. package/test/cursor-stall-not-read-failure.test.ts +126 -0
  262. package/test/declared-location-types.test.ts +89 -0
  263. package/test/discovery-result.test.ts +1 -1
  264. package/test/duration-estimators.test.ts +1 -1
  265. package/test/entity-delta.test.ts +2 -2
  266. package/test/ingest-bench.test.ts +3 -3
  267. package/test/ingest-health-engine.test.ts +248 -0
  268. package/test/ingest-health-wiring.test.ts +119 -0
  269. package/test/ingest-health.test.ts +306 -0
  270. package/test/ingest-history.test.ts +247 -0
  271. package/test/integration-probes.test.ts +103 -0
  272. package/test/integration-runner.test.ts +95 -0
  273. package/test/item-ref.test.ts +78 -0
  274. package/test/journal-read-discipline.test.ts +177 -0
  275. package/test/journal-retention.test.ts +133 -0
  276. package/test/journal-sort-axis.test.ts +142 -0
  277. package/test/journal-write-door.test.ts +110 -0
  278. package/test/journal-write-trend.test.ts +115 -0
  279. package/test/kernel-kind-guard.test.ts +4 -4
  280. package/test/kpi-fold.test.ts +90 -3
  281. package/test/kpi-query-bench.test.ts +2 -2
  282. package/test/lineage-survives-restart.test.ts +17 -1
  283. package/test/live-cursor-wiring.test.ts +87 -0
  284. package/test/live-feed-registry.test.ts +9 -1
  285. package/test/live-kernel-facts.test.ts +7 -7
  286. package/test/live-mirror-parity.test.ts +6 -0
  287. package/test/load-meter.test.ts +29 -15
  288. package/test/local-declarations.test.ts +1 -1
  289. package/test/log-stamp.test.ts +59 -0
  290. package/test/loop-lag.test.ts +82 -0
  291. package/test/mirror-resumes-from-checkpoint.test.ts +192 -0
  292. package/test/model-gap.test.ts +153 -0
  293. package/test/oee-accumulator.test.ts +4 -0
  294. package/test/operational-vocabulary.test.ts +4 -4
  295. package/test/read-failure-visible.test.ts +145 -0
  296. package/test/reference-grounding.test.ts +70 -0
  297. package/test/resolve-ts-siblings.mjs +52 -0
  298. package/test/restart-policy.test.ts +7 -7
  299. package/test/revision-axis.test.ts +104 -0
  300. package/test/runtime-key.test.ts +2 -2
  301. package/test/scale-twin-bench.test.ts +2 -2
  302. package/test/spec-coverage.test.ts +1 -1
  303. package/test/stage-path.test.ts +95 -0
  304. package/test/standard-coverage.test.ts +15 -3
  305. package/test/status-tally.test.ts +55 -0
  306. package/test/structure-revision-db.test.ts +4 -0
  307. package/test/tenant-registry-db.test.ts +1 -1
  308. package/test/time-range.test.ts +152 -0
  309. package/test/touched-items.test.ts +61 -0
  310. package/test/twin-event-keys.test.ts +10 -2
  311. package/test/twin-model-item-db.test.ts +2 -0
  312. package/test/warm-start-seam.test.ts +4 -0
  313. package/test/yield-loop.test.ts +57 -2
  314. package/tsconfig.shared.tsbuildinfo +1 -1
  315. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,57 @@
1
+ /*
2
+ * **저널에 그 축의 사실이 있나** — 「아직 없다」와 「왔는데 상태에 없다」를 가른다 (2026-08-22).
3
+ *
4
+ * ── 왜 필요했나 (실측) ──────────────────────────────────────────────────────
5
+ * 화면이 「작업 — 아직 하나도 없습니다」라고 말하는데, 그 트윈의 저널에는 `task.status` 5,786건이
6
+ * 있었고 **마지막 상태가 `in-progress`** 였다. 사용자가 「작업 지시 835건인데 작업이 0인 이유」를
7
+ * 물어 반나절을 조사해 원인을 찾았다: 관측 모드 커널이 관측기의 작업을 스냅샷으로 내보내지 않는다
8
+ * (커널 자리이고 그 레인에 넘겼다).
9
+ *
10
+ * 화면 몫은 다른 것이다. **카드가 단정하면 안 된다.** 「아직 하나도 없습니다」는 「선언은 있고 사실이
11
+ * 아직 일어나지 않았다」는 뜻인데, 실제로는 「사실이 일어났는데 상태에 오지 못했다」였다 — 그 차이가
12
+ * 사용자를 반나절 조사로 보낸다.
13
+ *
14
+ * 그래서 상태가 빈 축에 대해 **저널을 한 건만 물어본다.** 있으면 화면이 그렇게 말한다.
15
+ *
16
+ * ── 어휘의 집 ───────────────────────────────────────────────────────────────
17
+ * 어느 사건이 어느 축을 채우는지는 **커널이 선언한 낱말**에서 온다(`OP_EVENT`·`ENERGY_EVENT`, 그리고
18
+ * EPCIS 접두사). 여기서 이름을 새로 만들지 않는다 — 커널이 낱말을 늘리면 이 표에 한 줄이 늘고,
19
+ * 표에 없는 축은 **아무 말도 하지 않는다**(모르는 것을 지어내지 않는다).
20
+ */
21
+ import { anyJournalRow } from '../twin-event/journal-count.js'
22
+
23
+ /**
24
+ * 상태 축 → 그 축을 채우는 사건 낱말. 표에 없으면 판단하지 않는다.
25
+ *
26
+ * 물품은 EPCIS 로 온다(사건 이름이 여러 개다) — 그래서 접두사로 본다.
27
+ */
28
+ const AXIS_EVENTS: Record<string, { types?: string[]; prefix?: string }> = {
29
+ tasks: { types: ['task.status'] },
30
+ orders: { types: ['order.status'] },
31
+ items: { prefix: 'epcis.' },
32
+ demandWindows: { types: ['energy.demand.window'] }
33
+ }
34
+
35
+ /** 이 축의 사실이 저널에 한 건이라도 있나. 판단할 수 없으면 `null`(모른다). */
36
+ export async function axisSeenInJournal(
37
+ repo: any,
38
+ where: Record<string, unknown>,
39
+ axis: string
40
+ ): Promise<boolean | null> {
41
+ const spec = AXIS_EVENTS[axis]
42
+ if (!spec) return null
43
+
44
+ if (spec.types) {
45
+ for (const t of spec.types) {
46
+ if (await anyJournalRow(repo, { ...where, eventType: t })) return true
47
+ }
48
+ return false
49
+ }
50
+ /*
51
+ * 접두사로 찾는 축(물품) — 한 건만 있으면 된다. `Like` 를 쓰는 대신 **낱말 목록**을 쓰지 않는 이유는
52
+ * EPCIS 사건 이름이 커널에서 자라기 때문이다(객체·집약·변환·거래…). 이름을 여기 베끼면 커널이
53
+ * 하나 늘릴 때 이 자리가 조용히 낡는다.
54
+ */
55
+ const { Like } = await import('typeorm')
56
+ return anyJournalRow(repo, { ...where, eventType: Like(`${spec.prefix}%`) })
57
+ }
@@ -15,7 +15,7 @@
15
15
  *
16
16
  * ISA-95 표와 같은 규율이다(`isa95-coverage.ts` 머리말): 사람이 판단해 적고, 틀리면 여기를 고친다.
17
17
  */
18
- import type { Isa95Concept, CoverageLevel } from './isa95-coverage.js'
18
+ import { reconcileAxes, type Isa95Concept, type CoverageLevel } from './isa95-coverage.js'
19
19
 
20
20
  /* Part: EPCIS 2.0 의 사건 종류 — 무엇이 일어났는가. */
21
21
  const EVENTS: Isa95Concept[] = [
@@ -86,6 +86,20 @@ const FIELDS: Isa95Concept[] = [
86
86
  surface: 'full',
87
87
  note: 'twin.epcis.note.identity'
88
88
  },
89
+ /*
90
+ * ── 2026-08-24 주장 근거가 달라졌다 ────────────────────────────────────────
91
+ * 이 줄은 오래 `full` 이었지만 근거가 **우리 선언**이었다. 이제 **정본 원문 대조**다:
92
+ * CBV Standard Release 2.0(Ratified Jun 2022) §7.2.3 의 처분 38개를 전수 확인했다.
93
+ *
94
+ * 그 대조가 실제로 셋을 바꿨다.
95
+ * · `expired` — 있는 낱말인데 우리가 쓰지 않고 있었다(기한 경과를 처분으로 말할 수 있게 됐다)
96
+ * · `conformant` / `non_conformant` — 검사 판정을 처분으로 남기는 낱말. 함께 `inspecting` bizStep
97
+ * · `non_sellable_expired` 는 표준이 **폐기**하고 `expired` 로 대체한 것을 확인
98
+ *
99
+ * ★ 그리고 **검증 방법에 함정이 있다**: `ref.gs1.org/cbv/…` 로 URN 을 조회하면 **지어낸 값에도
100
+ * 똑같은 응답**이 온다. 그것으로 확인했다고 여기면 없는 낱말을 발행한다(한 번 그렇게 했다).
101
+ * 확인은 정본 문서로만 한다.
102
+ */
89
103
  {
90
104
  std: 'bizStep / disposition (CBV)',
91
105
  part: 'fields',
@@ -160,6 +174,19 @@ const FIELDS: Isa95Concept[] = [
160
174
  surface: 'none',
161
175
  note: 'twin.epcis.note.persistentDisposition'
162
176
  },
177
+ /*
178
+ * ── 2026-08-24 계측이 어디로 들어오는지 확정됐다 ──────────────────────────
179
+ * 「계측은 EPCIS 센서 필드로 오지 않는다」는 사실이 이제 **자리를 갖는다**: 자리의 상시 관측은
180
+ * ISA-95 `OperationsEvent` 로 들어오고(커널 `location.measured` 채널), 에너지는 그 위의 누적기가
181
+ * 받는다. ISA-95 표의 `OperationsEvent` 줄이 그 판정을 든다.
182
+ *
183
+ * 왜 EPCIS 가 아닌가: `SensorElement` 는 **개체에 붙는** 관측이고, 냉장실의 온도는 그 안의 물건
184
+ * 수백 개와 관계되며 그 수백 개는 시간에 따라 바뀐다. 개체마다 붙이면 같은 사실이 수백 벌이 되고,
185
+ * 물건이 떠나면 그 방의 온도 이력이 함께 사라진다. 주인은 **자리**다.
186
+ *
187
+ * 그래서 이 줄은 `behavior: 'none'` 으로 남는다 — 결손이 아니라 **다른 층으로 들어온다는 사실**이다.
188
+ * 그 구별을 표가 말하지 않으면 「센서를 못 받는 트윈」으로 읽힌다.
189
+ */
163
190
  {
164
191
  std: 'sensorElementList',
165
192
  part: 'fields',
@@ -179,13 +206,7 @@ export function epcisCoverage(axes: string[]): {
179
206
  concepts: (Isa95Concept & { axisMissing?: boolean })[]
180
207
  totals: { concepts: number; structure: number; behavior: number; surface: number }
181
208
  } {
182
- const known = new Set(axes)
183
- const concepts = EPCIS_CONCEPTS.map(c => {
184
- const axisMissing = !!c.axis && !known.has(c.axis)
185
- const shownOn = c.shownOn?.filter(a => known.has(a))
186
- const fixed = shownOn && shownOn.length !== c.shownOn?.length ? { ...c, shownOn } : c
187
- return axisMissing ? { ...fixed, structure: 'none' as CoverageLevel, axisMissing } : fixed
188
- })
209
+ const concepts = EPCIS_CONCEPTS.map(c => reconcileAxes(c, new Set(axes)))
189
210
  const count = (k: 'structure' | 'behavior' | 'surface') => concepts.filter(c => c[k] === 'full').length
190
211
  return {
191
212
  model: 'GS1 EPCIS 2.0',
@@ -14,7 +14,7 @@
14
14
  * ISA-95 표와 같은 규율이다(`isa95-coverage.ts` 머리말): 사람이 판단해 적고, 틀리면 여기를 고친다.
15
15
  * 자동으로 알 수 있는 것 하나만 자동으로 한다 — 축 이름이 커널 축 목록에 없으면 구조가 없는 것이다.
16
16
  */
17
- import type { Isa95Concept, CoverageLevel } from './isa95-coverage.js'
17
+ import { reconcileAxes, type Isa95Concept, type CoverageLevel } from './isa95-coverage.js'
18
18
 
19
19
  /*
20
20
  * Part 7-4 — 논리 노드 클래스. 이 프로파일이 쓰는 것만 적는다(표준 전체를 옮기지 않는다):
@@ -100,13 +100,7 @@ export function iec61850Coverage(axes: string[]): {
100
100
  concepts: (Isa95Concept & { axisMissing?: boolean })[]
101
101
  totals: { concepts: number; structure: number; behavior: number; surface: number }
102
102
  } {
103
- const known = new Set(axes)
104
- const concepts = IEC61850_CONCEPTS.map(c => {
105
- const axisMissing = !!c.axis && !known.has(c.axis)
106
- const shownOn = c.shownOn?.filter(a => known.has(a))
107
- const fixed = shownOn && shownOn.length !== c.shownOn?.length ? { ...c, shownOn } : c
108
- return axisMissing ? { ...fixed, structure: 'none' as CoverageLevel, axisMissing } : fixed
109
- })
103
+ const concepts = IEC61850_CONCEPTS.map(c => reconcileAxes(c, new Set(axes)))
110
104
  const count = (k: 'structure' | 'behavior' | 'surface') => concepts.filter(c => c[k] === 'full').length
111
105
  return {
112
106
  /* 이름에 (vocabulary) 를 달아 둔다 — 프로토콜 적합성으로 읽히지 않게. */
@@ -102,9 +102,39 @@ const PART2: Isa95Concept[] = [
102
102
  { std: 'PhysicalAsset', part: '2', label: 'twin.isa95.PhysicalAsset', axis: 'assets', structure: 'full', behavior: 'full', surface: 'full' },
103
103
  { std: 'MaterialClass', part: '2', label: 'twin.isa95.MaterialClass', axis: 'materialClasses', structure: 'full', behavior: 'partial', surface: 'full' },
104
104
  { std: 'MaterialDefinition', part: '2', label: 'twin.isa95.MaterialDefinition', axis: 'materialDefinitions', structure: 'full', behavior: 'full', surface: 'full' },
105
- /* 로트·서브로트는 개체 축이 아니라 **관측**으로 존재한다(EPCIS 개체) — 구조는 저널이 갖는다. */
105
+ /*
106
+ * 로트·서브로트는 개체 축이 아니라 **관측**으로 존재한다(EPCIS 개체) — 구조는 저널이 갖는다.
107
+ *
108
+ * ── 2026-08-24 로트가 시험 결과를 들게 됐다 (커널 0.7.57) ─────────────────
109
+ * `ItemState.testResults` 로 「이 로트를 쓸 수 있나」의 **근거**가 로트에 실린다. 그전에는 판정
110
+ * (`disposition`)만 있었고 「왜 그렇게 판정했나」를 되짚을 수 없었다.
111
+ *
112
+ * **구조를 `full` 로 올리지 않는다.** `MaterialLotType` 원문(B2MML-Material.xsd)이 드는 것 중
113
+ * 아직 없는 것이 있다: `MaterialLotProperty`(시험이 아닌 로트 속성 — 원산지·포장 형태) ·
114
+ * `TestSpecificationID`(로트별 기준 참조 — 품목 정의가 대신 답하므로 두지 않았다) · `Status` ·
115
+ * `HierarchyScope` · `Version`. 채운 것만 세는 것이 이 표의 규율이다.
116
+ */
106
117
  { std: 'MaterialLot', part: '2', label: 'twin.isa95.MaterialLot', axis: null, structure: 'partial', behavior: 'full', surface: 'partial', note: 'twin.isa95.note.lot' },
107
- { std: 'MaterialSublot', part: '2', label: 'twin.isa95.MaterialSublot', axis: null, structure: 'none', behavior: 'none', surface: 'none' },
118
+ /*
119
+ * ── 2026-08-24 판정 정정: `none` 이 아니었다 ──────────────────────────────
120
+ * 이 줄은 셋 다 `none` 이었는데 **표가 코드보다 뒤처져 있었다.** 커널은 서브로트를 든다:
121
+ *
122
+ * `ItemState.subLotId` 표준 `MaterialSubLot.ID` (계약이 그렇게 적고 있다)
123
+ * `subLotIdOf(class, location)` 비직렬 로트가 자리마다 나뉠 때 그 부분의 이름을 만든다
124
+ * `itemKeyOf(item)` **개체의 정체성이 이 값이다** — `subLotId ?? epc`
125
+ *
126
+ * 기능이 `full` 인 근거: 이 축이 없으면 같은 로트를 rack-1 에 100개·rack-2 에 60개 관측했을 때
127
+ * **뒤에 온 관측이 앞을 덮어 100개가 조용히 사라진다**(합계 160 → 60). 실제로 그 결함을 이 축으로
128
+ * 고쳤다. 즉 장식이 아니라 정체성을 지탱한다.
129
+ *
130
+ * 구조가 `partial` 인 이유: 서브로트는 로트와 같은 모양이고(위 참조) 그 로트가 `partial` 이다.
131
+ * 화면이 `partial` 인 이유: 값이 물품의 **이름으로** 나온다(`twin-item` 의 line) — 「이 로트가 자리마다
132
+ * 나뉘어 있다」를 말하는 자리는 아직 없다.
133
+ *
134
+ * 이 부류(코드에 있는데 표가 `none`)는 자동으로 잡히지 않는다 — 표의 가드는 반대 방향만 본다
135
+ * (축이 사라지면 구조를 내린다). 그래서 축을 늘릴 때 이 표를 함께 보는 것이 규율이다.
136
+ */
137
+ { std: 'MaterialSublot', part: '2', label: 'twin.isa95.MaterialSublot', axis: null, structure: 'partial', behavior: 'full', surface: 'partial' },
108
138
  { std: 'ProcessSegment', part: '2', label: 'twin.isa95.ProcessSegment', axis: 'operations', structure: 'full', behavior: 'full', surface: 'full' },
109
139
  /*
110
140
  * 속성은 자원마다 붙는 확장이라 **자기 축이 아니다** — 그래서 `axis` 는 없지만 구조는 완전하다:
@@ -114,7 +144,30 @@ const PART2: Isa95Concept[] = [
114
144
  * 기능은 **부분**이다: 호스트의 주행 추정기가 `speed` 를 읽어 이동 시간을 만드는 것이 유일한 소비처다
115
145
  * (다른 속성은 아직 아무도 읽지 않는다). 그 사실을 `full` 로 올리면 표가 거짓이 된다.
116
146
  */
117
- { std: 'Property', part: '2', label: 'twin.isa95.Property', axis: null, structure: 'full', behavior: 'partial', surface: 'full', note: 'twin.isa95.note.property' }
147
+ { std: 'Property', part: '2', label: 'twin.isa95.Property', axis: null, structure: 'full', behavior: 'partial', surface: 'full', note: 'twin.isa95.note.property' },
148
+ /*
149
+ * **자리의 상시 관측** — 2026-08-24 신설(커널 0.7.56). 표에 이 개념이 아예 없었다.
150
+ *
151
+ * 왜 이 축이 필요한가: 트윈의 판정 대상은 물건의 상태이고, 물건의 상태는 **조건 없이 정해지지
152
+ * 않는다.** 「이 로트가 냉장실에 있었다」까지만 아는 트윈은 그 로트가 괜찮았는지 말할 수 없다. 그리고
153
+ * **이 조인은 트윈만 할 수 있다** — 계측 시스템은 물건의 자리 이력을 모르고, 물류 시스템은 조건
154
+ * 이력을 모른다.
155
+ *
156
+ * 표준 앵커: `OperationsEventType`(B2MML-OperationsEvent.xsd, ISA-95.00.02-2018) +
157
+ * `OperationsRecordTemplateType`. 우리가 든 것 — `EffectiveTimestamp` · `EffectiveEndDate` ·
158
+ * `RecordTimestamp` · `HierarchyScope`(자리) · `Source` · 값은 `ValueType` 으로 **좁혔다**
159
+ * (단위 없는 물리량은 판정의 재료가 못 된다).
160
+ *
161
+ * 붙는 자리가 `HierarchyScope.EquipmentID` 인 것이 중요하다 — ISA-95 에서 장소 계층이 곧 설비
162
+ * 계층이고 냉장실은 `StorageZone` 수준의 설비다. `OperationalLocationType` 에는 이 축이 없다.
163
+ *
164
+ * 구조가 `partial`: 봉투 전체(`ID`·`Description`·사건 분류·`OperationsRecord` 구조)가 아니라 관측
165
+ * 하나를 담는 데 필요한 칸만 든다.
166
+ * 기능이 `full`: 사건 채널(`location.measured`)이 상태를 만들고, 선언된 기준으로 판정해
167
+ * `observation-out-of-limit` 신호를 세운다.
168
+ * 화면이 `none`: 클라이언트에 이 축을 그리는 곳이 없다(실측 0곳).
169
+ */
170
+ { std: 'OperationsEvent', part: '2', label: 'twin.isa95.OperationsEvent', axis: null, shownOn: ['locations'], structure: 'partial', behavior: 'full', surface: 'none' }
118
171
  ]
119
172
 
120
173
  /*
@@ -149,10 +202,37 @@ const PART4: Isa95Concept[] = [
149
202
  * 그것을 본다 — 만료·불합격이면 그 자격은 성립하지 않는다(실측: 만료된 용접사는 용접 작업 5건 중 한
150
203
  * 번도 배정되지 않았다).
151
204
  *
152
- * `full` 아닌 이유는 둘이다: **사람만 판정한다**(설비·자산의 검사 결과는 아직 배정에 쓰지 않는다),
153
- * 그리고 결과가 없으면 판정하지 않는다(선언한 것만 제약이 된다 그건 결손이 아니라 규율이다).
205
+ * ── 2026-08-24 문장 하나가 낡았다 ─────────────────────────────────────
206
+ * 「**사람만 판정한다**」고 적혀 있었다. 지금은 아니다`capabilityOfResource` 설비·사람·자산
207
+ * **셋 모두**에 `requiredTests` 를 걸고, 그 답이 배정의 가용성 필터로 들어간다. 그리고 판정 대상이
208
+ * 자원 밖으로 넓어졌다: **자리의 상시 관측**(`OperationsEvent` 줄)과 **로트의 시험 결과**
209
+ * (`TestResult` 줄)도 같은 기준으로 판정한다.
210
+ *
211
+ * 남은 규율은 하나다: **결과가 없으면 판정하지 않는다**(선언한 것만 제약이 된다 — 결손이 아니라 규율).
212
+ *
213
+ * 그리고 기준의 **숫자 한계는 우리가 더한 것**이다. 표준 `TestSpecificationCriteria.Expression` 은
214
+ * 자유 문장(`TextType`)이고, B2MML 일곱 파일에 `Minimum`·`Maximum`·`Tolerance` 가 하나도 없다
215
+ * (원문 대조). 그것 없이는 판정할 수 없으므로 `limit: {minimum, maximum, uom}` 을 더했고, 발명한
216
+ * 자리임을 계약에 적었다. 커널은 `Expression` 을 **읽지 않는다** — 문법이 정의되지 않았으므로
217
+ * 파싱하면 그 순간 방언이다.
154
218
  */
155
219
  { std: 'TestSpecification', part: '4', label: 'twin.isa95.TestSpecification', axis: 'testSpecifications', structure: 'full', behavior: 'full', surface: 'full', note: 'twin.isa95.note.test' },
220
+ /*
221
+ * **시험 결과** — 2026-08-24 신설(커널 0.7.57). 기준만 있고 결과의 자리가 표에 없었다.
222
+ *
223
+ * 표준 `TestResultType`(B2MML-OperationsTest.xsd)에서 우리가 든 것:
224
+ * `EvaluationDate`(at) · `Expiration`(expiresAt) · `TestableObjectID` · `PropertyMeasurement`
225
+ * · `EvaluatedCriterionResult` → **`result: 'pass'|'fail'` 로 좁혔다**(표준은 열거하지 않는다)
226
+ * 아직 없는 것: `ID` · `Description` · `HierarchyScope` · `OperationsTestRequirementID` ·
227
+ * `TestResultChild`. 그래서 구조는 `partial` 이다.
228
+ *
229
+ * 기능이 `partial` 인 이유를 정확히 적는다 — **자원에서는 결정을 바꾸고 로트에서는 아직 바꾸지
230
+ * 않는다.** 만료·불합격 자원에는 작업이 배정되지 않지만, 불합격 로트가 할당에서 빠지거나 주의 신호를
231
+ * 세우지는 않는다(들고·판정하고·되짚을 수는 있다). 그 자리를 `full` 로 적으면 표가 거짓이 된다.
232
+ *
233
+ * 화면은 `none` 이다 — 클라이언트에 이 축을 그리는 곳이 없다(실측: `operato-twin/client` 에 0곳).
234
+ */
235
+ { std: 'TestResult', part: '4', label: 'twin.isa95.TestResult', axis: null, shownOn: ['items'], structure: 'partial', behavior: 'partial', surface: 'none' },
156
236
  { std: 'WorkMaster', part: '4', label: 'twin.isa95.WorkMaster', axis: 'recipes', structure: 'full', behavior: 'full', surface: 'full' },
157
237
  { std: 'WorkDirective', part: '4', label: 'twin.isa95.WorkDirective', axis: null, structure: 'none', behavior: 'none', surface: 'none' }
158
238
  ]
@@ -166,20 +246,33 @@ export const ISA95_CONCEPTS: Isa95Concept[] = [...PART2, ...PART4]
166
246
  * `axis` 를 커널 축 목록과 맞춰 본다: 축이 있다고 적혀 있는데 커널에 없으면 그 선언이 낡은 것이므로
167
247
  * **구조 없음으로 낮추고 그 사실을 말한다**(조용히 통과시키면 이 표가 거짓이 된다).
168
248
  */
249
+ /**
250
+ * 선언한 축을 커널의 축 목록과 맞춘다 — **어긋난 것을 조용히 지우지 않는다.**
251
+ *
252
+ * 두 자리가 어긋날 수 있다.
253
+ * · `axis` — 그 개념 자신의 문. 커널에 없으면 구조를 「없음」으로 낮추고 그 사실을 말한다.
254
+ * · `shownOn` — 그 사실을 들고 있는 다른 축. 예전에는 없는 축을 **말없이 걸러 냈다.** 그러면 그 줄은
255
+ * 「개념 축이 없습니다」로 보이고, 사람은 그것을 **원래 그런 것**으로 읽는다. 실제로 그 일이 있었다:
256
+ * EPCIS 표의 일곱 줄이 커널에 없는 축(`items`)에 기대고 있었는데, 경고가 뜬 것은 두 줄뿐이었다.
257
+ *
258
+ * 둘 중 하나라도 어긋나면 `axisMissing` 이다 — 표가 스스로 「최신이 아니다」라고 말하게 한다.
259
+ */
260
+ export function reconcileAxes(c: Isa95Concept, known: Set<string>): Isa95Concept & { axisMissing?: boolean } {
261
+ const ownMissing = !!c.axis && !known.has(c.axis)
262
+ const shownOn = c.shownOn?.filter(a => known.has(a))
263
+ const lostShown = !!c.shownOn && (shownOn?.length ?? 0) !== c.shownOn.length
264
+ const fixed = lostShown ? { ...c, shownOn } : c
265
+ if (!ownMissing && !lostShown) return fixed
266
+ /* 구조를 낮추는 것은 **자기 문이 없어진 경우**다 — 남의 축에서 보이던 것은 구조가 아니라 표시의 문제다. */
267
+ return ownMissing ? { ...fixed, structure: 'none' as CoverageLevel, axisMissing: true } : { ...fixed, axisMissing: true }
268
+ }
269
+
169
270
  export function isa95Coverage(axes: string[]): {
170
271
  model: string
171
272
  concepts: (Isa95Concept & { axisMissing?: boolean })[]
172
273
  totals: { concepts: number; structure: number; behavior: number; surface: number }
173
274
  } {
174
- const known = new Set(axes)
175
- const concepts = ISA95_CONCEPTS.map(c => {
176
- const axisMissing = !!c.axis && !known.has(c.axis)
177
- /* 가리키는 곳이 없어진 축은 **가리키지 않는다** — 죽은 문으로 보내면 화면이 거짓말을 한다.
178
- `axis` 와 같은 규율이고, 여기가 비면 그 줄은 다시 "축이 없다" 로 정직하게 돌아간다. */
179
- const shownOn = c.shownOn?.filter(a => known.has(a))
180
- const fixed = shownOn && shownOn.length !== c.shownOn?.length ? { ...c, shownOn } : c
181
- return axisMissing ? { ...fixed, structure: 'none' as CoverageLevel, axisMissing } : fixed
182
- })
275
+ const concepts = ISA95_CONCEPTS.map(c => reconcileAxes(c, new Set(axes)))
183
276
  /* 채운 수는 `full` 만 센다 — 부분을 채운 것으로 세면 기준이 스스로를 속인다. */
184
277
  const count = (k: 'structure' | 'behavior' | 'surface') => concepts.filter(c => c[k] === 'full').length
185
278
  return {
@@ -0,0 +1,80 @@
1
+ /*
2
+ * **개체에서 품목으로** — 정체성 모양이 하나가 아니다 (2026-08-23).
3
+ *
4
+ * ── 무엇이 조용히 비어 있었나 ───────────────────────────────────────────────
5
+ * 화면이 「이 개체는 선언된 자재가 아닙니다(라인 내부의 중간 산물일 수 있습니다)」라고 말했는데, 그
6
+ * 품목은 원본의 선언에 이름까지 있었다(「냉동그린빈스」). 대응 표(`productionSpec.binding`)도 멀쩡했다.
7
+ *
8
+ * 원인은 **개체에서 품목을 꺼내는 규칙**이었다. 그 규칙이 GS1 SGTIN URN 하나만 알았다.
9
+ *
10
+ * urn:epc:id:sgtin:0614141.077101.42 → 077101 (알던 모양)
11
+ * https://…/class/product-M0000438@M2R0001 → null (모르던 모양)
12
+ *
13
+ * 웹 URI 로 정체성을 말하는 원본에서는 언제나 `null` 이 나왔고, 그래서 **개체를 자재에 이을 수 없었다.**
14
+ * 그 결과가 화면 두 곳에서 조용한 빈칸이었다(계보 적합성 · 오더가 무엇을 만드나). 「없으면 붙이지
15
+ * 않는다」는 규율을 지키기 때문에 아무 오류도 나지 않았다.
16
+ *
17
+ * ── 무엇을 돌려주나 ─────────────────────────────────────────────────────────
18
+ * **그 트윈의 대응 표가 쓰는 것과 같은 모양**이다. 두 세계가 서로 다른 것을 값으로 쓴다.
19
+ *
20
+ * SGTIN 세계 binding 값 = 품목 참조 숫자(`077101`) → 숫자를 돌려준다
21
+ * URI 세계 binding 값 = 클래스 URI(`…/product-M…`) → 클래스 URI 를 돌려준다
22
+ *
23
+ * 그래서 두 세계를 한 함수가 답한다 — 부르는 자리가 원본의 정체성 방식을 알지 않아도 된다.
24
+ *
25
+ * ── 무엇을 지어내지 않나 ────────────────────────────────────────────────────
26
+ * **어느 부호가 개체를 가리키는지 모른다.** 그것은 원본마다 다르고 커널 계약이 정한 적이 없다 — 한
27
+ * 원본(chef)이 쓰는 `@` 를 규칙으로 굳히면 이 화면이 그 레거시 위에 서게 된다. 자르는 판단은 그 트윈의
28
+ * **선언**이 하고(§`resolveByDeclaration`), 후보가 둘 이상이면 답하지 않는다. 짐작으로 이으면 남의
29
+ * 레시피와 비교하게 되고, 그것이 「설계와 다르다」는 거짓 진단이 된다.
30
+ */
31
+
32
+ /**
33
+ * 개체 식별자 → **품목 참조**. 대응 표의 값과 비교할 수 있는 모양으로 돌려준다.
34
+ *
35
+ * SGTIN 은 표준이 자리를 정해 두었으므로 그 자리에서 품목 참조를 꺼낸다. 그 밖의 모양은 **손대지
36
+ * 않는다** — 어느 부호가 개체를 가리키는지는 원본마다 다르고, 커널 계약이 그 구분자를 정한 적이 없다.
37
+ * 앞서 이 자리에 「`@` 뒤는 개체다」를 넣었는데, 그것은 한 원본(chef)의 모양을 규칙으로 굳힌 것이었다.
38
+ *
39
+ * 그래서 자르는 일은 여기서 하지 않고, **그 트윈의 선언과 비교해** 판정한다(§`resolveByDeclaration`).
40
+ * 선언이 답을 주지 못하면 `null` 이다 — 짐작으로 이으면 남의 레시피와 비교하게 되고, 그것이 「설계와
41
+ * 다르다」는 거짓 진단이 된다.
42
+ */
43
+ export function itemRefOf(epc: unknown): string | null {
44
+ if (typeof epc !== 'string') return null
45
+ const s = epc.trim()
46
+ if (!s) return null
47
+
48
+ /* GS1 SGTIN — 회사 접두사와 일련번호 사이의 품목 참조. `idpat` 의 `*` 도 같은 자리에서 걸린다. */
49
+ const sgtin = /sgtin:[^.]+\.([^.]+)\./.exec(s)
50
+ if (sgtin) return sgtin[1]
51
+
52
+ /* 그 밖의 정체성(웹 URI 등)은 그대로 낸다 — 자르는 판단은 선언이 한다. */
53
+ return s
54
+ }
55
+
56
+ /**
57
+ * 이 식별자가 가리키는 **선언 항목**을 찾는다 — 원본의 정체성 관행을 알지 않고, 구분자도 모른다.
58
+ *
59
+ * 세 가지를 차례로 본다. 어느 단계든 **후보가 둘 이상이면 답하지 않는다**(모르는 것이 틀린 답보다 낫다).
60
+ *
61
+ * ① 그대로 같다
62
+ * ② 관측된 식별자가 선언을 **확장**한다 — 개체는 자기 클래스로 시작한다(`…/product-M1` + 개체 표시)
63
+ * ③ 화면이 **짧게 줄인** 식별자를 건넸다 — 선언이 그것으로 끝난다(`net/class/product-M1`)
64
+ *
65
+ * ②③은 부호를 모른다. 그래서 원본이 `@`·`#`·`/` 무엇을 쓰든, 또는 아무것도 쓰지 않든 같은 규칙이 선다.
66
+ */
67
+ export function resolveByDeclaration<T>(ref: string | null, declared: Map<string, T>): T | undefined {
68
+ if (!ref) return undefined
69
+ const exact = declared.get(ref)
70
+ if (exact !== undefined) return exact
71
+ let hit: T | undefined
72
+ let found = 0
73
+ for (const [id, value] of declared) {
74
+ if (!(ref.startsWith(id) || id.endsWith(ref))) continue
75
+ found++
76
+ if (found > 1) return undefined
77
+ hit = value
78
+ }
79
+ return hit
80
+ }
@@ -0,0 +1,94 @@
1
+ /*
2
+ * 항목의 **이름**을 붙이는 자리 — 화면이 id 만 보이지 않게. (2026-08-22 공용으로 옮김)
3
+ *
4
+ * 이 둘은 항목 질의 안에 사적으로 있었다. 그런데 **전개(BOM 나무) 화면도 같은 이름이 필요하다** —
5
+ * 사용자가 「각 노드의 식별자가 반복되어서 이해가 어렵다. 식별자와 이름이 보이면」이라고 했다.
6
+ * 두 벌을 만들면 한쪽만 고쳐지므로 한 곳에 둔다(재발명 금지).
7
+ */
8
+ /* 커널 런타임 로드 — 어느 축이 있는지는 카탈로그가 선언한다(이 파일은 도메인 모양을 알지 않는다). */
9
+ const { TWIN_AXES, axisSource } = require('@operato/twin-kernel')
10
+
11
+ /** `productionSpec.definition.recipes` 같은 경로를 문서에서 따라간다. */
12
+ export const at = (o: any, path: string) => path.split('.').reduce((a: any, k: string) => (a == null ? undefined : a[k]), o)
13
+
14
+ /**
15
+ * 문서 항목의 **식별자** — 축마다 이름이 다르다(자리는 `id`, 공정은 `key`, 레시피도 `key`).
16
+ *
17
+ * 도메인 모양을 코드가 아는 것이 아니라 **식별자 관례**를 아는 것이다. 후보를 순서대로 보고 첫
18
+ * 값을 쓴다. 하나도 없으면 그 항목은 가리킬 수 없으므로 `null` — 목록에서 빼지 않고 **id 없음**으로
19
+ * 낸다(조용히 빠지면 건수가 안 맞는다).
20
+ */
21
+ export const idOf = (o: any, info?: { idField?: string | string[] }): string | null => {
22
+ if (typeof o === 'string') return o
23
+ /*
24
+ * **무엇이 이 항목을 가리키는지는 선언이 말한다**(§`TwinAxisInfo.idField`).
25
+ *
26
+ * 예전에는 `id`·`key`·`gtin` 을 짐작했다. 그래서 `id` 가 없는 축(수요 구간 — 정체성이 구간 시작
27
+ * 시각이다)의 항목이 전부 「식별자 없음 — 참조할 수 없는 항목」으로 보였다. 값은 실재하는데
28
+ * 가리킬 수 없다고 말한 것이다.
29
+ */
30
+ /*
31
+ * 선언이 **차례**를 말할 수 있다(2026-08-22) — 정체성이 한 칸으로 정해지지 않는 축이 있다. 물품이
32
+ * 그렇다: 직렬 물품은 `epc` 가 유일하지만 비직렬 로트가 자리마다 나뉘면 `subLotId` 가 개체를
33
+ * 구별한다(같은 로트의 두 부분은 같은 `epc` 를 갖는다). 앞에서 값이 있는 첫 칸이 이긴다.
34
+ */
35
+ for (const f of info?.idField ? (Array.isArray(info.idField) ? info.idField : [info.idField]) : []) {
36
+ const v = o?.[f]
37
+ if (v !== undefined && v !== null) return String(v)
38
+ }
39
+ return o?.id ?? o?.key ?? o?.gtin ?? null
40
+ }
41
+
42
+
43
+ /**
44
+ * **id → 이름**, 모든 축을 한 표로.
45
+ *
46
+ * ── 왜 이 표가 필요한가 ──────────────────────────────────────────────────────
47
+ * 행은 다른 것을 **id 로** 가리킨다(사람의 `personnelClassIds`, 설비의 `homeLocationId`, 공정의
48
+ * `locationType`). 그 id 의 이름은 **가리켜진 축의 행**에 있으므로, 화면이 그것을 알려면 축을
49
+ * 하나씩 더 읽어야 한다 — 그래서 지금까지 표면마다 그냥 id 를 찍었다(`cutter-op`, `cut-station`).
50
+ * 표면을 하나씩 고치는 것은 끝이 없다: 새 화면은 처음부터 다시 id 를 찍는다.
51
+ *
52
+ * 그래서 **서버가 붙인다.** 서버는 이미 모델을 손에 들고 있고, 어느 축이 있는지는 커널 카탈로그가
53
+ * 선언한다(`TWIN_AXES`) — 도메인이 늘어도 이 함수는 그대로다.
54
+ *
55
+ * 값은 그 항목이 **자기 이름이라고 말한 것**이다(`name`·`label`·`description`). 그중 어떤 것은
56
+ * i18n 키이므로(등급의 `description`) 번역은 화면의 일로 남긴다. 이름이 없으면 표에 넣지 않는다 —
57
+ * 없는 것을 지어내지 않고, 그 경우 화면은 id 를 그대로 보인다.
58
+ */
59
+ export function nameIndex(model: any): Record<string, string> {
60
+ const out: Record<string, string> = {}
61
+ for (const info of TWIN_AXES as any[]) {
62
+ /* 관측 축(상태·저널)은 문서에 없다 — 경로가 없는 축을 읽으면 모델 루트를 훑는다. */
63
+ if (axisSource(info.axis) !== 'document') continue
64
+ const raw = at(model ?? {}, info.path)
65
+ const list = Array.isArray(raw) ? raw : raw ? [raw] : []
66
+ for (const o of list) {
67
+ const id = idOf(o)
68
+ if (!id || typeof o !== 'object') continue
69
+ const label = o.name ?? o.label ?? o.description
70
+ /* 이름이 id 와 같으면 표에 넣지 않는다 — 같은 값을 두 번 보이는 셈이다. */
71
+ if (typeof label === 'string' && label && label !== id && !out[id]) out[id] = label
72
+ }
73
+ }
74
+ return out
75
+ }
76
+
77
+
78
+ /**
79
+ * 이 응답이 **실제로 가리킨 것만** 골라 낸다.
80
+ *
81
+ * 모델 전체 표를 그대로 실으면 자리가 10만인 현장에서 목록 한 페이지에 10만 줄짜리 이름표가
82
+ * 따라온다. 나온 값에 걸리는 것만 싣는다 — 규모에서 성립하는 유일한 방식이다.
83
+ */
84
+ export function namesFor(rows: any[], index: Record<string, string>): Record<string, string> {
85
+ const out: Record<string, string> = {}
86
+ const take = (v: any, depth: number): void => {
87
+ if (v == null || depth > 4) return
88
+ if (typeof v === 'string') { if (index[v]) out[v] = index[v]; return }
89
+ if (Array.isArray(v)) { for (const x of v) take(x, depth + 1); return }
90
+ if (typeof v === 'object') for (const x of Object.values(v)) take(x, depth + 1)
91
+ }
92
+ for (const r of rows) { take(r?.fields ?? r, 0); if (typeof r?.id === 'string' && index[r.id]) out[r.id] = index[r.id] }
93
+ return out
94
+ }
@@ -43,7 +43,7 @@ export async function projectStructure(domainId: string, instanceId: string, mod
43
43
  if (!instance) throw new Error(`cannot project structure: twin instance "${instanceId}" is not registered`)
44
44
 
45
45
  /*
46
- * **같은 id 가 둘이면 그 사실을 말한다** — 제약 위반 문구를 사용자에게 던지지 않는다.
46
+ * **같은 id 가 둘이면 그 사실을 말한다** — 제약 위반 문구를 사용자에게 보이지 않는다.
47
47
  *
48
48
  * 실측(2026-08-18): 현장 값을 그 현장의 트윈 전부에 얹다가 한 트윈에서 투영이 터졌다. 올라온 것은
49
49
  * `SQLITE_CONSTRAINT: UNIQUE constraint failed: twin_equipment...` 뿐이라 **무엇이 잘못됐는지 아무도
@@ -0,0 +1,37 @@
1
+ /*
2
+ * **수 하나가 뜻을 바꾸는 자리** — 상태별로 갈라 센다 (2026-08-23).
3
+ *
4
+ * ── 무엇이 있었나 ───────────────────────────────────────────────────────────
5
+ * 미러가 완료된 작업까지 상태에 들고 있게 되면서(관측 흡수에서 `completed` 를 떨어뜨리지 않게 커널을
6
+ * 고친 뒤) 개념 지도의 작업 카드가 「0」에서 「2,820」이 되었다. 이 카드는 **상태의 인구조사**이므로
7
+ * 그 수가 맞다 — 그런데 사람은 그것을 「지금 할 일 2,820건」으로 읽는다.
8
+ *
9
+ * 수를 줄여 맞는 척하게 만들지 않는다(그러면 인구조사가 거짓이 된다). 대신 **무엇으로 이루어진
10
+ * 수인지** 함께 낸다: 「진행 중 28 · 완료 2,792」.
11
+ *
12
+ * ── 지키는 것 ───────────────────────────────────────────────────────────────
13
+ * · 낱말은 **커널이 실어 보낸 그대로**다. 여기서 묶거나 고쳐 부르지 않는다(화면이 라벨을 입히고,
14
+ * 라벨이 없으면 낱말이 그대로 보여 누락이 드러난다).
15
+ * · 상태를 말하지 않는 축은 **아무것도 내지 않는다.** 빈 표를 내면 화면이 「상태가 없는 축」과
16
+ * 「상태를 못 읽은 축」을 구별할 수 없다.
17
+ * · 한 번만 훑는다. 카드가 이미 같은 배열의 길이를 세므로 셈이 한 벌 늘지 않는다.
18
+ */
19
+
20
+ /** 상태 한 갈래와 그 건수. */
21
+ export interface StatusCount {
22
+ status: string
23
+ count: number
24
+ }
25
+
26
+ /** 이 축의 개체를 상태별로 센다. 상태가 없으면 빈 객체(필드를 만들지 않는다). */
27
+ export function statusTally(bucket: unknown): { statusCounts?: StatusCount[] } {
28
+ if (!Array.isArray(bucket) || !bucket.length) return {}
29
+ const tally = new Map<string, number>()
30
+ for (const e of bucket) {
31
+ const s = (e as any)?.status
32
+ if (typeof s !== 'string' || !s) continue
33
+ tally.set(s, (tally.get(s) ?? 0) + 1)
34
+ }
35
+ if (!tally.size) return {}
36
+ return { statusCounts: [...tally].map(([status, count]) => ({ status, count })).sort((x, y) => y.count - x.count) }
37
+ }
@@ -2,6 +2,8 @@ import { Arg, Ctx, Int, Query, Resolver } from 'type-graphql'
2
2
 
3
3
  import { ScalarObject, getRepository } from '@things-factory/shell'
4
4
 
5
+ import { itemRefOf, resolveByDeclaration } from './item-ref.js'
6
+
5
7
  import { TwinInstance } from '../twin-instance/twin-instance.js'
6
8
  import { TwinEvent } from '../twin-event/twin-event.js'
7
9
 
@@ -24,11 +26,6 @@ import { TwinEvent } from '../twin-event/twin-event.js'
24
26
  * 읽히고, 계보에서 그 오해는 리콜 범위를 좁게 잡는 결과로 이어진다.
25
27
  */
26
28
 
27
- /** GS1 SGTIN 의 **품목 참조** — 개체(serial)를 떼고 품목만 남긴다(`…sgtin:0614141.077101.42` → `077101`). */
28
- function itemRefOf(epc: string): string | null {
29
- const m = /sgtin:[^.]+\.([^.]+)\./.exec(epc)
30
- return m ? m[1] : null
31
- }
32
29
 
33
30
  @Resolver()
34
31
  export class TwinLineageQuery {
@@ -110,18 +107,50 @@ export class TwinLineageQuery {
110
107
  const spec: any = (inst.model as any)?.productionSpec
111
108
  const binding: Record<string, string> = spec?.binding ?? {}
112
109
  const refToMaterial = new Map(Object.entries(binding).map(([mat, ref]) => [String(ref), mat]))
113
- const materialOf = (e: string) => {
114
- const ref = itemRefOf(e)
115
- return ref ? refToMaterial.get(ref) ?? null : null
116
- }
110
+ /*
111
+ * ── **화면은 끝마디로 묻는다** (2026-08-23) ─────────────────────────────────
112
+ *
113
+ * 서랍의 포커스 식별자는 클래스의 끝마디다(`net/class/product-M0000438`). 대응 표의 값은 전체
114
+ * 식별자(`https://…/class/product-M0000438`)이므로 곧바로는 맞지 않고, 그래서 「선언된 자재가
115
+ * 아닙니다」가 떴다 — 그 품목은 대응 표에 있었다.
116
+ *
117
+ * 끝마디로도 찾되 **하나뿐일 때만** 인정한다. 둘 이상이 같은 끝마디를 가지면 어느 것인지 모르므로
118
+ * 아무 말도 하지 않는다 — 짐작으로 이으면 남의 레시피와 견주게 되고, 그것이 「설계와 다르다」는
119
+ * 거짓 진단이 된다.
120
+ */
121
+ const materialOf = (e: string) => resolveByDeclaration(itemRefOf(e), refToMaterial) ?? null
117
122
 
118
123
  const myMaterial = materialOf(epc)
119
124
  const recipes: any[] = spec?.definition?.recipes ?? []
120
125
  const recipe = myMaterial ? recipes.find((r: any) => (r.outputs ?? []).some((o: any) => o.material === myMaterial)) : undefined
121
126
 
127
+ /*
128
+ * ── 「모르는 개체」와 「대응이 없는 개체」는 다른 사실이다 (2026-08-23) ──────
129
+ *
130
+ * 화면이 「이 개체는 선언된 자재가 아닙니다(라인 내부의 중간 산물일 수 있습니다)」라고 말했는데,
131
+ * 그 품목은 원본의 품목 선언에 **이름까지 있었다**(「냉동그린빈스」). 바인딩 표가 그 품목을 담지
132
+ * 못한 것이고, 그것은 「중간 산물」과 전혀 다른 이야기다 — 사용자가 할 일도 다르다(원본을 볼 일이
133
+ * 아니라 대응을 고칠 일이다).
134
+ *
135
+ * 그래서 개체가 가리키는 품목이 **모델의 품목 선언에 있는지** 함께 본다. 이름은 그 자리에 있으니
136
+ * 함께 낸다(화면이 사람에게 무엇을 말하는지 알 수 있게).
137
+ */
138
+ const definitions: any[] = (inst.model as any)?.materialDefinitions ?? []
139
+ const defByRef = new Map(definitions.filter(d => d?.id).map(d => [String(d.id), d]))
140
+ /* 정의도 같은 문으로 되찾는다 — 규칙 하나, 후보가 둘 이상이면 답하지 않는다. */
141
+ const myRef = itemRefOf(epc)
142
+ const myDef = resolveByDeclaration(myRef, defByRef)
143
+
122
144
  let conformance: any = null
123
145
  if (!Object.keys(binding).length) {
124
146
  conformance = { comparable: false, reason: 'no-binding' }
147
+ } else if (!myMaterial && myDef) {
148
+ /* 원본은 이 품목을 안다 — 개체를 자재에 잇는 대응이 없어서 비교하지 못한다. */
149
+ conformance = {
150
+ comparable: false,
151
+ reason: 'item-known-not-bound',
152
+ ...(typeof myDef.name === 'string' && myDef.name ? { itemName: myDef.name } : {})
153
+ }
125
154
  } else if (!myMaterial) {
126
155
  conformance = { comparable: false, reason: 'item-not-bound' }
127
156
  } else if (!recipe) {