@things-factory/headless-twin 10.0.6 → 10.0.8

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 (150) hide show
  1. package/README.md +37 -24
  2. package/dist-server/engine/entity-delta.d.ts +13 -1
  3. package/dist-server/engine/entity-delta.js +130 -16
  4. package/dist-server/engine/entity-delta.js.map +1 -1
  5. package/dist-server/engine/index.d.ts +4 -0
  6. package/dist-server/engine/index.js +4 -0
  7. package/dist-server/engine/index.js.map +1 -1
  8. package/dist-server/engine/kpi-fold.d.ts +20 -3
  9. package/dist-server/engine/kpi-fold.js +21 -4
  10. package/dist-server/engine/kpi-fold.js.map +1 -1
  11. package/dist-server/engine/kpi-query.d.ts +9 -0
  12. package/dist-server/engine/kpi-query.js +118 -19
  13. package/dist-server/engine/kpi-query.js.map +1 -1
  14. package/dist-server/engine/kpi-target.d.ts +141 -0
  15. package/dist-server/engine/kpi-target.js +137 -0
  16. package/dist-server/engine/kpi-target.js.map +1 -0
  17. package/dist-server/engine/live-attentions.d.ts +4 -2
  18. package/dist-server/engine/live-attentions.js +5 -1
  19. package/dist-server/engine/live-attentions.js.map +1 -1
  20. package/dist-server/engine/measured-estimator.d.ts +50 -0
  21. package/dist-server/engine/measured-estimator.js +78 -0
  22. package/dist-server/engine/measured-estimator.js.map +1 -0
  23. package/dist-server/engine/model-basis.d.ts +23 -0
  24. package/dist-server/engine/model-basis.js +100 -0
  25. package/dist-server/engine/model-basis.js.map +1 -0
  26. package/dist-server/engine/oee-accumulator.d.ts +2 -2
  27. package/dist-server/engine/oee-accumulator.js +4 -4
  28. package/dist-server/engine/oee-accumulator.js.map +1 -1
  29. package/dist-server/engine/spec-coverage.d.ts +49 -0
  30. package/dist-server/engine/spec-coverage.js +60 -0
  31. package/dist-server/engine/spec-coverage.js.map +1 -0
  32. package/dist-server/engine/structure-diff.d.ts +25 -0
  33. package/dist-server/engine/structure-diff.js +63 -0
  34. package/dist-server/engine/structure-diff.js.map +1 -0
  35. package/dist-server/engine/travel-estimator.d.ts +57 -0
  36. package/dist-server/engine/travel-estimator.js +120 -0
  37. package/dist-server/engine/travel-estimator.js.map +1 -0
  38. package/dist-server/engine/twin-engine.d.ts +153 -17
  39. package/dist-server/engine/twin-engine.js +473 -81
  40. package/dist-server/engine/twin-engine.js.map +1 -1
  41. package/dist-server/engine/warm-start.d.ts +5 -5
  42. package/dist-server/engine/warm-start.js +4 -4
  43. package/dist-server/engine/warm-start.js.map +1 -1
  44. package/dist-server/service/index.d.ts +4 -2
  45. package/dist-server/service/index.js +21 -14
  46. package/dist-server/service/index.js.map +1 -1
  47. package/dist-server/service/reference/reference-live.js +2 -1
  48. package/dist-server/service/reference/reference-live.js.map +1 -1
  49. package/dist-server/service/reference/reference-master.d.ts +307 -1
  50. package/dist-server/service/reference/reference-master.js +96 -8
  51. package/dist-server/service/reference/reference-master.js.map +1 -1
  52. package/dist-server/service/reference/reference-resolver.js +3 -3
  53. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  54. package/dist-server/service/reference/template-registry.d.ts +1 -1
  55. package/dist-server/service/reference/template-registry.js.map +1 -1
  56. package/dist-server/service/twin-attention/twin-attention-query.js +1 -1
  57. package/dist-server/service/twin-attention/twin-attention-query.js.map +1 -1
  58. package/dist-server/service/twin-control/twin-control-mutation.js +1 -1
  59. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  60. package/dist-server/service/twin-event/twin-event-keys.d.ts +1 -1
  61. package/dist-server/service/twin-event/twin-event-keys.js +3 -3
  62. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  63. package/dist-server/service/twin-event/twin-event.d.ts +11 -0
  64. package/dist-server/service/twin-event/twin-event.js +6 -1
  65. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  66. package/dist-server/service/twin-forecast/twin-forecast-query.js +50 -7
  67. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  68. package/dist-server/service/twin-instance/twin-instance.js +1 -1
  69. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  70. package/dist-server/service/twin-journal/twin-journal-query.d.ts +9 -0
  71. package/dist-server/service/twin-journal/twin-journal-query.js +54 -3
  72. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  73. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +1 -1
  74. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +4 -3
  75. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  76. package/dist-server/service/twin-space/twin-space-area.js +1 -1
  77. package/dist-server/service/twin-space/twin-space-area.js.map +1 -1
  78. package/dist-server/service/twin-space/twin-space-resolver.js +3 -3
  79. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  80. package/dist-server/service/twin-space/twin-space.d.ts +11 -0
  81. package/dist-server/service/twin-space/twin-space.js +5 -0
  82. package/dist-server/service/twin-space/twin-space.js.map +1 -1
  83. package/dist-server/service/twin-structure/index.d.ts +2 -0
  84. package/dist-server/service/twin-structure/index.js +6 -0
  85. package/dist-server/service/twin-structure/index.js.map +1 -0
  86. package/dist-server/service/twin-structure/twin-structure.d.ts +28 -0
  87. package/dist-server/service/twin-structure/twin-structure.js +97 -0
  88. package/dist-server/service/twin-structure/twin-structure.js.map +1 -0
  89. package/dist-server/service/twin-target/index.d.ts +4 -0
  90. package/dist-server/service/twin-target/index.js +8 -0
  91. package/dist-server/service/twin-target/index.js.map +1 -0
  92. package/dist-server/service/twin-target/twin-target-resolver.d.ts +7 -0
  93. package/dist-server/service/twin-target/twin-target-resolver.js +143 -0
  94. package/dist-server/service/twin-target/twin-target-resolver.js.map +1 -0
  95. package/dist-server/service/twin-target/twin-target.d.ts +25 -0
  96. package/dist-server/service/twin-target/twin-target.js +112 -0
  97. package/dist-server/service/twin-target/twin-target.js.map +1 -0
  98. package/dist-server/tsconfig.tsbuildinfo +1 -1
  99. package/package.json +7 -7
  100. package/server/engine/entity-delta.ts +135 -15
  101. package/server/engine/index.ts +4 -0
  102. package/server/engine/kpi-fold.ts +40 -8
  103. package/server/engine/kpi-query.ts +129 -19
  104. package/server/engine/kpi-target.ts +226 -0
  105. package/server/engine/live-attentions.ts +12 -2
  106. package/server/engine/measured-estimator.ts +91 -0
  107. package/server/engine/model-basis.ts +94 -0
  108. package/server/engine/oee-accumulator.ts +5 -5
  109. package/server/engine/spec-coverage.ts +85 -0
  110. package/server/engine/structure-diff.ts +88 -0
  111. package/server/engine/travel-estimator.ts +133 -0
  112. package/server/engine/twin-engine.ts +498 -84
  113. package/server/engine/warm-start.ts +8 -8
  114. package/server/service/index.ts +7 -0
  115. package/server/service/reference/reference-live.ts +2 -1
  116. package/server/service/reference/reference-master.ts +383 -10
  117. package/server/service/reference/reference-resolver.ts +3 -3
  118. package/server/service/reference/template-registry.ts +1 -1
  119. package/server/service/twin-attention/twin-attention-query.ts +1 -1
  120. package/server/service/twin-control/twin-control-mutation.ts +1 -1
  121. package/server/service/twin-event/twin-event-keys.ts +2 -2
  122. package/server/service/twin-event/twin-event.ts +15 -1
  123. package/server/service/twin-forecast/twin-forecast-query.ts +56 -8
  124. package/server/service/twin-instance/twin-instance.ts +1 -1
  125. package/server/service/twin-journal/twin-journal-query.ts +52 -4
  126. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +4 -3
  127. package/server/service/twin-space/twin-space-area.ts +1 -1
  128. package/server/service/twin-space/twin-space-resolver.ts +3 -3
  129. package/server/service/twin-space/twin-space.ts +14 -0
  130. package/server/service/twin-structure/index.ts +3 -0
  131. package/server/service/twin-structure/twin-structure.ts +93 -0
  132. package/server/service/twin-target/index.ts +5 -0
  133. package/server/service/twin-target/twin-target-resolver.ts +124 -0
  134. package/server/service/twin-target/twin-target.ts +97 -0
  135. package/test/capability-mapping.test.ts +5 -5
  136. package/test/duration-estimators.test.ts +144 -0
  137. package/test/entity-delta.test.ts +150 -19
  138. package/test/ingest-bench.test.ts +9 -9
  139. package/test/kpi-fold.test.ts +232 -3
  140. package/test/live-mirror-parity.test.ts +53 -19
  141. package/test/master-to-twin.test.ts +87 -13
  142. package/test/model-basis.test.ts +86 -0
  143. package/test/oee-accumulator.test.ts +9 -9
  144. package/test/scale-twin-bench.test.ts +22 -22
  145. package/test/spec-coverage.test.ts +113 -0
  146. package/test/streamline-e2e.test.ts +10 -10
  147. package/test/structure-revision-db.test.ts +310 -0
  148. package/test/twin-event-keys.test.ts +2 -2
  149. package/test/vocabulary-guard.test.ts +43 -0
  150. package/test/warm-start.test.ts +9 -9
@@ -0,0 +1,49 @@
1
+ /** 예측 자격 — 선언된 명세가 얼마나 덮는지에 따라 이 예측으로 정당하게 말할 수 있는 범위가 달라진다. */
2
+ export type ForecastQualification =
3
+ /** 모든 소요가 커널 기본 상수 → **상대 비교만**("자원 2대 더 넣으면 나아지나"). 절대 시각 주장 불가. */
4
+ 'relative'
5
+ /** 일부만 선언·실측 → 그 구간은 접지, 나머지는 상수. 혼합. */
6
+ | 'partial'
7
+ /** 소요 전부 선언 → 절대 예측 시도 가능. 단 **정확도는 이력이 확인한다**(선언 != 검증). */
8
+ | 'absolute-capable'
9
+ /**
10
+ * 소요 전부가 **이 현장의 이력에서 온 값**(추정기) → 가장 강한 근거. 선언은 사람이 적은 값이고
11
+ * 이것은 실제로 일어난 값이다. 그래도 "정확하다" 가 아니라 "관측에 접지됐다" 는 뜻이다.
12
+ */
13
+ | 'calibrated';
14
+ export interface SpecCoverageReport {
15
+ operations: {
16
+ kind: string;
17
+ duration: 'measured' | 'declared' | 'default';
18
+ variability?: string;
19
+ parameters: string[];
20
+ }[];
21
+ /** 이력·계산에서 온 소요(추정기) — 선언값보다 강한 근거. */
22
+ measuredDurations: number;
23
+ declaredDurations: number;
24
+ defaultDurations: number;
25
+ /** 변동(분포)을 가진 작업 종류 수. **0 이면 모든 소요가 고정값**이라 대기·병목이 과소평가된다. */
26
+ varyingDurations: number;
27
+ qualification: ForecastQualification;
28
+ }
29
+ /**
30
+ * 명세 자격 판정(순수) — 커널 보고를 받아 분류한다. 커널을 모르므로 테스트가 쉽다.
31
+ * 관측된 작업이 하나도 없으면(`operations` 빈 배열) 판정할 근거가 없으므로 **undefined**(모른다).
32
+ */
33
+ export declare function classifySpecCoverage(cov: {
34
+ operations?: {
35
+ kind: string;
36
+ duration: 'measured' | 'declared' | 'default';
37
+ variability?: string;
38
+ parameters?: string[];
39
+ }[];
40
+ measuredDurations?: number;
41
+ declaredDurations?: number;
42
+ defaultDurations?: number;
43
+ }): SpecCoverageReport | undefined;
44
+ /**
45
+ * 커널에서 읽는다 — **아직 이 API 가 없는 커널(발행 이전)이면 undefined.**
46
+ * 그때 `undefined` 는 "명세가 없다" 가 아니라 "모른다" 다. 소비처가 그 둘을 섞지 않도록 주석을 남긴다:
47
+ * 없는 것을 "전부 기본값" 이라고 단정하지 않는다(단정하려면 커널이 말해 줘야 한다).
48
+ */
49
+ export declare function readSpecCoverage(kernel: any): SpecCoverageReport | undefined;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ /*
3
+ * 시뮬 명세 자기보고 — **"이 예측으로 무엇을 말할 수 있나" 를 예측과 함께 내보낸다.**
4
+ *
5
+ * 배경(operato-twin design/plans/simulation-spec.md): 공정 소요·수율이 커널 소스의 상수였다. 이제
6
+ * 마스터가 명세를 실어 오면 커널이 그것으로 굴리고, `specCoverage()` 가 무엇을 선언값으로 무엇을
7
+ * 기본값으로 썼는지 보고한다. 그 보고를 **예측 응답에 붙이지 않으면** 받는 쪽은 상수로 굴린 숫자와
8
+ * 현장 명세로 굴린 숫자를 구별할 수 없다 — 똑같이 "p50 168" 로 보인다.
9
+ *
10
+ * 언어 중립: 여기서는 **코드와 수치만** 낸다(`qualification` + 카운트). 사람이 읽는 문장은 표현
11
+ * 계층(클라 i18next / AI 답변)이 만든다 — attention 과 같은 규율.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.classifySpecCoverage = classifySpecCoverage;
15
+ exports.readSpecCoverage = readSpecCoverage;
16
+ /**
17
+ * 명세 자격 판정(순수) — 커널 보고를 받아 분류한다. 커널을 모르므로 테스트가 쉽다.
18
+ * 관측된 작업이 하나도 없으면(`operations` 빈 배열) 판정할 근거가 없으므로 **undefined**(모른다).
19
+ */
20
+ function classifySpecCoverage(cov) {
21
+ const ops = cov?.operations;
22
+ if (!ops?.length)
23
+ return undefined;
24
+ const operations = ops.map(o => ({
25
+ kind: o.kind,
26
+ duration: o.duration,
27
+ ...(o.variability ? { variability: o.variability } : {}),
28
+ parameters: o.parameters ?? []
29
+ }));
30
+ const measuredDurations = cov.measuredDurations ?? operations.filter(o => o.duration === 'measured').length;
31
+ const declaredDurations = cov.declaredDurations ?? operations.filter(o => o.duration === 'declared').length;
32
+ const defaultDurations = cov.defaultDurations ?? operations.filter(o => o.duration === 'default').length;
33
+ const varyingDurations = operations.filter(o => o.variability && o.variability !== 'constant').length;
34
+ /* 사다리: 전부 이력 → calibrated · 상수 없음 → absolute-capable · 접지가 하나도 없음 → relative · 나머지 → partial. */
35
+ const grounded = measuredDurations + declaredDurations;
36
+ const qualification = grounded === 0
37
+ ? 'relative'
38
+ : defaultDurations > 0
39
+ ? 'partial'
40
+ : declaredDurations === 0
41
+ ? 'calibrated'
42
+ : 'absolute-capable';
43
+ return { operations, measuredDurations, declaredDurations, defaultDurations, varyingDurations, qualification };
44
+ }
45
+ /**
46
+ * 커널에서 읽는다 — **아직 이 API 가 없는 커널(발행 이전)이면 undefined.**
47
+ * 그때 `undefined` 는 "명세가 없다" 가 아니라 "모른다" 다. 소비처가 그 둘을 섞지 않도록 주석을 남긴다:
48
+ * 없는 것을 "전부 기본값" 이라고 단정하지 않는다(단정하려면 커널이 말해 줘야 한다).
49
+ */
50
+ function readSpecCoverage(kernel) {
51
+ if (typeof kernel?.specCoverage !== 'function')
52
+ return undefined;
53
+ try {
54
+ return classifySpecCoverage(kernel.specCoverage());
55
+ }
56
+ catch {
57
+ return undefined;
58
+ }
59
+ }
60
+ //# sourceMappingURL=spec-coverage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spec-coverage.js","sourceRoot":"","sources":["../../server/engine/spec-coverage.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;AA+BH,oDA6BC;AAOD,4CAOC;AA/CD;;;GAGG;AACH,SAAgB,oBAAoB,CAAC,GAKpC;IACC,MAAM,GAAG,GAAG,GAAG,EAAE,UAAU,CAAA;IAC3B,IAAI,CAAC,GAAG,EAAE,MAAM;QAAE,OAAO,SAAS,CAAA;IAClC,MAAM,UAAU,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC/B,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACxD,UAAU,EAAE,CAAC,CAAC,UAAU,IAAI,EAAE;KAC/B,CAAC,CAAC,CAAA;IACH,MAAM,iBAAiB,GAAG,GAAG,CAAC,iBAAiB,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,UAAU,CAAC,CAAC,MAAM,CAAA;IAC3G,MAAM,iBAAiB,GAAG,GAAG,CAAC,iBAAiB,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,UAAU,CAAC,CAAC,MAAM,CAAA;IAC3G,MAAM,gBAAgB,GAAG,GAAG,CAAC,gBAAgB,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,MAAM,CAAA;IACxG,MAAM,gBAAgB,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC,WAAW,KAAK,UAAU,CAAC,CAAC,MAAM,CAAA;IACrG,iGAAiG;IACjG,MAAM,QAAQ,GAAG,iBAAiB,GAAG,iBAAiB,CAAA;IACtD,MAAM,aAAa,GACjB,QAAQ,KAAK,CAAC;QACZ,CAAC,CAAC,UAAU;QACZ,CAAC,CAAC,gBAAgB,GAAG,CAAC;YACpB,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,iBAAiB,KAAK,CAAC;gBACvB,CAAC,CAAC,YAAY;gBACd,CAAC,CAAC,kBAAkB,CAAA;IAC5B,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,aAAa,EAAE,CAAA;AAChH,CAAC;AAED;;;;GAIG;AACH,SAAgB,gBAAgB,CAAC,MAAW;IAC1C,IAAI,OAAO,MAAM,EAAE,YAAY,KAAK,UAAU;QAAE,OAAO,SAAS,CAAA;IAChE,IAAI,CAAC;QACH,OAAO,oBAAoB,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC,CAAA;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC","sourcesContent":["/*\n * 시뮬 명세 자기보고 — **\"이 예측으로 무엇을 말할 수 있나\" 를 예측과 함께 내보낸다.**\n *\n * 배경(operato-twin design/plans/simulation-spec.md): 공정 소요·수율이 커널 소스의 상수였다. 이제\n * 마스터가 명세를 실어 오면 커널이 그것으로 굴리고, `specCoverage()` 가 무엇을 선언값으로 무엇을\n * 기본값으로 썼는지 보고한다. 그 보고를 **예측 응답에 붙이지 않으면** 받는 쪽은 상수로 굴린 숫자와\n * 현장 명세로 굴린 숫자를 구별할 수 없다 — 똑같이 \"p50 168\" 로 보인다.\n *\n * 언어 중립: 여기서는 **코드와 수치만** 낸다(`qualification` + 카운트). 사람이 읽는 문장은 표현\n * 계층(클라 i18next / AI 답변)이 만든다 — attention 과 같은 규율.\n */\n\n/** 예측 자격 — 선언된 명세가 얼마나 덮는지에 따라 이 예측으로 정당하게 말할 수 있는 범위가 달라진다. */\nexport type ForecastQualification =\n /** 모든 소요가 커널 기본 상수 → **상대 비교만**(\"자원 2대 더 넣으면 나아지나\"). 절대 시각 주장 불가. */\n | 'relative'\n /** 일부만 선언·실측 → 그 구간은 접지, 나머지는 상수. 혼합. */\n | 'partial'\n /** 소요 전부 선언 → 절대 예측 시도 가능. 단 **정확도는 이력이 확인한다**(선언 != 검증). */\n | 'absolute-capable'\n /**\n * 소요 전부가 **이 현장의 이력에서 온 값**(추정기) → 가장 강한 근거. 선언은 사람이 적은 값이고\n * 이것은 실제로 일어난 값이다. 그래도 \"정확하다\" 가 아니라 \"관측에 접지됐다\" 는 뜻이다.\n */\n | 'calibrated'\n\nexport interface SpecCoverageReport {\n operations: { kind: string; duration: 'measured' | 'declared' | 'default'; variability?: string; parameters: string[] }[]\n /** 이력·계산에서 온 소요(추정기) — 선언값보다 강한 근거. */\n measuredDurations: number\n declaredDurations: number\n defaultDurations: number\n /** 변동(분포)을 가진 작업 종류 수. **0 이면 모든 소요가 고정값**이라 대기·병목이 과소평가된다. */\n varyingDurations: number\n qualification: ForecastQualification\n}\n\n/**\n * 명세 자격 판정(순수) — 커널 보고를 받아 분류한다. 커널을 모르므로 테스트가 쉽다.\n * 관측된 작업이 하나도 없으면(`operations` 빈 배열) 판정할 근거가 없으므로 **undefined**(모른다).\n */\nexport function classifySpecCoverage(cov: {\n operations?: { kind: string; duration: 'measured' | 'declared' | 'default'; variability?: string; parameters?: string[] }[]\n measuredDurations?: number\n declaredDurations?: number\n defaultDurations?: number\n}): SpecCoverageReport | undefined {\n const ops = cov?.operations\n if (!ops?.length) return undefined\n const operations = ops.map(o => ({\n kind: o.kind,\n duration: o.duration,\n ...(o.variability ? { variability: o.variability } : {}),\n parameters: o.parameters ?? []\n }))\n const measuredDurations = cov.measuredDurations ?? operations.filter(o => o.duration === 'measured').length\n const declaredDurations = cov.declaredDurations ?? operations.filter(o => o.duration === 'declared').length\n const defaultDurations = cov.defaultDurations ?? operations.filter(o => o.duration === 'default').length\n const varyingDurations = operations.filter(o => o.variability && o.variability !== 'constant').length\n /* 사다리: 전부 이력 → calibrated · 상수 없음 → absolute-capable · 접지가 하나도 없음 → relative · 나머지 → partial. */\n const grounded = measuredDurations + declaredDurations\n const qualification: ForecastQualification =\n grounded === 0\n ? 'relative'\n : defaultDurations > 0\n ? 'partial'\n : declaredDurations === 0\n ? 'calibrated'\n : 'absolute-capable'\n return { operations, measuredDurations, declaredDurations, defaultDurations, varyingDurations, qualification }\n}\n\n/**\n * 커널에서 읽는다 — **아직 이 API 가 없는 커널(발행 이전)이면 undefined.**\n * 그때 `undefined` 는 \"명세가 없다\" 가 아니라 \"모른다\" 다. 소비처가 그 둘을 섞지 않도록 주석을 남긴다:\n * 없는 것을 \"전부 기본값\" 이라고 단정하지 않는다(단정하려면 커널이 말해 줘야 한다).\n */\nexport function readSpecCoverage(kernel: any): SpecCoverageReport | undefined {\n if (typeof kernel?.specCoverage !== 'function') return undefined\n try {\n return classifySpecCoverage(kernel.specCoverage())\n } catch {\n return undefined\n }\n}\n"]}
@@ -0,0 +1,25 @@
1
+ /** 종류별 증감 — "paint-booth +4" 처럼 읽힌다. */
2
+ export interface StructureCountChange {
3
+ key: string;
4
+ before: number;
5
+ after: number;
6
+ delta: number;
7
+ }
8
+ export interface StructureDiff {
9
+ /** 자리 종류별 증감(0 인 것은 빼고). */
10
+ locationTypes: StructureCountChange[];
11
+ /** 설비 종류별 증감(0 인 것은 빼고). */
12
+ equipmentKinds: StructureCountChange[];
13
+ locationsAdded: number;
14
+ locationsRemoved: number;
15
+ equipmentAdded: number;
16
+ equipmentRemoved: number;
17
+ /** 개수는 같은데 내용이 바뀐 경우(용량 조정·자리 이동 등)가 있는가. */
18
+ changedInPlace: boolean;
19
+ }
20
+ /**
21
+ * 두 구조의 차이. `before` 가 없으면 **최초 리비전**이라 비교 대상이 없다 — 전부 새로 생긴 것으로 센다.
22
+ *
23
+ * 아이디는 **집합으로** 비교한다(수만 세면 넷이 사라지고 넷이 생긴 교체를 "변화 없음" 으로 답한다).
24
+ */
25
+ export declare function diffStructures(before: any | undefined, after: any): StructureDiff;
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ /*
3
+ * 구조 이력의 **차이** — "2번째에 도장 부스가 넷 늘었다" 를 말할 수 있게.
4
+ *
5
+ * 리비전은 그 시절 공장을 통째로 담는다(재생하려면 전체 위상이 필요하다). 그런데 사람이 보고 싶은
6
+ * 것은 통째가 아니라 **무엇이 달라졌나** 다 — 62KB 짜리 보드 두 개를 나란히 놓고 눈으로 비교할 수는
7
+ * 없다. 그 한 줄을 여기서 만든다.
8
+ *
9
+ * 순수 함수인 이유: 같은 문장을 화면과 AI 가 함께 써야 한다. 한쪽에서만 계산하면 둘이 다른 말을 한다.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.diffStructures = diffStructures;
13
+ const countBy = (rows, key) => {
14
+ const out = new Map();
15
+ for (const r of rows ?? []) {
16
+ const k = key(r);
17
+ if (k)
18
+ out.set(k, (out.get(k) ?? 0) + 1);
19
+ }
20
+ return out;
21
+ };
22
+ const idsOf = (rows) => new Set((rows ?? []).map(r => r.id).filter((x) => !!x));
23
+ function changes(before, after) {
24
+ const keys = [...new Set([...before.keys(), ...after.keys()])].sort();
25
+ return keys
26
+ .map(key => ({ key, before: before.get(key) ?? 0, after: after.get(key) ?? 0, delta: (after.get(key) ?? 0) - (before.get(key) ?? 0) }))
27
+ .filter(c => c.delta !== 0);
28
+ }
29
+ /**
30
+ * 두 구조의 차이. `before` 가 없으면 **최초 리비전**이라 비교 대상이 없다 — 전부 새로 생긴 것으로 센다.
31
+ *
32
+ * 아이디는 **집합으로** 비교한다(수만 세면 넷이 사라지고 넷이 생긴 교체를 "변화 없음" 으로 답한다).
33
+ */
34
+ function diffStructures(before, after) {
35
+ const beforeLocations = before?.locations ?? [];
36
+ const beforeEquipment = before?.equipment ?? [];
37
+ const afterLocations = after?.locations ?? [];
38
+ const afterEquipment = after?.equipment ?? [];
39
+ const beforeLocationIds = idsOf(beforeLocations);
40
+ const afterLocationIds = idsOf(afterLocations);
41
+ const beforeEquipmentIds = idsOf(beforeEquipment);
42
+ const afterEquipmentIds = idsOf(afterEquipment);
43
+ const added = (a, b) => [...b].filter(x => !a.has(x)).length;
44
+ const locationsAdded = added(beforeLocationIds, afterLocationIds);
45
+ const locationsRemoved = added(afterLocationIds, beforeLocationIds);
46
+ const equipmentAdded = added(beforeEquipmentIds, afterEquipmentIds);
47
+ const equipmentRemoved = added(afterEquipmentIds, beforeEquipmentIds);
48
+ /*
49
+ * 아이디는 그대로인데 속성이 바뀐 경우 — 용량을 늘렸거나 설비를 옮겼다. 지문이 달라 새 리비전이
50
+ * 생겼는데 증감이 전부 0 이면, 화면이 "바뀐 게 없다" 고 답하게 된다. 그런 침묵을 막는다.
51
+ */
52
+ const changedInPlace = !locationsAdded && !locationsRemoved && !equipmentAdded && !equipmentRemoved && !!before;
53
+ return {
54
+ locationTypes: changes(countBy(beforeLocations, (n) => n.type), countBy(afterLocations, (n) => n.type)),
55
+ equipmentKinds: changes(countBy(beforeEquipment, (m) => m.kind), countBy(afterEquipment, (m) => m.kind)),
56
+ locationsAdded,
57
+ locationsRemoved,
58
+ equipmentAdded,
59
+ equipmentRemoved,
60
+ changedInPlace
61
+ };
62
+ }
63
+ //# sourceMappingURL=structure-diff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structure-diff.js","sourceRoot":"","sources":["../../server/engine/structure-diff.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 구조 이력의 **차이** — \"2번째에 도장 부스가 넷 늘었다\" 를 말할 수 있게.\n *\n * 리비전은 그 시절 공장을 통째로 담는다(재생하려면 전체 위상이 필요하다). 그런데 사람이 보고 싶은\n * 것은 통째가 아니라 **무엇이 달라졌나** 다 — 62KB 짜리 보드 두 개를 나란히 놓고 눈으로 비교할 수는\n * 없다. 그 한 줄을 여기서 만든다.\n *\n * 순수 함수인 이유: 같은 문장을 화면과 AI 가 함께 써야 한다. 한쪽에서만 계산하면 둘이 다른 말을 한다.\n */\n\n/** 종류별 증감 — \"paint-booth +4\" 처럼 읽힌다. */\nexport interface StructureCountChange {\n key: string\n before: number\n after: number\n delta: number\n}\n\nexport interface StructureDiff {\n /** 자리 종류별 증감(0 인 것은 빼고). */\n locationTypes: StructureCountChange[]\n /** 설비 종류별 증감(0 인 것은 빼고). */\n equipmentKinds: StructureCountChange[]\n locationsAdded: number\n locationsRemoved: number\n equipmentAdded: number\n equipmentRemoved: number\n /** 개수는 같은데 내용이 바뀐 경우(용량 조정·자리 이동 등)가 있는가. */\n changedInPlace: boolean\n}\n\nconst countBy = <T>(rows: readonly T[] | undefined, key: (r: T) => string | undefined): Map<string, number> => {\n const out = new Map<string, number>()\n for (const r of rows ?? []) {\n const k = key(r)\n if (k) out.set(k, (out.get(k) ?? 0) + 1)\n }\n return out\n}\n\nconst idsOf = (rows: readonly { id?: string }[] | undefined): Set<string> => new Set((rows ?? []).map(r => r.id).filter((x): x is string => !!x))\n\nfunction changes(before: Map<string, number>, after: Map<string, number>): StructureCountChange[] {\n const keys = [...new Set([...before.keys(), ...after.keys()])].sort()\n return keys\n .map(key => ({ key, before: before.get(key) ?? 0, after: after.get(key) ?? 0, delta: (after.get(key) ?? 0) - (before.get(key) ?? 0) }))\n .filter(c => c.delta !== 0)\n}\n\n/**\n * 두 구조의 차이. `before` 가 없으면 **최초 리비전**이라 비교 대상이 없다 — 전부 새로 생긴 것으로 센다.\n *\n * 아이디는 **집합으로** 비교한다(수만 세면 넷이 사라지고 넷이 생긴 교체를 \"변화 없음\" 으로 답한다).\n */\nexport function diffStructures(before: any | undefined, after: any): StructureDiff {\n const beforeLocations = before?.locations ?? []\n const beforeEquipment = before?.equipment ?? []\n const afterLocations = after?.locations ?? []\n const afterEquipment = after?.equipment ?? []\n\n const beforeLocationIds = idsOf(beforeLocations)\n const afterLocationIds = idsOf(afterLocations)\n const beforeEquipmentIds = idsOf(beforeEquipment)\n const afterEquipmentIds = idsOf(afterEquipment)\n\n const added = <T>(a: Set<T>, b: Set<T>) => [...b].filter(x => !a.has(x)).length\n const locationsAdded = added(beforeLocationIds, afterLocationIds)\n const locationsRemoved = added(afterLocationIds, beforeLocationIds)\n const equipmentAdded = added(beforeEquipmentIds, afterEquipmentIds)\n const equipmentRemoved = added(afterEquipmentIds, beforeEquipmentIds)\n\n /*\n * 아이디는 그대로인데 속성이 바뀐 경우 — 용량을 늘렸거나 설비를 옮겼다. 지문이 달라 새 리비전이\n * 생겼는데 증감이 전부 0 이면, 화면이 \"바뀐 게 없다\" 고 답하게 된다. 그런 침묵을 막는다.\n */\n const changedInPlace =\n !locationsAdded && !locationsRemoved && !equipmentAdded && !equipmentRemoved && !!before\n\n return {\n locationTypes: changes(countBy(beforeLocations, (n: any) => n.type), countBy(afterLocations, (n: any) => n.type)),\n equipmentKinds: changes(countBy(beforeEquipment, (m: any) => m.kind), countBy(afterEquipment, (m: any) => m.kind)),\n locationsAdded,\n locationsRemoved,\n equipmentAdded,\n equipmentRemoved,\n changedInPlace\n }\n}\n"]}
@@ -0,0 +1,57 @@
1
+ import type { MeasuredEstimate } from './measured-estimator.js';
2
+ /** 우리가 읽는 canonical 설비 속성 ID — 표준이 속성 ID 어휘를 정하지 않으므로 한 곳에서 정한다. */
3
+ export declare const EQUIPMENT_PROPERTY: {
4
+ readonly speed: "speed";
5
+ };
6
+ /** 추정기 문맥 — 커널 `DurationContext` 와 같은 모양(커널 타입에 의존하지 않기 위해 구조로 맞춘다). */
7
+ export interface DurationCtx {
8
+ kind: string;
9
+ fromNode: string;
10
+ toNode: string;
11
+ resourceKind?: string;
12
+ }
13
+ /**
14
+ * 추정기 — 숫자(평균만) 또는 추정치 객체(퍼짐까지). 커널 `DurationEstimator` 와 같은 모양.
15
+ * 거리 계산은 숫자를 내고, 실측은 관측된 퍼짐을 함께 낸다.
16
+ */
17
+ export interface DurationEstimatorLike {
18
+ estimate(ctx: DurationCtx): number | MeasuredEstimate | undefined;
19
+ }
20
+ export interface TravelEstimatorInput {
21
+ /** 노드 중심 좌표(masterToTwin 산출 `board.layout`). */
22
+ layout?: Record<string, {
23
+ x: number;
24
+ y: number;
25
+ }>;
26
+ /** 무버(자원) — kind 와 속성. `board.equipment`. */
27
+ equipment?: {
28
+ id: string;
29
+ kind: string;
30
+ properties?: {
31
+ id: string;
32
+ value: string;
33
+ uom?: string;
34
+ }[];
35
+ }[];
36
+ /** 좌표 단위(공간 `content.unit`). 미터가 아니면 계산하지 않는다(환산을 추측하지 않는다). */
37
+ unit?: string;
38
+ }
39
+ export interface TravelEstimatorResult {
40
+ estimator?: {
41
+ estimate(ctx: DurationCtx): number | undefined;
42
+ };
43
+ /** 왜 못 만들었나(만들었으면 비어 있음) — 조용히 없는 상태로 두지 않는다. */
44
+ reasons: string[];
45
+ /** 속도를 읽어낸 자원 종류들 — 무엇이 계산 대상인지 드러낸다. */
46
+ speedsByKind: Record<string, number>;
47
+ }
48
+ /**
49
+ * 이동시간 추정기를 만든다 — 만들 수 없으면 **이유와 함께** 비운다.
50
+ * `estimator` 가 undefined 면 커널은 명세·상수로 굴러가고, `reasons` 가 왜 그런지 말한다.
51
+ */
52
+ export declare function buildTravelEstimator(input: TravelEstimatorInput): TravelEstimatorResult;
53
+ /**
54
+ * 추정기 사슬 — 앞선 것이 답하면 그 값, 아니면 다음. **실측 → 계산 → (커널이) 명세 → 상수** 순서를
55
+ * 이 사슬과 커널 `durationOf` 가 함께 만든다. 앞에 둘수록 강한 근거다.
56
+ */
57
+ export declare function chainEstimators(estimators: (DurationEstimatorLike | undefined)[]): DurationEstimatorLike | undefined;
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ /*
3
+ * 이동시간 추정기 — **거리 × 속도.** 커널의 `TRAVEL_MS` 상수를 대체하는 유일한 정당한 길.
4
+ *
5
+ * 커널은 좌표를 모른다(설계: 좌표-free — "얼마 걸리나" 만 소비하고 "어떤 경로로" 는 씬 소관).
6
+ * 그래서 거리 기반 계산은 **호스트가 추정기로 주입**한다(`DurationEstimator` 시임).
7
+ *
8
+ * 좌표는 이미 있다: 마스터의 `RefLocation.footprint` → `masterToTwin` 이 `board.layout`(노드 중심)으로
9
+ * 낸다. 없던 것은 **속도**다 — 속도 없이 거리만으로는 시간이 나오지 않는다. 그래서 설비 속성
10
+ * (ISA-95 `EquipmentSpecificationProperty`)으로 받는다.
11
+ *
12
+ * 정직 규율: **셋 중 하나라도 모르면 답하지 않는다**(undefined → 커널이 명세·상수로 떨어진다).
13
+ * ① 두 노드의 좌표 ② 그 작업을 할 자원 종류의 속도 ③ 좌표의 단위가 미터인지
14
+ * 그리고 같은 노드 안의 작업(처리·체류)은 이동이 아니므로 다루지 않는다 — 거리 0 을 "0초" 로
15
+ * 만들면 공정 시간이 사라진다.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.EQUIPMENT_PROPERTY = void 0;
19
+ exports.buildTravelEstimator = buildTravelEstimator;
20
+ exports.chainEstimators = chainEstimators;
21
+ /** UN/CEFACT 권고 20 공통코드 → m/s 변환. 모르는 코드는 변환하지 않는다(발명 금지). */
22
+ const SPEED_TO_MPS = {
23
+ MTS: 1, // metre per second
24
+ KMH: 1000 / 3600, // kilometre per hour
25
+ '2M': 0.3048, // foot per second
26
+ HM: 0.44704 // mile per hour
27
+ };
28
+ /** 우리가 읽는 canonical 설비 속성 ID — 표준이 속성 ID 어휘를 정하지 않으므로 한 곳에서 정한다. */
29
+ exports.EQUIPMENT_PROPERTY = { speed: 'speed' };
30
+ /** 자원 종류별 속도(m/s) — 같은 종류에 값이 여럿이면 첫 유효값. 단위 없는 속도는 받지 않는다. */
31
+ function readSpeeds(equipment, reasons) {
32
+ const out = {};
33
+ for (const m of equipment ?? []) {
34
+ if (out[m.kind] !== undefined)
35
+ continue;
36
+ const p = m.properties?.find(x => x.id === exports.EQUIPMENT_PROPERTY.speed);
37
+ if (!p)
38
+ continue;
39
+ const n = Number(p.value);
40
+ if (!Number.isFinite(n) || n <= 0) {
41
+ reasons.push(`equipment "${m.id}": speed "${p.value}" is not a positive number`);
42
+ continue;
43
+ }
44
+ /* 단위가 없으면 m/s 라고 **가정하지 않는다** — 3 이 초속 3m 인지 시속 3km 인지 뜻이 완전히 다르다. */
45
+ if (!p.uom) {
46
+ reasons.push(`equipment "${m.id}": speed has no unit of measure (expected UN/CEFACT code such as MTS or KMH)`);
47
+ continue;
48
+ }
49
+ const factor = SPEED_TO_MPS[p.uom];
50
+ if (factor === undefined) {
51
+ reasons.push(`equipment "${m.id}": unknown speed unit "${p.uom}"`);
52
+ continue;
53
+ }
54
+ out[m.kind] = n * factor;
55
+ }
56
+ return out;
57
+ }
58
+ /**
59
+ * 이동시간 추정기를 만든다 — 만들 수 없으면 **이유와 함께** 비운다.
60
+ * `estimator` 가 undefined 면 커널은 명세·상수로 굴러가고, `reasons` 가 왜 그런지 말한다.
61
+ */
62
+ function buildTravelEstimator(input) {
63
+ const reasons = [];
64
+ const layout = input.layout;
65
+ const unit = input.unit ?? 'm';
66
+ if (unit !== 'm')
67
+ reasons.push(`space unit is "${unit}", not metres — travel time is not computed (no assumed conversion)`);
68
+ if (!layout || !Object.keys(layout).length)
69
+ reasons.push('no location coordinates (board.layout empty) — travel time cannot be derived from distance');
70
+ const speedsByKind = readSpeeds(input.equipment, reasons);
71
+ if (!Object.keys(speedsByKind).length)
72
+ reasons.push('no resource declares a speed property — distance alone cannot produce a duration');
73
+ if (reasons.length)
74
+ return { reasons, speedsByKind };
75
+ const lay = layout;
76
+ return {
77
+ reasons,
78
+ speedsByKind,
79
+ estimator: {
80
+ estimate(ctx) {
81
+ /* 같은 자리에서 벌어지는 일(처리·체류)은 이동이 아니다 — 다루지 않고 넘긴다. */
82
+ if (!ctx.fromNode || !ctx.toNode || ctx.fromNode === ctx.toNode)
83
+ return undefined;
84
+ const a = lay[ctx.fromNode];
85
+ const b = lay[ctx.toNode];
86
+ if (!a || !b)
87
+ return undefined; // 좌표를 모르는 노드(관측으로 알게 된 자리 등)
88
+ const speed = ctx.resourceKind ? speedsByKind[ctx.resourceKind] : undefined;
89
+ if (!speed)
90
+ return undefined; // 자원 종류를 모르거나 그 종류의 속도가 없다
91
+ const dist = Math.hypot(b.x - a.x, b.y - a.y);
92
+ if (!Number.isFinite(dist) || dist <= 0)
93
+ return undefined;
94
+ return Math.round((dist / speed) * 1000);
95
+ }
96
+ }
97
+ };
98
+ }
99
+ /**
100
+ * 추정기 사슬 — 앞선 것이 답하면 그 값, 아니면 다음. **실측 → 계산 → (커널이) 명세 → 상수** 순서를
101
+ * 이 사슬과 커널 `durationOf` 가 함께 만든다. 앞에 둘수록 강한 근거다.
102
+ */
103
+ function chainEstimators(estimators) {
104
+ const live = estimators.filter(Boolean);
105
+ if (!live.length)
106
+ return undefined;
107
+ if (live.length === 1)
108
+ return live[0];
109
+ return {
110
+ estimate(ctx) {
111
+ for (const e of live) {
112
+ const v = e.estimate(ctx);
113
+ if (v !== undefined)
114
+ return v;
115
+ }
116
+ return undefined;
117
+ }
118
+ };
119
+ }
120
+ //# sourceMappingURL=travel-estimator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"travel-estimator.js","sourceRoot":"","sources":["../../server/engine/travel-estimator.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 이동시간 추정기 — **거리 × 속도.** 커널의 `TRAVEL_MS` 상수를 대체하는 유일한 정당한 길.\n *\n * 커널은 좌표를 모른다(설계: 좌표-free — \"얼마 걸리나\" 만 소비하고 \"어떤 경로로\" 는 씬 소관).\n * 그래서 거리 기반 계산은 **호스트가 추정기로 주입**한다(`DurationEstimator` 시임).\n *\n * 좌표는 이미 있다: 마스터의 `RefLocation.footprint` → `masterToTwin` 이 `board.layout`(노드 중심)으로\n * 낸다. 없던 것은 **속도**다 — 속도 없이 거리만으로는 시간이 나오지 않는다. 그래서 설비 속성\n * (ISA-95 `EquipmentSpecificationProperty`)으로 받는다.\n *\n * 정직 규율: **셋 중 하나라도 모르면 답하지 않는다**(undefined → 커널이 명세·상수로 떨어진다).\n * ① 두 노드의 좌표 ② 그 작업을 할 자원 종류의 속도 ③ 좌표의 단위가 미터인지\n * 그리고 같은 노드 안의 작업(처리·체류)은 이동이 아니므로 다루지 않는다 — 거리 0 을 \"0초\" 로\n * 만들면 공정 시간이 사라진다.\n */\n\nimport type { MeasuredEstimate } from './measured-estimator.js'\n\n/** UN/CEFACT 권고 20 공통코드 → m/s 변환. 모르는 코드는 변환하지 않는다(발명 금지). */\nconst SPEED_TO_MPS: Record<string, number> = {\n MTS: 1, // metre per second\n KMH: 1000 / 3600, // kilometre per hour\n '2M': 0.3048, // foot per second\n HM: 0.44704 // mile per hour\n}\n\n/** 우리가 읽는 canonical 설비 속성 ID — 표준이 속성 ID 어휘를 정하지 않으므로 한 곳에서 정한다. */\nexport const EQUIPMENT_PROPERTY = { speed: 'speed' } as const\n\n/** 추정기 문맥 — 커널 `DurationContext` 와 같은 모양(커널 타입에 의존하지 않기 위해 구조로 맞춘다). */\nexport interface DurationCtx {\n kind: string\n fromNode: string\n toNode: string\n resourceKind?: string\n}\n\n/**\n * 추정기 — 숫자(평균만) 또는 추정치 객체(퍼짐까지). 커널 `DurationEstimator` 와 같은 모양.\n * 거리 계산은 숫자를 내고, 실측은 관측된 퍼짐을 함께 낸다.\n */\nexport interface DurationEstimatorLike {\n estimate(ctx: DurationCtx): number | MeasuredEstimate | undefined\n}\n\nexport interface TravelEstimatorInput {\n /** 노드 중심 좌표(masterToTwin 산출 `board.layout`). */\n layout?: Record<string, { x: number; y: number }>\n /** 무버(자원) — kind 와 속성. `board.equipment`. */\n equipment?: { id: string; kind: string; properties?: { id: string; value: string; uom?: string }[] }[]\n /** 좌표 단위(공간 `content.unit`). 미터가 아니면 계산하지 않는다(환산을 추측하지 않는다). */\n unit?: string\n}\n\nexport interface TravelEstimatorResult {\n estimator?: { estimate(ctx: DurationCtx): number | undefined }\n /** 왜 못 만들었나(만들었으면 비어 있음) — 조용히 없는 상태로 두지 않는다. */\n reasons: string[]\n /** 속도를 읽어낸 자원 종류들 — 무엇이 계산 대상인지 드러낸다. */\n speedsByKind: Record<string, number>\n}\n\n/** 자원 종류별 속도(m/s) — 같은 종류에 값이 여럿이면 첫 유효값. 단위 없는 속도는 받지 않는다. */\nfunction readSpeeds(equipment: TravelEstimatorInput['equipment'], reasons: string[]): Record<string, number> {\n const out: Record<string, number> = {}\n for (const m of equipment ?? []) {\n if (out[m.kind] !== undefined) continue\n const p = m.properties?.find(x => x.id === EQUIPMENT_PROPERTY.speed)\n if (!p) continue\n const n = Number(p.value)\n if (!Number.isFinite(n) || n <= 0) { reasons.push(`equipment \"${m.id}\": speed \"${p.value}\" is not a positive number`); continue }\n /* 단위가 없으면 m/s 라고 **가정하지 않는다** — 3 이 초속 3m 인지 시속 3km 인지 뜻이 완전히 다르다. */\n if (!p.uom) { reasons.push(`equipment \"${m.id}\": speed has no unit of measure (expected UN/CEFACT code such as MTS or KMH)`); continue }\n const factor = SPEED_TO_MPS[p.uom]\n if (factor === undefined) { reasons.push(`equipment \"${m.id}\": unknown speed unit \"${p.uom}\"`); continue }\n out[m.kind] = n * factor\n }\n return out\n}\n\n/**\n * 이동시간 추정기를 만든다 — 만들 수 없으면 **이유와 함께** 비운다.\n * `estimator` 가 undefined 면 커널은 명세·상수로 굴러가고, `reasons` 가 왜 그런지 말한다.\n */\nexport function buildTravelEstimator(input: TravelEstimatorInput): TravelEstimatorResult {\n const reasons: string[] = []\n const layout = input.layout\n const unit = input.unit ?? 'm'\n if (unit !== 'm') reasons.push(`space unit is \"${unit}\", not metres — travel time is not computed (no assumed conversion)`)\n if (!layout || !Object.keys(layout).length) reasons.push('no location coordinates (board.layout empty) — travel time cannot be derived from distance')\n const speedsByKind = readSpeeds(input.equipment, reasons)\n if (!Object.keys(speedsByKind).length) reasons.push('no resource declares a speed property — distance alone cannot produce a duration')\n if (reasons.length) return { reasons, speedsByKind }\n\n const lay = layout as Record<string, { x: number; y: number }>\n return {\n reasons,\n speedsByKind,\n estimator: {\n estimate(ctx) {\n /* 같은 자리에서 벌어지는 일(처리·체류)은 이동이 아니다 — 다루지 않고 넘긴다. */\n if (!ctx.fromNode || !ctx.toNode || ctx.fromNode === ctx.toNode) return undefined\n const a = lay[ctx.fromNode]\n const b = lay[ctx.toNode]\n if (!a || !b) return undefined // 좌표를 모르는 노드(관측으로 알게 된 자리 등)\n const speed = ctx.resourceKind ? speedsByKind[ctx.resourceKind] : undefined\n if (!speed) return undefined // 자원 종류를 모르거나 그 종류의 속도가 없다\n const dist = Math.hypot(b.x - a.x, b.y - a.y)\n if (!Number.isFinite(dist) || dist <= 0) return undefined\n return Math.round((dist / speed) * 1000)\n }\n }\n }\n}\n\n/**\n * 추정기 사슬 — 앞선 것이 답하면 그 값, 아니면 다음. **실측 → 계산 → (커널이) 명세 → 상수** 순서를\n * 이 사슬과 커널 `durationOf` 가 함께 만든다. 앞에 둘수록 강한 근거다.\n */\nexport function chainEstimators(estimators: (DurationEstimatorLike | undefined)[]): DurationEstimatorLike | undefined {\n const live = estimators.filter(Boolean) as DurationEstimatorLike[]\n if (!live.length) return undefined\n if (live.length === 1) return live[0]\n return {\n estimate(ctx) {\n for (const e of live) {\n const v = e.estimate(ctx)\n if (v !== undefined) return v\n }\n return undefined\n }\n }\n}\n"]}