@things-factory/station 10.1.33

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 (185) hide show
  1. package/README.md +124 -0
  2. package/dist-client/index.d.ts +11 -0
  3. package/dist-client/index.js +13 -0
  4. package/dist-client/index.js.map +1 -0
  5. package/dist-client/station/scene/index.d.ts +11 -0
  6. package/dist-client/station/scene/index.js +12 -0
  7. package/dist-client/station/scene/index.js.map +1 -0
  8. package/dist-client/station/scene/station-context-binding.d.ts +25 -0
  9. package/dist-client/station/scene/station-context-binding.js +39 -0
  10. package/dist-client/station/scene/station-context-binding.js.map +1 -0
  11. package/dist-client/station/scene/station-context-scene.d.ts +28 -0
  12. package/dist-client/station/scene/station-context-scene.js +138 -0
  13. package/dist-client/station/scene/station-context-scene.js.map +1 -0
  14. package/dist-client/station/scene/station-counts-scene.d.ts +6 -0
  15. package/dist-client/station/scene/station-counts-scene.js +51 -0
  16. package/dist-client/station/scene/station-counts-scene.js.map +1 -0
  17. package/dist-client/station/scene/station-field-scene.d.ts +6 -0
  18. package/dist-client/station/scene/station-field-scene.js +89 -0
  19. package/dist-client/station/scene/station-field-scene.js.map +1 -0
  20. package/dist-client/station/scene/station-icons.d.ts +82 -0
  21. package/dist-client/station/scene/station-icons.js +131 -0
  22. package/dist-client/station/scene/station-icons.js.map +1 -0
  23. package/dist-client/station/scene/station-numpad-scene.d.ts +9 -0
  24. package/dist-client/station/scene/station-numpad-scene.js +144 -0
  25. package/dist-client/station/scene/station-numpad-scene.js.map +1 -0
  26. package/dist-client/station/scene/station-scene-style.d.ts +51 -0
  27. package/dist-client/station/scene/station-scene-style.js +108 -0
  28. package/dist-client/station/scene/station-scene-style.js.map +1 -0
  29. package/dist-client/station/scene/station-scene.d.ts +295 -0
  30. package/dist-client/station/scene/station-scene.js +68 -0
  31. package/dist-client/station/scene/station-scene.js.map +1 -0
  32. package/dist-client/station/scene/station-who-scene.d.ts +6 -0
  33. package/dist-client/station/scene/station-who-scene.js +48 -0
  34. package/dist-client/station/scene/station-who-scene.js.map +1 -0
  35. package/dist-client/station/scene/station-work-panel-scene.d.ts +28 -0
  36. package/dist-client/station/scene/station-work-panel-scene.js +37 -0
  37. package/dist-client/station/scene/station-work-panel-scene.js.map +1 -0
  38. package/dist-client/station/station-board-binding.d.ts +16 -0
  39. package/dist-client/station/station-board-binding.js +20 -0
  40. package/dist-client/station/station-board-binding.js.map +1 -0
  41. package/dist-client/station/station-context.d.ts +178 -0
  42. package/dist-client/station/station-context.js +30 -0
  43. package/dist-client/station/station-context.js.map +1 -0
  44. package/dist-client/station/station-palette.d.ts +34 -0
  45. package/dist-client/station/station-palette.js +119 -0
  46. package/dist-client/station/station-palette.js.map +1 -0
  47. package/dist-client/station/station-runtime.d.ts +47 -0
  48. package/dist-client/station/station-runtime.js +47 -0
  49. package/dist-client/station/station-runtime.js.map +1 -0
  50. package/dist-client/station/station-scene-context.d.ts +68 -0
  51. package/dist-client/station/station-scene-context.js +62 -0
  52. package/dist-client/station/station-scene-context.js.map +1 -0
  53. package/dist-client/station/station-screen-flow.d.ts +16 -0
  54. package/dist-client/station/station-screen-flow.js +58 -0
  55. package/dist-client/station/station-screen-flow.js.map +1 -0
  56. package/dist-client/station/station-tile-registry.d.ts +89 -0
  57. package/dist-client/station/station-tile-registry.js +86 -0
  58. package/dist-client/station/station-tile-registry.js.map +1 -0
  59. package/dist-client/station/station-work-panel-view.d.ts +23 -0
  60. package/dist-client/station/station-work-panel-view.js +125 -0
  61. package/dist-client/station/station-work-panel-view.js.map +1 -0
  62. package/dist-client/station/station-work-runtime.d.ts +47 -0
  63. package/dist-client/station/station-work-runtime.js +161 -0
  64. package/dist-client/station/station-work-runtime.js.map +1 -0
  65. package/dist-client/station/station-work-scenarios.d.ts +41 -0
  66. package/dist-client/station/station-work-scenarios.js +59 -0
  67. package/dist-client/station/station-work-scenarios.js.map +1 -0
  68. package/dist-client/station/task-counts.d.ts +32 -0
  69. package/dist-client/station/task-counts.js +26 -0
  70. package/dist-client/station/task-counts.js.map +1 -0
  71. package/dist-client/tsconfig.tsbuildinfo +1 -0
  72. package/dist-server/activities/activity-station-screen-control-withdrawal.d.ts +62 -0
  73. package/dist-server/activities/activity-station-screen-control-withdrawal.js +147 -0
  74. package/dist-server/activities/activity-station-screen-control-withdrawal.js.map +1 -0
  75. package/dist-server/activities/activity-station-screen-release.d.ts +81 -0
  76. package/dist-server/activities/activity-station-screen-release.js +147 -0
  77. package/dist-server/activities/activity-station-screen-release.js.map +1 -0
  78. package/dist-server/activities/index.d.ts +49 -0
  79. package/dist-server/activities/index.js +21 -0
  80. package/dist-server/activities/index.js.map +1 -0
  81. package/dist-server/index.d.ts +3 -0
  82. package/dist-server/index.js +8 -0
  83. package/dist-server/index.js.map +1 -0
  84. package/dist-server/service/index.d.ts +23 -0
  85. package/dist-server/service/index.js +41 -0
  86. package/dist-server/service/index.js.map +1 -0
  87. package/dist-server/service/station/deletion-changes.d.ts +14 -0
  88. package/dist-server/service/station/deletion-changes.js +25 -0
  89. package/dist-server/service/station/deletion-changes.js.map +1 -0
  90. package/dist-server/service/station/index.d.ts +13 -0
  91. package/dist-server/service/station/index.js +25 -0
  92. package/dist-server/service/station/index.js.map +1 -0
  93. package/dist-server/service/station/reversibility.d.ts +21 -0
  94. package/dist-server/service/station/reversibility.js +24 -0
  95. package/dist-server/service/station/reversibility.js.map +1 -0
  96. package/dist-server/service/station/screen-model-digest.d.ts +60 -0
  97. package/dist-server/service/station/screen-model-digest.js +97 -0
  98. package/dist-server/service/station/screen-model-digest.js.map +1 -0
  99. package/dist-server/service/station/staged-material.d.ts +62 -0
  100. package/dist-server/service/station/staged-material.js +44 -0
  101. package/dist-server/service/station/staged-material.js.map +1 -0
  102. package/dist-server/service/station/station-argument-sources.d.ts +31 -0
  103. package/dist-server/service/station/station-argument-sources.js +66 -0
  104. package/dist-server/service/station/station-argument-sources.js.map +1 -0
  105. package/dist-server/service/station/station-assignment-choice.d.ts +81 -0
  106. package/dist-server/service/station/station-assignment-choice.js +105 -0
  107. package/dist-server/service/station/station-assignment-choice.js.map +1 -0
  108. package/dist-server/service/station/station-assignment-resolver.d.ts +104 -0
  109. package/dist-server/service/station/station-assignment-resolver.js +426 -0
  110. package/dist-server/service/station/station-assignment-resolver.js.map +1 -0
  111. package/dist-server/service/station/station-assignment-service.d.ts +33 -0
  112. package/dist-server/service/station/station-assignment-service.js +64 -0
  113. package/dist-server/service/station/station-assignment-service.js.map +1 -0
  114. package/dist-server/service/station/station-axes.d.ts +9 -0
  115. package/dist-server/service/station/station-axes.js +16 -0
  116. package/dist-server/service/station/station-axes.js.map +1 -0
  117. package/dist-server/service/station/station-flow.d.ts +156 -0
  118. package/dist-server/service/station/station-flow.js +255 -0
  119. package/dist-server/service/station/station-flow.js.map +1 -0
  120. package/dist-server/service/station/station-generate.d.ts +76 -0
  121. package/dist-server/service/station/station-generate.js +178 -0
  122. package/dist-server/service/station/station-generate.js.map +1 -0
  123. package/dist-server/service/station/station-product.d.ts +123 -0
  124. package/dist-server/service/station/station-product.js +77 -0
  125. package/dist-server/service/station/station-product.js.map +1 -0
  126. package/dist-server/service/station/station-runtime-binding.d.ts +6 -0
  127. package/dist-server/service/station/station-runtime-binding.js +46 -0
  128. package/dist-server/service/station/station-runtime-binding.js.map +1 -0
  129. package/dist-server/service/station/station-screen-assignment.d.ts +94 -0
  130. package/dist-server/service/station/station-screen-assignment.js +154 -0
  131. package/dist-server/service/station/station-screen-assignment.js.map +1 -0
  132. package/dist-server/service/station/station-screen-control-withdrawal-resolver.d.ts +37 -0
  133. package/dist-server/service/station/station-screen-control-withdrawal-resolver.js +130 -0
  134. package/dist-server/service/station/station-screen-control-withdrawal-resolver.js.map +1 -0
  135. package/dist-server/service/station/station-screen-control-withdrawal.d.ts +104 -0
  136. package/dist-server/service/station/station-screen-control-withdrawal.js +128 -0
  137. package/dist-server/service/station/station-screen-control-withdrawal.js.map +1 -0
  138. package/dist-server/service/station/station-screen-draft-resolver.d.ts +47 -0
  139. package/dist-server/service/station/station-screen-draft-resolver.js +153 -0
  140. package/dist-server/service/station/station-screen-draft-resolver.js.map +1 -0
  141. package/dist-server/service/station/station-screen-layout.d.ts +119 -0
  142. package/dist-server/service/station/station-screen-layout.js +202 -0
  143. package/dist-server/service/station/station-screen-layout.js.map +1 -0
  144. package/dist-server/service/station/station-screen-list-resolver.d.ts +69 -0
  145. package/dist-server/service/station/station-screen-list-resolver.js +215 -0
  146. package/dist-server/service/station/station-screen-list-resolver.js.map +1 -0
  147. package/dist-server/service/station/station-screen-publish-subscriber.d.ts +22 -0
  148. package/dist-server/service/station/station-screen-publish-subscriber.js +90 -0
  149. package/dist-server/service/station/station-screen-publish-subscriber.js.map +1 -0
  150. package/dist-server/service/station/station-screen-release-gate.d.ts +66 -0
  151. package/dist-server/service/station/station-screen-release-gate.js +67 -0
  152. package/dist-server/service/station/station-screen-release-gate.js.map +1 -0
  153. package/dist-server/service/station/station-screen-release-record.d.ts +62 -0
  154. package/dist-server/service/station/station-screen-release-record.js +139 -0
  155. package/dist-server/service/station/station-screen-release-record.js.map +1 -0
  156. package/dist-server/service/station/station-screen-release-signature.d.ts +23 -0
  157. package/dist-server/service/station/station-screen-release-signature.js +32 -0
  158. package/dist-server/service/station/station-screen-release-signature.js.map +1 -0
  159. package/dist-server/service/station/station-screen-scorecard.d.ts +15 -0
  160. package/dist-server/service/station/station-screen-scorecard.js +35 -0
  161. package/dist-server/service/station/station-screen-scorecard.js.map +1 -0
  162. package/dist-server/service/station/station-screen-spec-resolver.d.ts +36 -0
  163. package/dist-server/service/station/station-screen-spec-resolver.js +202 -0
  164. package/dist-server/service/station/station-screen-spec-resolver.js.map +1 -0
  165. package/dist-server/service/station/station-screen-spec.d.ts +104 -0
  166. package/dist-server/service/station/station-screen-spec.js +114 -0
  167. package/dist-server/service/station/station-screen-spec.js.map +1 -0
  168. package/dist-server/service/station/station-screen-thumbnail.d.ts +11 -0
  169. package/dist-server/service/station/station-screen-thumbnail.js +36 -0
  170. package/dist-server/service/station/station-screen-thumbnail.js.map +1 -0
  171. package/dist-server/service/station/station-service-resolver.d.ts +27 -0
  172. package/dist-server/service/station/station-service-resolver.js +95 -0
  173. package/dist-server/service/station/station-service-resolver.js.map +1 -0
  174. package/dist-server/tsconfig.tsbuildinfo +1 -0
  175. package/package.json +44 -0
  176. package/shared/station-axes.d.ts +4 -0
  177. package/shared/station-axes.js +155 -0
  178. package/shared/station-screen-flow-check.d.ts +12 -0
  179. package/shared/station-screen-flow-check.js +74 -0
  180. package/things-scene.config.js +15 -0
  181. package/translations/en.json +42 -0
  182. package/translations/ja.json +42 -0
  183. package/translations/ko.json +42 -0
  184. package/translations/ms.json +42 -0
  185. package/translations/zh.json +42 -0
@@ -0,0 +1,60 @@
1
+ /**
2
+ * 화면 도안 한 판의 **지문** — 서명이 어느 판에 걸린 것인지 가리는 값.
3
+ *
4
+ * ═══════════════════════════════════════════════════════════════════════════
5
+ * ── 왜 필요한가 ───────────────────────────────────────────────────────────
6
+ * 통제 문서는 **판마다** 승인받는다(ISO 9001 7.5.3). 그런데 서명이 화면(`boardId`)에만 걸려 있으면
7
+ * v1 을 승인한 서명 위로 **v2 가 그대로 나간다** — 통제가 이름만 남는 자리다.
8
+ *
9
+ * 번호로는 못 묶는다. `Board.version` 은 발행할 때 정해지고, 상신하는 시점에는 그 번호가 없다.
10
+ * 그래서 **그때 도안 자체**를 굳힌다 — `shownAtSigning` 이 결재자에게 보인 문장을 굳히는 것과 같다.
11
+ *
12
+ * ── 정규화가 이 값의 전부다 ───────────────────────────────────────────────
13
+ * 키 순서만 달라져도 지문이 바뀌면 **저장할 때마다 재결재**가 되고, 관문은 곧 소음이 되어 사람이
14
+ * 끄게 된다. 그래서 키를 정렬해 직렬화한 뒤 해시한다.
15
+ *
16
+ * **배열 순서는 안 건드린다.** 도안에서 순서는 뜻이다 — 겹치는 순서가 바뀌면 작업자가 보는 것이
17
+ * 달라진다. 정렬하면 그 변화가 지문에서 사라진다.
18
+ *
19
+ * ── 한 자리에서만 만든다 ─────────────────────────────────────────────────
20
+ * 상신하는 쪽과 발행 관문이 **같은 함수**를 부른다. 두 벌로 두면 한쪽이 정규화를 조금 다르게 하는 날
21
+ * 모든 서명이 낡은 것으로 읽히고, 아무도 이유를 모른다.
22
+ *
23
+ * ── 빼는 칸: **작업자가 화면에서 읽는 것이 아닌 칸** ──────────────────────
24
+ * 도안 안에 저장할 때마다(또는 한 번) 바뀌는 칸이 있으면 이 값이 달라지고, **내용이 안 바뀐 화면의
25
+ * 서명이 낡는다.** 기준은 하나다 — 그 칸이 작업자가 화면에서 읽는 것을 바꾸는가.
26
+ *
27
+ * ```
28
+ * threed 뺀다 2D/3D 토글의 보기 상태. 3D 로 보다가 저장하면 읽는 것은 그대로인데 지문이 바뀐다
29
+ * refid 뺀다 things-scene 이 없을 때 한 번 매겨 저장하는 식별 색인
30
+ * (RootContainer.addRefidIndex → set('refid'); 있으면 그대로 둔다)
31
+ * 세션마다 바뀌진 않지만 **저작면 도안이 편집기를 처음 지날 때** 붙어
32
+ * 지문이 한 번 바뀐다. 부품마다 붙으므로 **재귀로** 뺀다
33
+ * stationScenario 뺀다 어느 시나리오가 만들었나 — **출처**다. 'rework' 'packing' 같은 값이고
34
+ * 작업자는 이것을 안 읽는다(실측: spec 붙은 화면 11개에 있다)
35
+ * stationScenarioVersion 뺀다 그 출처의 판. 지금 전부 1이고, **시드를 다시 돌려 2가 되면 그려진 것은
36
+ * 하나도 안 바뀌는데 서명이 전부 낡는다**(실측: 10개)
37
+ * type · view · counts 남긴다 구조 · 묶음 타일 종류 · 양품/불량 — 작업자가 읽거나 만지는 내용이다
38
+ * removable 남긴다 줄을 뺄 수 있나 — 작업자가 만지는 것이다
39
+ * ```
40
+ *
41
+ * **값어치로 예외를 두지 않는다.** 「이건 중요하니까 세자」가 한 번 들어오면 기준이 사라진다.
42
+ *
43
+ * ⚠ 이름으로 빼므로 **부품 자료 안에 같은 이름의 칸이 있으면 그것도 빠진다.** 지금 도안에는 없고,
44
+ * 그런 칸이 생기면 그때는 경로로 빼는 수밖에 없다.
45
+ *
46
+ * [합의했다] ADR-0047 ④ — Shop Floor 가 저장된 도안 둘(저작면 것 · 편집기를 지난 것)을 견줘 찾았다.
47
+ *
48
+ * ── 글자로 와도 받는다. **그것이 실제 경로다** ────────────────────────────
49
+ * `Board.model` 은 **문자열 칸**이다(text). 그래서 상신하는 쪽이 읽어 넘기는 값도 글자이고, 그것을
50
+ * 그대로 해시하면 **정규화가 한 번도 안 돈다** — 같은 도안이라도 저장된 글자의 키 순서나 들여쓰기가
51
+ * 달라지면 지문이 달라지고, 그 순간 모든 서명이 낡는다. 막으려고 만든 결함을 막는 코드가 그대로
52
+ * 만들어 내는 자리였다(2026-09-16에 실제로 그랬다).
53
+ *
54
+ * 그래서 글자면 **먼저 읽어서** 정규화한다. 못 읽는 글자는 지어내지 않고 글자 그대로 해시한다 —
55
+ * 여기서 던지면 발행 관문이 그 화면에서 멈춘다.
56
+ *
57
+ * [합의했다] operato-application/adr/ADR-0047.md ④
58
+ * ═══════════════════════════════════════════════════════════════════════════
59
+ */
60
+ export declare function screenModelDigest(model: unknown): string;
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.screenModelDigest = screenModelDigest;
4
+ const node_crypto_1 = require("node:crypto");
5
+ /**
6
+ * 화면 도안 한 판의 **지문** — 서명이 어느 판에 걸린 것인지 가리는 값.
7
+ *
8
+ * ═══════════════════════════════════════════════════════════════════════════
9
+ * ── 왜 필요한가 ───────────────────────────────────────────────────────────
10
+ * 통제 문서는 **판마다** 승인받는다(ISO 9001 7.5.3). 그런데 서명이 화면(`boardId`)에만 걸려 있으면
11
+ * v1 을 승인한 서명 위로 **v2 가 그대로 나간다** — 통제가 이름만 남는 자리다.
12
+ *
13
+ * 번호로는 못 묶는다. `Board.version` 은 발행할 때 정해지고, 상신하는 시점에는 그 번호가 없다.
14
+ * 그래서 **그때 도안 자체**를 굳힌다 — `shownAtSigning` 이 결재자에게 보인 문장을 굳히는 것과 같다.
15
+ *
16
+ * ── 정규화가 이 값의 전부다 ───────────────────────────────────────────────
17
+ * 키 순서만 달라져도 지문이 바뀌면 **저장할 때마다 재결재**가 되고, 관문은 곧 소음이 되어 사람이
18
+ * 끄게 된다. 그래서 키를 정렬해 직렬화한 뒤 해시한다.
19
+ *
20
+ * **배열 순서는 안 건드린다.** 도안에서 순서는 뜻이다 — 겹치는 순서가 바뀌면 작업자가 보는 것이
21
+ * 달라진다. 정렬하면 그 변화가 지문에서 사라진다.
22
+ *
23
+ * ── 한 자리에서만 만든다 ─────────────────────────────────────────────────
24
+ * 상신하는 쪽과 발행 관문이 **같은 함수**를 부른다. 두 벌로 두면 한쪽이 정규화를 조금 다르게 하는 날
25
+ * 모든 서명이 낡은 것으로 읽히고, 아무도 이유를 모른다.
26
+ *
27
+ * ── 빼는 칸: **작업자가 화면에서 읽는 것이 아닌 칸** ──────────────────────
28
+ * 도안 안에 저장할 때마다(또는 한 번) 바뀌는 칸이 있으면 이 값이 달라지고, **내용이 안 바뀐 화면의
29
+ * 서명이 낡는다.** 기준은 하나다 — 그 칸이 작업자가 화면에서 읽는 것을 바꾸는가.
30
+ *
31
+ * ```
32
+ * threed 뺀다 2D/3D 토글의 보기 상태. 3D 로 보다가 저장하면 읽는 것은 그대로인데 지문이 바뀐다
33
+ * refid 뺀다 things-scene 이 없을 때 한 번 매겨 저장하는 식별 색인
34
+ * (RootContainer.addRefidIndex → set('refid'); 있으면 그대로 둔다)
35
+ * 세션마다 바뀌진 않지만 **저작면 도안이 편집기를 처음 지날 때** 붙어
36
+ * 지문이 한 번 바뀐다. 부품마다 붙으므로 **재귀로** 뺀다
37
+ * stationScenario 뺀다 어느 시나리오가 만들었나 — **출처**다. 'rework' 'packing' 같은 값이고
38
+ * 작업자는 이것을 안 읽는다(실측: spec 붙은 화면 11개에 있다)
39
+ * stationScenarioVersion 뺀다 그 출처의 판. 지금 전부 1이고, **시드를 다시 돌려 2가 되면 그려진 것은
40
+ * 하나도 안 바뀌는데 서명이 전부 낡는다**(실측: 10개)
41
+ * type · view · counts 남긴다 구조 · 묶음 타일 종류 · 양품/불량 — 작업자가 읽거나 만지는 내용이다
42
+ * removable 남긴다 줄을 뺄 수 있나 — 작업자가 만지는 것이다
43
+ * ```
44
+ *
45
+ * **값어치로 예외를 두지 않는다.** 「이건 중요하니까 세자」가 한 번 들어오면 기준이 사라진다.
46
+ *
47
+ * ⚠ 이름으로 빼므로 **부품 자료 안에 같은 이름의 칸이 있으면 그것도 빠진다.** 지금 도안에는 없고,
48
+ * 그런 칸이 생기면 그때는 경로로 빼는 수밖에 없다.
49
+ *
50
+ * [합의했다] ADR-0047 ④ — Shop Floor 가 저장된 도안 둘(저작면 것 · 편집기를 지난 것)을 견줘 찾았다.
51
+ *
52
+ * ── 글자로 와도 받는다. **그것이 실제 경로다** ────────────────────────────
53
+ * `Board.model` 은 **문자열 칸**이다(text). 그래서 상신하는 쪽이 읽어 넘기는 값도 글자이고, 그것을
54
+ * 그대로 해시하면 **정규화가 한 번도 안 돈다** — 같은 도안이라도 저장된 글자의 키 순서나 들여쓰기가
55
+ * 달라지면 지문이 달라지고, 그 순간 모든 서명이 낡는다. 막으려고 만든 결함을 막는 코드가 그대로
56
+ * 만들어 내는 자리였다(2026-09-16에 실제로 그랬다).
57
+ *
58
+ * 그래서 글자면 **먼저 읽어서** 정규화한다. 못 읽는 글자는 지어내지 않고 글자 그대로 해시한다 —
59
+ * 여기서 던지면 발행 관문이 그 화면에서 멈춘다.
60
+ *
61
+ * [합의했다] operato-application/adr/ADR-0047.md ④
62
+ * ═══════════════════════════════════════════════════════════════════════════
63
+ */
64
+ function screenModelDigest(model) {
65
+ return (0, node_crypto_1.createHash)('sha256').update(stable(parsed(model))).digest('hex');
66
+ }
67
+ /** 글자로 온 도안은 읽어서 본다. 못 읽으면 글자 그대로 — 던지지 않는다. */
68
+ function parsed(model) {
69
+ if (typeof model !== 'string') {
70
+ return model;
71
+ }
72
+ try {
73
+ return JSON.parse(model);
74
+ }
75
+ catch {
76
+ return model;
77
+ }
78
+ }
79
+ /** 보기 상태 · 색인 · 출처 — 읽는 것을 안 바꾸면서 지문을 흔드는 칸들. */
80
+ const NOT_WHAT_THE_OPERATOR_READS = new Set(['threed', 'refid', 'stationScenario', 'stationScenarioVersion']);
81
+ /** 키를 정렬해 직렬화한다. 배열은 그대로 — 순서가 뜻이다. */
82
+ function stable(value) {
83
+ if (value === null || typeof value !== 'object') {
84
+ return JSON.stringify(value ?? null);
85
+ }
86
+ if (Array.isArray(value)) {
87
+ return `[${value.map(stable).join(',')}]`;
88
+ }
89
+ const entries = Object.entries(value)
90
+ /* `undefined` 는 JSON 에서 사라진다 — 여기서도 없는 것으로 읽어 둘이 안 갈리게 한다. */
91
+ .filter(([, one]) => one !== undefined)
92
+ /* 작업자가 읽는 것이 아닌 칸은 빼고 센다(위 표). 부품 안에도 붙으므로 여기서 걸러야 재귀로 빠진다. */
93
+ .filter(([key]) => !NOT_WHAT_THE_OPERATOR_READS.has(key))
94
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
95
+ return `{${entries.map(([key, one]) => `${JSON.stringify(key)}:${stable(one)}`).join(',')}}`;
96
+ }
97
+ //# sourceMappingURL=screen-model-digest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screen-model-digest.js","sourceRoot":"","sources":["../../../server/service/station/screen-model-digest.ts"],"names":[],"mappings":";;AA6DA,8CAEC;AA/DD,6CAAwC;AAExC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AACH,SAAgB,iBAAiB,CAAC,KAAc;IAC9C,OAAO,IAAA,wBAAU,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AACzE,CAAC;AAED,gDAAgD;AAChD,SAAS,MAAM,CAAC,KAAc;IAC5B,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAA;IACd,CAAC;IAED,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED,iDAAiD;AACjD,MAAM,2BAA2B,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,OAAO,EAAE,iBAAiB,EAAE,wBAAwB,CAAC,CAAC,CAAA;AAE7G,uCAAuC;AACvC,SAAS,MAAM,CAAC,KAAc;IAC5B,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,IAAI,CAAC,CAAA;IACtC,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAA;IAC3C,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC;QAC9D,8DAA8D;SAC7D,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,KAAK,SAAS,CAAC;QACvC,+DAA+D;SAC9D,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,2BAA2B,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;SACxD,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAEnD,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAA;AAC9F,CAAC","sourcesContent":["import { createHash } from 'node:crypto'\n\n/**\n * 화면 도안 한 판의 **지문** — 서명이 어느 판에 걸린 것인지 가리는 값.\n *\n * ═══════════════════════════════════════════════════════════════════════════\n * ── 왜 필요한가 ───────────────────────────────────────────────────────────\n * 통제 문서는 **판마다** 승인받는다(ISO 9001 7.5.3). 그런데 서명이 화면(`boardId`)에만 걸려 있으면\n * v1 을 승인한 서명 위로 **v2 가 그대로 나간다** — 통제가 이름만 남는 자리다.\n *\n * 번호로는 못 묶는다. `Board.version` 은 발행할 때 정해지고, 상신하는 시점에는 그 번호가 없다.\n * 그래서 **그때 도안 자체**를 굳힌다 — `shownAtSigning` 이 결재자에게 보인 문장을 굳히는 것과 같다.\n *\n * ── 정규화가 이 값의 전부다 ───────────────────────────────────────────────\n * 키 순서만 달라져도 지문이 바뀌면 **저장할 때마다 재결재**가 되고, 관문은 곧 소음이 되어 사람이\n * 끄게 된다. 그래서 키를 정렬해 직렬화한 뒤 해시한다.\n *\n * **배열 순서는 안 건드린다.** 도안에서 순서는 뜻이다 — 겹치는 순서가 바뀌면 작업자가 보는 것이\n * 달라진다. 정렬하면 그 변화가 지문에서 사라진다.\n *\n * ── 한 자리에서만 만든다 ─────────────────────────────────────────────────\n * 상신하는 쪽과 발행 관문이 **같은 함수**를 부른다. 두 벌로 두면 한쪽이 정규화를 조금 다르게 하는 날\n * 모든 서명이 낡은 것으로 읽히고, 아무도 이유를 모른다.\n *\n * ── 빼는 칸: **작업자가 화면에서 읽는 것이 아닌 칸** ──────────────────────\n * 도안 안에 저장할 때마다(또는 한 번) 바뀌는 칸이 있으면 이 값이 달라지고, **내용이 안 바뀐 화면의\n * 서명이 낡는다.** 기준은 하나다 — 그 칸이 작업자가 화면에서 읽는 것을 바꾸는가.\n *\n * ```\n * threed 뺀다 2D/3D 토글의 보기 상태. 3D 로 보다가 저장하면 읽는 것은 그대로인데 지문이 바뀐다\n * refid 뺀다 things-scene 이 없을 때 한 번 매겨 저장하는 식별 색인\n * (RootContainer.addRefidIndex → set('refid'); 있으면 그대로 둔다)\n * 세션마다 바뀌진 않지만 **저작면 도안이 편집기를 처음 지날 때** 붙어\n * 지문이 한 번 바뀐다. 부품마다 붙으므로 **재귀로** 뺀다\n * stationScenario 뺀다 어느 시나리오가 만들었나 — **출처**다. 'rework' 'packing' 같은 값이고\n * 작업자는 이것을 안 읽는다(실측: spec 붙은 화면 11개에 있다)\n * stationScenarioVersion 뺀다 그 출처의 판. 지금 전부 1이고, **시드를 다시 돌려 2가 되면 그려진 것은\n * 하나도 안 바뀌는데 서명이 전부 낡는다**(실측: 10개)\n * type · view · counts 남긴다 구조 · 묶음 타일 종류 · 양품/불량 — 작업자가 읽거나 만지는 내용이다\n * removable 남긴다 줄을 뺄 수 있나 — 작업자가 만지는 것이다\n * ```\n *\n * **값어치로 예외를 두지 않는다.** 「이건 중요하니까 세자」가 한 번 들어오면 기준이 사라진다.\n *\n * ⚠ 이름으로 빼므로 **부품 자료 안에 같은 이름의 칸이 있으면 그것도 빠진다.** 지금 도안에는 없고,\n * 그런 칸이 생기면 그때는 경로로 빼는 수밖에 없다.\n *\n * [합의했다] ADR-0047 ④ — Shop Floor 가 저장된 도안 둘(저작면 것 · 편집기를 지난 것)을 견줘 찾았다.\n *\n * ── 글자로 와도 받는다. **그것이 실제 경로다** ────────────────────────────\n * `Board.model` 은 **문자열 칸**이다(text). 그래서 상신하는 쪽이 읽어 넘기는 값도 글자이고, 그것을\n * 그대로 해시하면 **정규화가 한 번도 안 돈다** — 같은 도안이라도 저장된 글자의 키 순서나 들여쓰기가\n * 달라지면 지문이 달라지고, 그 순간 모든 서명이 낡는다. 막으려고 만든 결함을 막는 코드가 그대로\n * 만들어 내는 자리였다(2026-09-16에 실제로 그랬다).\n *\n * 그래서 글자면 **먼저 읽어서** 정규화한다. 못 읽는 글자는 지어내지 않고 글자 그대로 해시한다 —\n * 여기서 던지면 발행 관문이 그 화면에서 멈춘다.\n *\n * [합의했다] operato-application/adr/ADR-0047.md ④\n * ═══════════════════════════════════════════════════════════════════════════\n */\nexport function screenModelDigest(model: unknown): string {\n return createHash('sha256').update(stable(parsed(model))).digest('hex')\n}\n\n/** 글자로 온 도안은 읽어서 본다. 못 읽으면 글자 그대로 — 던지지 않는다. */\nfunction parsed(model: unknown): unknown {\n if (typeof model !== 'string') {\n return model\n }\n\n try {\n return JSON.parse(model)\n } catch {\n return model\n }\n}\n\n/** 보기 상태 · 색인 · 출처 — 읽는 것을 안 바꾸면서 지문을 흔드는 칸들. */\nconst NOT_WHAT_THE_OPERATOR_READS = new Set(['threed', 'refid', 'stationScenario', 'stationScenarioVersion'])\n\n/** 키를 정렬해 직렬화한다. 배열은 그대로 — 순서가 뜻이다. */\nfunction stable(value: unknown): string {\n if (value === null || typeof value !== 'object') {\n return JSON.stringify(value ?? null)\n }\n\n if (Array.isArray(value)) {\n return `[${value.map(stable).join(',')}]`\n }\n\n const entries = Object.entries(value as Record<string, unknown>)\n /* `undefined` 는 JSON 에서 사라진다 — 여기서도 없는 것으로 읽어 둘이 안 갈리게 한다. */\n .filter(([, one]) => one !== undefined)\n /* 작업자가 읽는 것이 아닌 칸은 빼고 센다(위 표). 부품 안에도 붙으므로 여기서 걸러야 재귀로 빠진다. */\n .filter(([key]) => !NOT_WHAT_THE_OPERATOR_READS.has(key))\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n\n return `{${entries.map(([key, one]) => `${JSON.stringify(key)}:${stable(one)}`).join(',')}}`\n}\n"]}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * 스캔한 자재가 이 작업지시에 들어갈 것인가 — **순수 판단.**
3
+ *
4
+ * ── 오투입은 며칠 뒤에 드러난다 ────────────────────────────────────────────
5
+ * 비슷하게 생긴 원료를 잘못 넣으면 그날은 아무 일도 안 일어난다. 검사에서 걸리거나, 걸리지 않으면
6
+ * 고객이 찾아낸다. 막을 수 있는 자리는 **투입하는 순간**뿐이고, 그 순간에 시스템이 아는 것은 스캔한
7
+ * 라벨 하나다.
8
+ *
9
+ * ── 대조할 근거가 없으면 없다고 한다 ───────────────────────────────────────
10
+ * 레시피가 안 붙은 지시가 정상으로 있다. 그때 「맞다」고 하면 확인한 적 없는 것을 확인했다고 말하는
11
+ * 것이고, 「틀리다」고 하면 정상 작업을 막는다. 둘 다 아니고 **근거가 없다**고 말한다.
12
+ *
13
+ * ── 마스터에 없는 것과 이 지시가 안 쓰는 것은 다르다 ───────────────────────
14
+ * 앞은 라벨이 깨졌거나 등록이 안 된 것이고, 뒤는 **오투입**이다. 사람이 할 일이 다르므로 갈라 놓는다.
15
+ */
16
+ export type StagedVerdict =
17
+ /** 이 지시가 쓰는 자재다. */
18
+ {
19
+ kind: 'belongs';
20
+ materialName: string;
21
+ }
22
+ /**
23
+ * 마스터에는 있는데 **이 지시가 쓰지 않는다.** 오투입이다.
24
+ *
25
+ * `expected` 는 이 지시가 실제로 쓰는 것들 — 화면이 「이것 대신 이것들입니다」라고 말할 수 있다.
26
+ */
27
+ | {
28
+ kind: 'not-in-recipe';
29
+ materialName: string;
30
+ expected: string[];
31
+ }
32
+ /** 이 품번이 마스터에 없다. 라벨이 깨졌거나 등록이 안 된 것이다. */
33
+ | {
34
+ kind: 'unknown-item';
35
+ gtin: string;
36
+ }
37
+ /** 레시피가 안 붙어 있어 대조할 근거가 없다. 막지 않는다. */
38
+ | {
39
+ kind: 'no-recipe';
40
+ materialName: string;
41
+ };
42
+ export interface StagedInput {
43
+ /** 스캔에서 나온 품번(AI 01). */
44
+ gtin: string;
45
+ /** 그 품번으로 찾은 자재의 이름. 못 찾았으면 없다. */
46
+ materialName?: string;
47
+ /**
48
+ * 이 지시의 레시피가 쓰는 자재들 — `RecipePart.material`.
49
+ *
50
+ * 레시피가 안 붙었으면 없다. **빈 배열과 없는 것은 다르다** — 빈 배열은 「아무것도 안 쓰는 레시피」이고
51
+ * 그런 레시피는 없다시피 하지만, 없는 것은 「레시피가 없다」이다.
52
+ */
53
+ recipeInputs?: string[];
54
+ }
55
+ export declare function judgeStaged(input: StagedInput): StagedVerdict;
56
+ /**
57
+ * 레시피의 투입 목록에서 자재 이름만 뽑는다.
58
+ *
59
+ * 커널 `RecipePart` 는 `material`(자재 키)을 든다. 그 이름이 아닌 칸에서 값을 끌어오지 않는다 — 끌어오면
60
+ * 레시피 모양이 바뀌는 날 **조용히 빈 목록**이 되고, 그러면 모든 스캔이 「이 지시가 안 쓴다」가 된다.
61
+ */
62
+ export declare function recipeInputNames(inputs: Record<string, unknown>[] | undefined): string[] | undefined;
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ /**
3
+ * 스캔한 자재가 이 작업지시에 들어갈 것인가 — **순수 판단.**
4
+ *
5
+ * ── 오투입은 며칠 뒤에 드러난다 ────────────────────────────────────────────
6
+ * 비슷하게 생긴 원료를 잘못 넣으면 그날은 아무 일도 안 일어난다. 검사에서 걸리거나, 걸리지 않으면
7
+ * 고객이 찾아낸다. 막을 수 있는 자리는 **투입하는 순간**뿐이고, 그 순간에 시스템이 아는 것은 스캔한
8
+ * 라벨 하나다.
9
+ *
10
+ * ── 대조할 근거가 없으면 없다고 한다 ───────────────────────────────────────
11
+ * 레시피가 안 붙은 지시가 정상으로 있다. 그때 「맞다」고 하면 확인한 적 없는 것을 확인했다고 말하는
12
+ * 것이고, 「틀리다」고 하면 정상 작업을 막는다. 둘 다 아니고 **근거가 없다**고 말한다.
13
+ *
14
+ * ── 마스터에 없는 것과 이 지시가 안 쓰는 것은 다르다 ───────────────────────
15
+ * 앞은 라벨이 깨졌거나 등록이 안 된 것이고, 뒤는 **오투입**이다. 사람이 할 일이 다르므로 갈라 놓는다.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.judgeStaged = judgeStaged;
19
+ exports.recipeInputNames = recipeInputNames;
20
+ function judgeStaged(input) {
21
+ if (!input.materialName) {
22
+ return { kind: 'unknown-item', gtin: input.gtin };
23
+ }
24
+ if (!input.recipeInputs) {
25
+ return { kind: 'no-recipe', materialName: input.materialName };
26
+ }
27
+ if (input.recipeInputs.includes(input.materialName)) {
28
+ return { kind: 'belongs', materialName: input.materialName };
29
+ }
30
+ return { kind: 'not-in-recipe', materialName: input.materialName, expected: input.recipeInputs };
31
+ }
32
+ /**
33
+ * 레시피의 투입 목록에서 자재 이름만 뽑는다.
34
+ *
35
+ * 커널 `RecipePart` 는 `material`(자재 키)을 든다. 그 이름이 아닌 칸에서 값을 끌어오지 않는다 — 끌어오면
36
+ * 레시피 모양이 바뀌는 날 **조용히 빈 목록**이 되고, 그러면 모든 스캔이 「이 지시가 안 쓴다」가 된다.
37
+ */
38
+ function recipeInputNames(inputs) {
39
+ if (!inputs) {
40
+ return undefined;
41
+ }
42
+ return inputs.map(part => part.material).filter((material) => typeof material === 'string' && material.length > 0);
43
+ }
44
+ //# sourceMappingURL=staged-material.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"staged-material.js","sourceRoot":"","sources":["../../../server/service/station/staged-material.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;AA8BH,kCAcC;AAQD,4CAMC;AA5BD,SAAgB,WAAW,CAAC,KAAkB;IAC5C,IAAI,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC;QACxB,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAA;IACnD,CAAC;IAED,IAAI,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC;QACxB,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,YAAY,EAAE,KAAK,CAAC,YAAY,EAAE,CAAA;IAChE,CAAC;IAED,IAAI,KAAK,CAAC,YAAY,CAAC,QAAQ,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,CAAC;QACpD,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,CAAC,YAAY,EAAE,CAAA;IAC9D,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,YAAY,EAAE,KAAK,CAAC,YAAY,EAAE,QAAQ,EAAE,KAAK,CAAC,YAAY,EAAE,CAAA;AAClG,CAAC;AAED;;;;;GAKG;AACH,SAAgB,gBAAgB,CAAC,MAA6C;IAC5E,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,OAAO,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAsB,EAAE,CAAC,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;AACxI,CAAC","sourcesContent":["/**\n * 스캔한 자재가 이 작업지시에 들어갈 것인가 — **순수 판단.**\n *\n * ── 오투입은 며칠 뒤에 드러난다 ────────────────────────────────────────────\n * 비슷하게 생긴 원료를 잘못 넣으면 그날은 아무 일도 안 일어난다. 검사에서 걸리거나, 걸리지 않으면\n * 고객이 찾아낸다. 막을 수 있는 자리는 **투입하는 순간**뿐이고, 그 순간에 시스템이 아는 것은 스캔한\n * 라벨 하나다.\n *\n * ── 대조할 근거가 없으면 없다고 한다 ───────────────────────────────────────\n * 레시피가 안 붙은 지시가 정상으로 있다. 그때 「맞다」고 하면 확인한 적 없는 것을 확인했다고 말하는\n * 것이고, 「틀리다」고 하면 정상 작업을 막는다. 둘 다 아니고 **근거가 없다**고 말한다.\n *\n * ── 마스터에 없는 것과 이 지시가 안 쓰는 것은 다르다 ───────────────────────\n * 앞은 라벨이 깨졌거나 등록이 안 된 것이고, 뒤는 **오투입**이다. 사람이 할 일이 다르므로 갈라 놓는다.\n */\n\nexport type StagedVerdict =\n /** 이 지시가 쓰는 자재다. */\n | { kind: 'belongs'; materialName: string }\n /**\n * 마스터에는 있는데 **이 지시가 쓰지 않는다.** 오투입이다.\n *\n * `expected` 는 이 지시가 실제로 쓰는 것들 — 화면이 「이것 대신 이것들입니다」라고 말할 수 있다.\n */\n | { kind: 'not-in-recipe'; materialName: string; expected: string[] }\n /** 이 품번이 마스터에 없다. 라벨이 깨졌거나 등록이 안 된 것이다. */\n | { kind: 'unknown-item'; gtin: string }\n /** 레시피가 안 붙어 있어 대조할 근거가 없다. 막지 않는다. */\n | { kind: 'no-recipe'; materialName: string }\n\nexport interface StagedInput {\n /** 스캔에서 나온 품번(AI 01). */\n gtin: string\n /** 그 품번으로 찾은 자재의 이름. 못 찾았으면 없다. */\n materialName?: string\n /**\n * 이 지시의 레시피가 쓰는 자재들 — `RecipePart.material`.\n *\n * 레시피가 안 붙었으면 없다. **빈 배열과 없는 것은 다르다** — 빈 배열은 「아무것도 안 쓰는 레시피」이고\n * 그런 레시피는 없다시피 하지만, 없는 것은 「레시피가 없다」이다.\n */\n recipeInputs?: string[]\n}\n\nexport function judgeStaged(input: StagedInput): StagedVerdict {\n if (!input.materialName) {\n return { kind: 'unknown-item', gtin: input.gtin }\n }\n\n if (!input.recipeInputs) {\n return { kind: 'no-recipe', materialName: input.materialName }\n }\n\n if (input.recipeInputs.includes(input.materialName)) {\n return { kind: 'belongs', materialName: input.materialName }\n }\n\n return { kind: 'not-in-recipe', materialName: input.materialName, expected: input.recipeInputs }\n}\n\n/**\n * 레시피의 투입 목록에서 자재 이름만 뽑는다.\n *\n * 커널 `RecipePart` 는 `material`(자재 키)을 든다. 그 이름이 아닌 칸에서 값을 끌어오지 않는다 — 끌어오면\n * 레시피 모양이 바뀌는 날 **조용히 빈 목록**이 되고, 그러면 모든 스캔이 「이 지시가 안 쓴다」가 된다.\n */\nexport function recipeInputNames(inputs: Record<string, unknown>[] | undefined): string[] | undefined {\n if (!inputs) {\n return undefined\n }\n\n return inputs.map(part => part.material).filter((material): material is string => typeof material === 'string' && material.length > 0)\n}\n"]}
@@ -0,0 +1,31 @@
1
+ import type { StationArgumentSource } from './station-flow.js';
2
+ /**
3
+ * Where an argument's value would come from, if nobody said otherwise.
4
+ *
5
+ * ═══════════════════════════════════════════════════════════════════════════
6
+ * ## Why guess at all
7
+ *
8
+ * `reportProduction` takes fourteen fields and eleven of them have exactly one sensible answer —
9
+ * the equipment is the station's equipment, the operator is whoever scanned in, `status` is
10
+ * whatever the button means. Making an author answer fourteen questions to reach the two that are
11
+ * actually a decision is how a tool stops being used.
12
+ *
13
+ * ## Why it is a suggestion and not a rule
14
+ *
15
+ * Every one is overridable, and the composer shows what was chosen rather than hiding it. A guess
16
+ * the author cannot see or change is worse than no guess: the day it is wrong, the report is wrong
17
+ * and nothing on the screen says why.
18
+ *
19
+ * ## Why this is its own file
20
+ *
21
+ * It is a table of names and nothing else — no schema, no repositories, no entity imports. That
22
+ * keeps it testable on its own, which matters here more than usual: **every value in it is a name
23
+ * that lives in another file**, and a wrong one produces no error at all. The field simply goes out
24
+ * empty, and a production report arrives with no equipment on it.
25
+ * ═══════════════════════════════════════════════════════════════════════════
26
+ */
27
+ export declare function suggestSource(field: string): StationArgumentSource | undefined;
28
+ /** The axes this table points at — the tests check every one of them is real. */
29
+ export declare function suggestedAxes(): string[];
30
+ /** Which fields this table can answer — the composer greys those rows as 「automatic」. */
31
+ export declare function suggestableFields(): string[];
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.suggestSource = suggestSource;
4
+ exports.suggestedAxes = suggestedAxes;
5
+ exports.suggestableFields = suggestableFields;
6
+ /**
7
+ * Where an argument's value would come from, if nobody said otherwise.
8
+ *
9
+ * ═══════════════════════════════════════════════════════════════════════════
10
+ * ## Why guess at all
11
+ *
12
+ * `reportProduction` takes fourteen fields and eleven of them have exactly one sensible answer —
13
+ * the equipment is the station's equipment, the operator is whoever scanned in, `status` is
14
+ * whatever the button means. Making an author answer fourteen questions to reach the two that are
15
+ * actually a decision is how a tool stops being used.
16
+ *
17
+ * ## Why it is a suggestion and not a rule
18
+ *
19
+ * Every one is overridable, and the composer shows what was chosen rather than hiding it. A guess
20
+ * the author cannot see or change is worse than no guess: the day it is wrong, the report is wrong
21
+ * and nothing on the screen says why.
22
+ *
23
+ * ## Why this is its own file
24
+ *
25
+ * It is a table of names and nothing else — no schema, no repositories, no entity imports. That
26
+ * keeps it testable on its own, which matters here more than usual: **every value in it is a name
27
+ * that lives in another file**, and a wrong one produces no error at all. The field simply goes out
28
+ * empty, and a production report arrives with no equipment on it.
29
+ * ═══════════════════════════════════════════════════════════════════════════
30
+ */
31
+ function suggestSource(field) {
32
+ /* The step's own meaning settles these — a finish button knows it is finishing. */
33
+ if (STEP_DECIDES.has(field)) {
34
+ return { from: 'step' };
35
+ }
36
+ const axis = CONTEXT_AXIS[field];
37
+ return axis ? { from: 'context', axis } : undefined;
38
+ }
39
+ /** Settled by what the button means, not by anything the author types. */
40
+ const STEP_DECIDES = new Set(['status', 'startTime', 'endTime', 'at', 'fromTime', 'toTime']);
41
+ /**
42
+ * Field name → the context axis that answers it.
43
+ *
44
+ * The axis names are the ones `stationBindingNames()` lists. A name that is not an axis resolves
45
+ * to nothing at run time, so the tests check every one of these against that list rather than
46
+ * trusting it.
47
+ */
48
+ const CONTEXT_AXIS = {
49
+ name: 'task.name',
50
+ operationKey: 'station.operation',
51
+ jobOrderName: 'order.name',
52
+ locationName: 'location.name',
53
+ equipmentNames: 'station.equipment',
54
+ equipmentName: 'station.equipment',
55
+ personNames: 'operator.name',
56
+ testableObjectId: 'order.name'
57
+ };
58
+ /** The axes this table points at — the tests check every one of them is real. */
59
+ function suggestedAxes() {
60
+ return [...new Set(Object.values(CONTEXT_AXIS))];
61
+ }
62
+ /** Which fields this table can answer — the composer greys those rows as 「automatic」. */
63
+ function suggestableFields() {
64
+ return [...STEP_DECIDES, ...Object.keys(CONTEXT_AXIS)];
65
+ }
66
+ //# sourceMappingURL=station-argument-sources.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"station-argument-sources.js","sourceRoot":"","sources":["../../../server/service/station/station-argument-sources.ts"],"names":[],"mappings":";;AA2BA,sCASC;AAwBD,sCAEC;AAGD,8CAEC;AAjED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,SAAgB,aAAa,CAAC,KAAa;IACzC,mFAAmF;IACnF,IAAI,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5B,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;IACzB,CAAC;IAED,MAAM,IAAI,GAAG,YAAY,CAAC,KAAK,CAAC,CAAA;IAEhC,OAAO,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;AACrD,CAAC;AAED,0EAA0E;AAC1E,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC,CAAA;AAE5F;;;;;;GAMG;AACH,MAAM,YAAY,GAA2B;IAC3C,IAAI,EAAE,WAAW;IACjB,YAAY,EAAE,mBAAmB;IACjC,YAAY,EAAE,YAAY;IAC1B,YAAY,EAAE,eAAe;IAC7B,cAAc,EAAE,mBAAmB;IACnC,aAAa,EAAE,mBAAmB;IAClC,WAAW,EAAE,eAAe;IAC5B,gBAAgB,EAAE,YAAY;CAC/B,CAAA;AAED,iFAAiF;AACjF,SAAgB,aAAa;IAC3B,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;AAClD,CAAC;AAED,yFAAyF;AACzF,SAAgB,iBAAiB;IAC/B,OAAO,CAAC,GAAG,YAAY,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAA;AACxD,CAAC","sourcesContent":["import type { StationArgumentSource } from './station-flow.js'\n\n/**\n * Where an argument's value would come from, if nobody said otherwise.\n *\n * ═══════════════════════════════════════════════════════════════════════════\n * ## Why guess at all\n *\n * `reportProduction` takes fourteen fields and eleven of them have exactly one sensible answer —\n * the equipment is the station's equipment, the operator is whoever scanned in, `status` is\n * whatever the button means. Making an author answer fourteen questions to reach the two that are\n * actually a decision is how a tool stops being used.\n *\n * ## Why it is a suggestion and not a rule\n *\n * Every one is overridable, and the composer shows what was chosen rather than hiding it. A guess\n * the author cannot see or change is worse than no guess: the day it is wrong, the report is wrong\n * and nothing on the screen says why.\n *\n * ## Why this is its own file\n *\n * It is a table of names and nothing else — no schema, no repositories, no entity imports. That\n * keeps it testable on its own, which matters here more than usual: **every value in it is a name\n * that lives in another file**, and a wrong one produces no error at all. The field simply goes out\n * empty, and a production report arrives with no equipment on it.\n * ═══════════════════════════════════════════════════════════════════════════\n */\nexport function suggestSource(field: string): StationArgumentSource | undefined {\n /* The step's own meaning settles these — a finish button knows it is finishing. */\n if (STEP_DECIDES.has(field)) {\n return { from: 'step' }\n }\n\n const axis = CONTEXT_AXIS[field]\n\n return axis ? { from: 'context', axis } : undefined\n}\n\n/** Settled by what the button means, not by anything the author types. */\nconst STEP_DECIDES = new Set(['status', 'startTime', 'endTime', 'at', 'fromTime', 'toTime'])\n\n/**\n * Field name → the context axis that answers it.\n *\n * The axis names are the ones `stationBindingNames()` lists. A name that is not an axis resolves\n * to nothing at run time, so the tests check every one of these against that list rather than\n * trusting it.\n */\nconst CONTEXT_AXIS: Record<string, string> = {\n name: 'task.name',\n operationKey: 'station.operation',\n jobOrderName: 'order.name',\n locationName: 'location.name',\n equipmentNames: 'station.equipment',\n equipmentName: 'station.equipment',\n personNames: 'operator.name',\n testableObjectId: 'order.name'\n}\n\n/** The axes this table points at — the tests check every one of them is real. */\nexport function suggestedAxes(): string[] {\n return [...new Set(Object.values(CONTEXT_AXIS))]\n}\n\n/** Which fields this table can answer — the composer greys those rows as 「automatic」. */\nexport function suggestableFields(): string[] {\n return [...STEP_DECIDES, ...Object.keys(CONTEXT_AXIS)]\n}\n"]}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Which drawn screen a station gets — **the picking rule, and nothing else.**
3
+ *
4
+ * Pure on purpose: the rule is the part that will be argued about, and arguing about it needs to be
5
+ * possible without a database and a shift calendar.
6
+ *
7
+ * ── The rule ───────────────────────────────────────────────────────────────
8
+ * A screen declares constraints on the axes it cares about and leaves the rest open. It is a
9
+ * candidate when **every constraint it declared** matches the station. Among candidates, the more
10
+ * specific one wins — the same rule the kernel already uses where a recipe line beats a general
11
+ * material spec.
12
+ *
13
+ * ── Specificity is a partial order, not a ranking ──────────────────────────
14
+ * "More specific" means **constraining a strict superset of the axes**: a screen for
15
+ * `{workCenter, operation}` beats one for `{workCenter}`, because it says everything the other says and more.
16
+ *
17
+ * What it deliberately does not do is weigh axes against each other. `{equipment}` versus
18
+ * `{workCenter, operation}` — one names a single machine, the other two broader things, and any ordering
19
+ * between them would be a number somebody made up. Inventing that weight is how a rule becomes
20
+ * folklore that nobody can predict. So they are a **tie**, and a tie is decided by a priority
21
+ * somebody wrote down.
22
+ *
23
+ * ── A tie nobody resolved is reported, not hidden ──────────────────────────
24
+ * If two screens tie and no priority separates them, one still has to be drawn — but the answer says
25
+ * it was ambiguous and names the others. Quietly dropping one is the worst outcome: the factory sees
26
+ * a screen, cannot tell why that one, and changing the other has no effect.
27
+ */
28
+ /** The axes a screen can constrain. Everything is optional — absent means "any". */
29
+ export interface StationAssignmentSelector {
30
+ equipmentName?: string;
31
+ equipmentClassId?: string;
32
+ workCenterName?: string;
33
+ operationKey?: string;
34
+ routeKey?: string;
35
+ materialClassId?: string;
36
+ personnelClassId?: string;
37
+ }
38
+ /** One assignment: a board, and when to show it. */
39
+ export interface StationAssignmentRule extends StationAssignmentSelector {
40
+ name: string;
41
+ /** 그릴 화면의 이름. 판은 배정이 못 박았을 때만 있다. */
42
+ boardId: string;
43
+ /** Breaks a tie between equally specific screens. Higher wins. */
44
+ priority?: number;
45
+ }
46
+ /**
47
+ * What the station actually is right now — the same five axes, plus the classes it belongs to.
48
+ *
49
+ * The axis names are the contract's, not this factory's (ADR-0083 decision 5): `workCenterName`
50
+ * covers a production line, a storage zone and a yard area, which is what lets the warehouse select
51
+ * on the same axis without a word of ours in it.
52
+ */
53
+ export interface StationAssignmentSituation {
54
+ equipmentName?: string;
55
+ equipmentClassIds?: string[];
56
+ workCenterName?: string;
57
+ operationKey?: string;
58
+ routeKey?: string;
59
+ materialClassIds?: string[];
60
+ personnelClassIds?: string[];
61
+ }
62
+ export interface StationAssignmentChoice {
63
+ name: string;
64
+ boardId: string;
65
+ /** Which axes this screen constrained. The admin screen says "chosen because …" from this. */
66
+ matchedOn: (keyof StationAssignmentSelector)[];
67
+ /**
68
+ * Equally specific screens that nothing separated.
69
+ *
70
+ * Empty is the normal case. Non-empty means the factory has two answers to the same question and
71
+ * somebody has to write a priority — the screen drawn is deterministic but arbitrary.
72
+ */
73
+ ambiguousWith: string[];
74
+ }
75
+ /**
76
+ * Pick one. Nothing fits → nothing is drawn, and the caller says so rather than showing a default.
77
+ *
78
+ * A screen that appears when nothing was assigned teaches the factory that assignment does not
79
+ * matter, and then nobody trusts what they see.
80
+ */
81
+ export declare function chooseStationAssignment(rules: readonly StationAssignmentRule[], situation: StationAssignmentSituation): StationAssignmentChoice | undefined;
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ /**
3
+ * Which drawn screen a station gets — **the picking rule, and nothing else.**
4
+ *
5
+ * Pure on purpose: the rule is the part that will be argued about, and arguing about it needs to be
6
+ * possible without a database and a shift calendar.
7
+ *
8
+ * ── The rule ───────────────────────────────────────────────────────────────
9
+ * A screen declares constraints on the axes it cares about and leaves the rest open. It is a
10
+ * candidate when **every constraint it declared** matches the station. Among candidates, the more
11
+ * specific one wins — the same rule the kernel already uses where a recipe line beats a general
12
+ * material spec.
13
+ *
14
+ * ── Specificity is a partial order, not a ranking ──────────────────────────
15
+ * "More specific" means **constraining a strict superset of the axes**: a screen for
16
+ * `{workCenter, operation}` beats one for `{workCenter}`, because it says everything the other says and more.
17
+ *
18
+ * What it deliberately does not do is weigh axes against each other. `{equipment}` versus
19
+ * `{workCenter, operation}` — one names a single machine, the other two broader things, and any ordering
20
+ * between them would be a number somebody made up. Inventing that weight is how a rule becomes
21
+ * folklore that nobody can predict. So they are a **tie**, and a tie is decided by a priority
22
+ * somebody wrote down.
23
+ *
24
+ * ── A tie nobody resolved is reported, not hidden ──────────────────────────
25
+ * If two screens tie and no priority separates them, one still has to be drawn — but the answer says
26
+ * it was ambiguous and names the others. Quietly dropping one is the worst outcome: the factory sees
27
+ * a screen, cannot tell why that one, and changing the other has no effect.
28
+ */
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.chooseStationAssignment = chooseStationAssignment;
31
+ const SINGLE_AXES = ['equipmentName', 'workCenterName', 'operationKey', 'routeKey'];
32
+ const CLASS_AXES = [
33
+ ['equipmentClassId', 'equipmentClassIds'],
34
+ ['materialClassId', 'materialClassIds'],
35
+ ['personnelClassId', 'personnelClassIds']
36
+ ];
37
+ /** Which axes a rule constrains. Its specificity is this set, not its size. */
38
+ function constrainedAxes(rule) {
39
+ const axes = [];
40
+ for (const axis of SINGLE_AXES) {
41
+ if (rule[axis]) {
42
+ axes.push(axis);
43
+ }
44
+ }
45
+ for (const [axis] of CLASS_AXES) {
46
+ if (rule[axis]) {
47
+ axes.push(axis);
48
+ }
49
+ }
50
+ return axes;
51
+ }
52
+ /**
53
+ * Does every constraint this rule declared hold here.
54
+ *
55
+ * An axis the situation does not know is **not a match.** A screen asking for welders should not be
56
+ * drawn before anyone has scanned in — that is the moment it would matter most that we do not guess.
57
+ */
58
+ function fits(rule, situation) {
59
+ for (const axis of SINGLE_AXES) {
60
+ if (rule[axis] && rule[axis] !== situation[axis]) {
61
+ return false;
62
+ }
63
+ }
64
+ for (const [axis, membership] of CLASS_AXES) {
65
+ const wanted = rule[axis];
66
+ if (wanted && !(situation[membership] ?? []).includes(wanted)) {
67
+ return false;
68
+ }
69
+ }
70
+ return true;
71
+ }
72
+ function isSuperset(of, than) {
73
+ return than.every(axis => of.includes(axis)) && of.length > than.length;
74
+ }
75
+ /**
76
+ * Pick one. Nothing fits → nothing is drawn, and the caller says so rather than showing a default.
77
+ *
78
+ * A screen that appears when nothing was assigned teaches the factory that assignment does not
79
+ * matter, and then nobody trusts what they see.
80
+ */
81
+ function chooseStationAssignment(rules, situation) {
82
+ const candidates = rules.filter(rule => fits(rule, situation));
83
+ if (!candidates.length) {
84
+ return undefined;
85
+ }
86
+ const withAxes = candidates.map(rule => ({ rule, axes: constrainedAxes(rule) }));
87
+ /* Drop anything another candidate says strictly more than. */
88
+ const finalists = withAxes.filter(one => !withAxes.some(other => isSuperset(other.axes, one.axes)));
89
+ const best = finalists.reduce((winner, one) => {
90
+ const byPriority = (one.rule.priority ?? 0) - (winner.rule.priority ?? 0);
91
+ if (byPriority !== 0) {
92
+ return byPriority > 0 ? one : winner;
93
+ }
94
+ /* Same priority, neither more specific. Pick by name so the same factory sees the same screen. */
95
+ return one.rule.name < winner.rule.name ? one : winner;
96
+ });
97
+ const tied = finalists.filter(one => one.rule !== best.rule && (one.rule.priority ?? 0) === (best.rule.priority ?? 0));
98
+ return {
99
+ name: best.rule.name,
100
+ boardId: best.rule.boardId,
101
+ matchedOn: best.axes,
102
+ ambiguousWith: tied.map(one => one.rule.name).sort()
103
+ };
104
+ }
105
+ //# sourceMappingURL=station-assignment-choice.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"station-assignment-choice.js","sourceRoot":"","sources":["../../../server/service/station/station-assignment-choice.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Which drawn screen a station gets — **the picking rule, and nothing else.**\n *\n * Pure on purpose: the rule is the part that will be argued about, and arguing about it needs to be\n * possible without a database and a shift calendar.\n *\n * ── The rule ───────────────────────────────────────────────────────────────\n * A screen declares constraints on the axes it cares about and leaves the rest open. It is a\n * candidate when **every constraint it declared** matches the station. Among candidates, the more\n * specific one wins — the same rule the kernel already uses where a recipe line beats a general\n * material spec.\n *\n * ── Specificity is a partial order, not a ranking ──────────────────────────\n * \"More specific\" means **constraining a strict superset of the axes**: a screen for\n * `{workCenter, operation}` beats one for `{workCenter}`, because it says everything the other says and more.\n *\n * What it deliberately does not do is weigh axes against each other. `{equipment}` versus\n * `{workCenter, operation}` — one names a single machine, the other two broader things, and any ordering\n * between them would be a number somebody made up. Inventing that weight is how a rule becomes\n * folklore that nobody can predict. So they are a **tie**, and a tie is decided by a priority\n * somebody wrote down.\n *\n * ── A tie nobody resolved is reported, not hidden ──────────────────────────\n * If two screens tie and no priority separates them, one still has to be drawn — but the answer says\n * it was ambiguous and names the others. Quietly dropping one is the worst outcome: the factory sees\n * a screen, cannot tell why that one, and changing the other has no effect.\n */\n\n/** The axes a screen can constrain. Everything is optional — absent means \"any\". */\nexport interface StationAssignmentSelector {\n equipmentName?: string\n equipmentClassId?: string\n workCenterName?: string\n operationKey?: string\n routeKey?: string\n materialClassId?: string\n personnelClassId?: string\n}\n\n/** One assignment: a board, and when to show it. */\nexport interface StationAssignmentRule extends StationAssignmentSelector {\n name: string\n /** 그릴 화면의 이름. 판은 배정이 못 박았을 때만 있다. */\n boardId: string\n /** Breaks a tie between equally specific screens. Higher wins. */\n priority?: number\n}\n\n/**\n * What the station actually is right now — the same five axes, plus the classes it belongs to.\n *\n * The axis names are the contract's, not this factory's (ADR-0083 decision 5): `workCenterName`\n * covers a production line, a storage zone and a yard area, which is what lets the warehouse select\n * on the same axis without a word of ours in it.\n */\nexport interface StationAssignmentSituation {\n equipmentName?: string\n equipmentClassIds?: string[]\n workCenterName?: string\n operationKey?: string\n routeKey?: string\n materialClassIds?: string[]\n personnelClassIds?: string[]\n}\n\nexport interface StationAssignmentChoice {\n name: string\n boardId: string\n /** Which axes this screen constrained. The admin screen says \"chosen because …\" from this. */\n matchedOn: (keyof StationAssignmentSelector)[]\n /**\n * Equally specific screens that nothing separated.\n *\n * Empty is the normal case. Non-empty means the factory has two answers to the same question and\n * somebody has to write a priority — the screen drawn is deterministic but arbitrary.\n */\n ambiguousWith: string[]\n}\n\nconst SINGLE_AXES = ['equipmentName', 'workCenterName', 'operationKey', 'routeKey'] as const\n\nconst CLASS_AXES = [\n ['equipmentClassId', 'equipmentClassIds'],\n ['materialClassId', 'materialClassIds'],\n ['personnelClassId', 'personnelClassIds']\n] as const\n\n/** Which axes a rule constrains. Its specificity is this set, not its size. */\nfunction constrainedAxes(rule: StationAssignmentRule): (keyof StationAssignmentSelector)[] {\n const axes: (keyof StationAssignmentSelector)[] = []\n\n for (const axis of SINGLE_AXES) {\n if (rule[axis]) {\n axes.push(axis)\n }\n }\n\n for (const [axis] of CLASS_AXES) {\n if (rule[axis]) {\n axes.push(axis)\n }\n }\n\n return axes\n}\n\n/**\n * Does every constraint this rule declared hold here.\n *\n * An axis the situation does not know is **not a match.** A screen asking for welders should not be\n * drawn before anyone has scanned in — that is the moment it would matter most that we do not guess.\n */\nfunction fits(rule: StationAssignmentRule, situation: StationAssignmentSituation): boolean {\n for (const axis of SINGLE_AXES) {\n if (rule[axis] && rule[axis] !== situation[axis]) {\n return false\n }\n }\n\n for (const [axis, membership] of CLASS_AXES) {\n const wanted = rule[axis]\n\n if (wanted && !(situation[membership] ?? []).includes(wanted)) {\n return false\n }\n }\n\n return true\n}\n\nfunction isSuperset(of: (keyof StationAssignmentSelector)[], than: (keyof StationAssignmentSelector)[]): boolean {\n return than.every(axis => of.includes(axis)) && of.length > than.length\n}\n\n/**\n * Pick one. Nothing fits → nothing is drawn, and the caller says so rather than showing a default.\n *\n * A screen that appears when nothing was assigned teaches the factory that assignment does not\n * matter, and then nobody trusts what they see.\n */\nexport function chooseStationAssignment(rules: readonly StationAssignmentRule[], situation: StationAssignmentSituation): StationAssignmentChoice | undefined {\n const candidates = rules.filter(rule => fits(rule, situation))\n\n if (!candidates.length) {\n return undefined\n }\n\n const withAxes = candidates.map(rule => ({ rule, axes: constrainedAxes(rule) }))\n\n /* Drop anything another candidate says strictly more than. */\n const finalists = withAxes.filter(one => !withAxes.some(other => isSuperset(other.axes, one.axes)))\n\n const best = finalists.reduce((winner, one) => {\n const byPriority = (one.rule.priority ?? 0) - (winner.rule.priority ?? 0)\n\n if (byPriority !== 0) {\n return byPriority > 0 ? one : winner\n }\n\n /* Same priority, neither more specific. Pick by name so the same factory sees the same screen. */\n return one.rule.name < winner.rule.name ? one : winner\n })\n\n const tied = finalists.filter(one => one.rule !== best.rule && (one.rule.priority ?? 0) === (best.rule.priority ?? 0))\n\n return {\n name: best.rule.name,\n boardId: best.rule.boardId,\n matchedOn: best.axes,\n ambiguousWith: tied.map(one => one.rule.name).sort()\n }\n}\n"]}