@things-factory/headless-twin 10.0.5 → 10.0.7
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.
- package/README.md +37 -24
- package/dist-server/engine/entity-delta.d.ts +13 -1
- package/dist-server/engine/entity-delta.js +130 -16
- package/dist-server/engine/entity-delta.js.map +1 -1
- package/dist-server/engine/index.d.ts +4 -0
- package/dist-server/engine/index.js +4 -0
- package/dist-server/engine/index.js.map +1 -1
- package/dist-server/engine/kpi-fold.d.ts +20 -3
- package/dist-server/engine/kpi-fold.js +45 -53
- package/dist-server/engine/kpi-fold.js.map +1 -1
- package/dist-server/engine/kpi-query.d.ts +16 -0
- package/dist-server/engine/kpi-query.js +129 -19
- package/dist-server/engine/kpi-query.js.map +1 -1
- package/dist-server/engine/kpi-target.d.ts +141 -0
- package/dist-server/engine/kpi-target.js +137 -0
- package/dist-server/engine/kpi-target.js.map +1 -0
- package/dist-server/engine/live-attentions.d.ts +4 -2
- package/dist-server/engine/live-attentions.js +5 -1
- package/dist-server/engine/live-attentions.js.map +1 -1
- package/dist-server/engine/measured-estimator.d.ts +50 -0
- package/dist-server/engine/measured-estimator.js +78 -0
- package/dist-server/engine/measured-estimator.js.map +1 -0
- package/dist-server/engine/model-basis.d.ts +23 -0
- package/dist-server/engine/model-basis.js +100 -0
- package/dist-server/engine/model-basis.js.map +1 -0
- package/dist-server/engine/oee-accumulator.d.ts +2 -2
- package/dist-server/engine/oee-accumulator.js +4 -4
- package/dist-server/engine/oee-accumulator.js.map +1 -1
- package/dist-server/engine/spec-coverage.d.ts +49 -0
- package/dist-server/engine/spec-coverage.js +60 -0
- package/dist-server/engine/spec-coverage.js.map +1 -0
- package/dist-server/engine/structure-diff.d.ts +25 -0
- package/dist-server/engine/structure-diff.js +63 -0
- package/dist-server/engine/structure-diff.js.map +1 -0
- package/dist-server/engine/travel-estimator.d.ts +57 -0
- package/dist-server/engine/travel-estimator.js +120 -0
- package/dist-server/engine/travel-estimator.js.map +1 -0
- package/dist-server/engine/twin-engine.d.ts +181 -18
- package/dist-server/engine/twin-engine.js +545 -96
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/engine/warm-start.d.ts +39 -0
- package/dist-server/engine/warm-start.js +37 -0
- package/dist-server/engine/warm-start.js.map +1 -0
- package/dist-server/index.js +8 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/service/index.d.ts +4 -2
- package/dist-server/service/index.js +21 -14
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/reference/reference-live.js +2 -1
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-master.d.ts +307 -1
- package/dist-server/service/reference/reference-master.js +96 -8
- package/dist-server/service/reference/reference-master.js.map +1 -1
- package/dist-server/service/reference/reference-resolver.js +3 -3
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/reference/template-registry.d.ts +1 -1
- package/dist-server/service/reference/template-registry.js.map +1 -1
- package/dist-server/service/twin-attention/twin-attention-query.js +1 -1
- package/dist-server/service/twin-attention/twin-attention-query.js.map +1 -1
- package/dist-server/service/twin-control/twin-control-mutation.js +1 -1
- package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
- package/dist-server/service/twin-event/backfill-keys.d.ts +11 -0
- package/dist-server/service/twin-event/backfill-keys.js +63 -0
- package/dist-server/service/twin-event/backfill-keys.js.map +1 -0
- package/dist-server/service/twin-event/twin-event-keys.d.ts +35 -0
- package/dist-server/service/twin-event/twin-event-keys.js +95 -0
- package/dist-server/service/twin-event/twin-event-keys.js.map +1 -0
- package/dist-server/service/twin-event/twin-event-type.d.ts +6 -0
- package/dist-server/service/twin-event/twin-event-type.js +32 -0
- package/dist-server/service/twin-event/twin-event-type.js.map +1 -0
- package/dist-server/service/twin-event/twin-event.d.ts +16 -0
- package/dist-server/service/twin-event/twin-event.js +50 -0
- package/dist-server/service/twin-event/twin-event.js.map +1 -1
- package/dist-server/service/twin-forecast/twin-forecast-query.js +50 -7
- package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
- package/dist-server/service/twin-instance/twin-instance.js +1 -1
- package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
- package/dist-server/service/twin-journal/twin-journal-query.d.ts +28 -0
- package/dist-server/service/twin-journal/twin-journal-query.js +127 -2
- package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +1 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +4 -3
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
- package/dist-server/service/twin-space/twin-space-area.js +1 -1
- package/dist-server/service/twin-space/twin-space-area.js.map +1 -1
- package/dist-server/service/twin-space/twin-space-resolver.js +3 -3
- package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
- package/dist-server/service/twin-space/twin-space.d.ts +11 -0
- package/dist-server/service/twin-space/twin-space.js +5 -0
- package/dist-server/service/twin-space/twin-space.js.map +1 -1
- package/dist-server/service/twin-structure/index.d.ts +2 -0
- package/dist-server/service/twin-structure/index.js +6 -0
- package/dist-server/service/twin-structure/index.js.map +1 -0
- package/dist-server/service/twin-structure/twin-structure.d.ts +28 -0
- package/dist-server/service/twin-structure/twin-structure.js +97 -0
- package/dist-server/service/twin-structure/twin-structure.js.map +1 -0
- package/dist-server/service/twin-target/index.d.ts +4 -0
- package/dist-server/service/twin-target/index.js +8 -0
- package/dist-server/service/twin-target/index.js.map +1 -0
- package/dist-server/service/twin-target/twin-target-resolver.d.ts +7 -0
- package/dist-server/service/twin-target/twin-target-resolver.js +143 -0
- package/dist-server/service/twin-target/twin-target-resolver.js.map +1 -0
- package/dist-server/service/twin-target/twin-target.d.ts +25 -0
- package/dist-server/service/twin-target/twin-target.js +112 -0
- package/dist-server/service/twin-target/twin-target.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/server/engine/entity-delta.ts +135 -15
- package/server/engine/index.ts +4 -0
- package/server/engine/kpi-fold.ts +62 -59
- package/server/engine/kpi-query.ts +140 -19
- package/server/engine/kpi-target.ts +226 -0
- package/server/engine/live-attentions.ts +12 -2
- package/server/engine/measured-estimator.ts +91 -0
- package/server/engine/model-basis.ts +94 -0
- package/server/engine/oee-accumulator.ts +5 -5
- package/server/engine/spec-coverage.ts +85 -0
- package/server/engine/structure-diff.ts +88 -0
- package/server/engine/travel-estimator.ts +133 -0
- package/server/engine/twin-engine.ts +587 -106
- package/server/engine/warm-start.ts +53 -0
- package/server/index.ts +9 -0
- package/server/service/index.ts +7 -0
- package/server/service/reference/reference-live.ts +2 -1
- package/server/service/reference/reference-master.ts +383 -10
- package/server/service/reference/reference-resolver.ts +3 -3
- package/server/service/reference/template-registry.ts +1 -1
- package/server/service/twin-attention/twin-attention-query.ts +1 -1
- package/server/service/twin-control/twin-control-mutation.ts +1 -1
- package/server/service/twin-event/backfill-keys.ts +72 -0
- package/server/service/twin-event/twin-event-keys.ts +102 -0
- package/server/service/twin-event/twin-event-type.ts +27 -0
- package/server/service/twin-event/twin-event.ts +62 -0
- package/server/service/twin-forecast/twin-forecast-query.ts +56 -8
- package/server/service/twin-instance/twin-instance.ts +1 -1
- package/server/service/twin-journal/twin-journal-query.ts +129 -5
- package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +4 -3
- package/server/service/twin-space/twin-space-area.ts +1 -1
- package/server/service/twin-space/twin-space-resolver.ts +3 -3
- package/server/service/twin-space/twin-space.ts +14 -0
- package/server/service/twin-structure/index.ts +3 -0
- package/server/service/twin-structure/twin-structure.ts +93 -0
- package/server/service/twin-target/index.ts +5 -0
- package/server/service/twin-target/twin-target-resolver.ts +124 -0
- package/server/service/twin-target/twin-target.ts +97 -0
- package/test/capability-mapping.test.ts +5 -5
- package/test/duration-estimators.test.ts +144 -0
- package/test/entity-delta.test.ts +150 -19
- package/test/ingest-bench.test.ts +9 -9
- package/test/kpi-fold.test.ts +254 -3
- package/test/live-mirror-parity.test.ts +53 -19
- package/test/master-to-twin.test.ts +87 -13
- package/test/model-basis.test.ts +86 -0
- package/test/oee-accumulator.test.ts +9 -9
- package/test/scale-twin-bench.test.ts +22 -22
- package/test/spec-coverage.test.ts +113 -0
- package/test/streamline-e2e.test.ts +10 -10
- package/test/structure-revision-db.test.ts +310 -0
- package/test/twin-event-keys.test.ts +108 -0
- package/test/vocabulary-guard.test.ts +43 -0
- package/test/warm-start.test.ts +78 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-basis.js","sourceRoot":"","sources":["../../server/engine/model-basis.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;GAkBG;;;AA4CH,8CASC;AAMD,gDAgBC;AAzED,sDAAsD;AACzC,QAAA,eAAe,GAAG,GAAG,CAAA;AAWlC,wCAAwC;AACxC,SAAS,KAAK,CAAC,GAA4B;IACzC,MAAM,GAAG,GAA2B,EAAE,CAAA;IACtC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,IAAI,EAAE,CAAC;QAAE,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;IAC3F,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,SAAS,WAAW,CAAC,IAA2C;IAC9D,MAAM,GAAG,GAA2B,EAAE,CAAA;IACtC,KAAK,MAAM,CAAC,IAAI,IAAI,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC,QAAQ;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAA;IAC7E,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,4DAA4D;AAC5D,SAAS,cAAc,CAAC,CAAyB,EAAE,CAAyB,EAAE,GAAW;IACvF,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAC5D,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;QACd,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;QACd,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,SAAS;YAAE,OAAO,IAAI,CAAA,CAAC,0BAA0B;QAC9E,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;QAC/C,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,GAAG,GAAG;YAAE,OAAO,IAAI,CAAA;IAC3D,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;GAGG;AACH,SAAgB,iBAAiB,CAAC,MAAmB,EAAE,KAAkB,EAAE,MAAc,uBAAe;IACtG,IAAI,CAAC,MAAM,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAA;IACnC,IAAI,cAAc,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC;QAAE,OAAO,IAAI,CAAA;IACnF,IAAI,cAAc,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,GAAG,CAAC;QAAE,OAAO,IAAI,CAAA;IAC/E,MAAM,EAAE,GAAG,WAAW,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IACvC,MAAM,EAAE,GAAG,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;IACtC,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;IAC9D,KAAK,MAAM,CAAC,IAAI,IAAI;QAAE,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;YAAE,OAAO,IAAI,CAAA;IACtD,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;GAGG;AACH,SAAgB,kBAAkB,CAAC,KAAkB;IACnD,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAA;IAC5B,MAAM,GAAG,GAAa,EAAE,CAAA;IACxB,MAAM,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;SAC5C,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC;SAChD,IAAI,EAAE,CAAA;IACT,IAAI,CAAC,CAAC,MAAM;QAAE,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAClD,MAAM,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;SAC1C,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC;SACrD,IAAI,EAAE,CAAA;IACT,IAAI,CAAC,CAAC,MAAM;QAAE,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAC/C,MAAM,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;SAClD,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;SAC5B,IAAI,EAAE,CAAA;IACT,IAAI,CAAC,CAAC,MAAM;QAAE,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAClD,OAAO,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,sBAAsB,CAAA;AAC5D,CAAC","sourcesContent":["/*\n * 모델 근거 — **배운 모델에만 보정을 적용하기 위한 비교.**\n *\n * 예측 출력 보정(P3.2 `twinLearnCalibration`)은 \"우리 모델이 늘 이만큼 적게 본다\" 를 배워 결과를\n * 밀어 준다. 그 학습은 **그때의 모델**을 전제로 한다. 그런데 소요시간 추정기(실측·거리계산)와 공정\n * 명세가 들어오면서 모델 자체가 바뀔 수 있게 됐다 — 모델이 이미 그 편향을 잃었는데 보정이 또 밀어\n * 주면 **이중 보정**이 되어 정확도가 나빠진다. 학습 시점의 accAfter 도 더는 유효하지 않다.\n *\n * 그래서 보정 기록에 **그때 모델의 근거**를 함께 남기고, 적용 전에 지금 근거와 비교한다. 달라졌으면\n * 적용하지 않고 **그 사실을 응답에 싣는다**(조용히 사라지면 사용자는 학습이 왜 안 듣는지 모른다).\n *\n * 비교는 **상대 허용오차**로 한다(양자화 지문이 아니라). 실측 p50 은 새 기록이 쌓일 때마다 1~2초씩\n * 흔들리는데, 값을 버킷으로 뭉개 비교하면 버킷 경계에 걸린 값이 1초 드리프트로도 달라져 버린다 —\n * \"작은 드리프트로 학습을 무효화하지 않는다\" 는 목적을 경계에서 못 지킨다. 비율로 비교하면 어디서든\n * 같은 규칙이 성립한다: 10% 이내는 같은 모델, 30초→47초(57%)는 다른 모델.\n *\n * 재료는 호스트가 아는 것들이다(커널 API 에 의존하지 않아 발행 상태와 무관하게 동작한다):\n * ① 실측으로 배운 작업 종류별 소요 ② 자원 종류별 속도 ③ 선언된 공정 소요.\n */\n\n/** 같은 모델로 볼 상대 허용오차 — 이 비율 안의 변화는 드리프트, 넘으면 다른 모델. */\nexport const BASIS_TOLERANCE = 0.1\n\nexport interface ModelBasis {\n /** 작업 종류 → 실측 소요(ms). `buildMeasuredEstimator().learned`. */\n measured?: Record<string, number>\n /** 자원 종류 → 속도(m/s). `buildTravelEstimator().speedsByKind`. */\n speeds?: Record<string, number>\n /** 선언된 공정 소요 — key + ISO 8601 원문(문자열은 그대로 비교한다: 선언이 바뀌면 다른 모델). */\n declared?: { key: string; duration?: string }[]\n}\n\n/** 뜻 있는 수만 남긴다 — 0·음수·NaN 은 모델이 아니다. */\nfunction clean(map?: Record<string, number>): Record<string, number> {\n const out: Record<string, number> = {}\n for (const [k, v] of Object.entries(map ?? {})) if (Number.isFinite(v) && v > 0) out[k] = v\n return out\n}\n\nfunction declaredMap(list?: { key: string; duration?: string }[]): Record<string, string> {\n const out: Record<string, string> = {}\n for (const o of list ?? []) if (o?.key && o.duration) out[o.key] = o.duration\n return out\n}\n\n/** 두 수치 지도가 실질적으로 다른가 — 키 집합이 다르거나, 값이 허용오차를 넘어 다르면 다르다. */\nfunction numericChanged(a: Record<string, number>, b: Record<string, number>, tol: number): boolean {\n const keys = new Set([...Object.keys(a), ...Object.keys(b)])\n for (const k of keys) {\n const x = a[k]\n const y = b[k]\n if (x === undefined || y === undefined) return true // 새로 배운 종류·사라진 종류 = 다른 모델\n const base = Math.max(Math.abs(x), Math.abs(y))\n if (base > 0 && Math.abs(x - y) / base > tol) return true\n }\n return false\n}\n\n/**\n * 모델이 달라졌는가 — 보정을 적용하기 전의 관문.\n * 한쪽 근거를 모르면 **판정하지 않는다**(false) — 모르는 것을 근거로 학습을 끊지 않는다.\n */\nexport function modelBasisChanged(before?: ModelBasis, after?: ModelBasis, tol: number = BASIS_TOLERANCE): boolean {\n if (!before || !after) return false\n if (numericChanged(clean(before.measured), clean(after.measured), tol)) return true\n if (numericChanged(clean(before.speeds), clean(after.speeds), tol)) return true\n const da = declaredMap(before.declared)\n const db = declaredMap(after.declared)\n const keys = new Set([...Object.keys(da), ...Object.keys(db)])\n for (const k of keys) if (da[k] !== db[k]) return true\n return false\n}\n\n/**\n * 사람이 읽는 근거 요약 — **로그·진단용**(비교의 근거가 아니다).\n * 지문으로 같음을 판정하지 않는 이유는 위 주석 참조. 두 근거가 왜 달라졌는지 눈으로 보려면 이것을 찍는다.\n */\nexport function describeModelBasis(basis?: ModelBasis): string {\n if (!basis) return 'unknown'\n const seg: string[] = []\n const m = Object.entries(clean(basis.measured))\n .map(([k, v]) => `${k}=${Math.round(v / 1000)}s`)\n .sort()\n if (m.length) seg.push(`measured(${m.join(',')})`)\n const s = Object.entries(clean(basis.speeds))\n .map(([k, v]) => `${k}=${Math.round(v * 10) / 10}m/s`)\n .sort()\n if (s.length) seg.push(`speed(${s.join(',')})`)\n const d = Object.entries(declaredMap(basis.declared))\n .map(([k, v]) => `${k}=${v}`)\n .sort()\n if (d.length) seg.push(`declared(${d.join(',')})`)\n return seg.length ? seg.join(' ') : 'kernel defaults only'\n}\n"]}
|
|
@@ -12,9 +12,9 @@ export declare class OeeAccumulator {
|
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
14
|
* live State 를 OEE 로 보강 — projector 미러 State(관측)의 무버에 누적기 OEE(계산)를 채운다.
|
|
15
|
-
* live-모드 publishEntityData 가 buildEntityDeltas 전에 적용 →
|
|
15
|
+
* live-모드 publishEntityData 가 buildEntityDeltas 전에 적용 → equipment data(tag) payload 에 oee 포함
|
|
16
16
|
* (sim 은 커널이 채움 — 이로써 live 도 동일 payload 형상). 순수(입력 불변, 얕은 복사).
|
|
17
17
|
*/
|
|
18
18
|
export declare function withLiveOee<S extends {
|
|
19
|
-
|
|
19
|
+
equipment?: any[];
|
|
20
20
|
}>(state: S, acc: OeeAccumulator, nowMs?: number): S;
|
|
@@ -6,7 +6,7 @@ exports.withLiveOee = withLiveOee;
|
|
|
6
6
|
* OEE 이벤트 누적기 (순수·근사) — live-모드 OEE 입력 생산의 한 경로(face2-inbound-live §1.1, option ②).
|
|
7
7
|
*
|
|
8
8
|
* 문제: OEE 공식(computeOee)은 sim/live 공유되나, 입력 카운터(runMs/downMs/…)는 미러 State·이벤트에 없다
|
|
9
|
-
* (
|
|
9
|
+
* (emitEquipment 는 status/location/motion 만 방출). → live 는 카운터를 별도 생산해야 한다.
|
|
10
10
|
* 이 누적기: equipment.status 전이 스트림으로 무버별 시간-in-상태를 누적해 카운터를 근사 생산.
|
|
11
11
|
*
|
|
12
12
|
* 정확: downMs(고장 시간 = down 구간 합) · quality(good/scrap = quality.output 델타). 근사·한계:
|
|
@@ -78,12 +78,12 @@ class OeeAccumulator {
|
|
|
78
78
|
exports.OeeAccumulator = OeeAccumulator;
|
|
79
79
|
/**
|
|
80
80
|
* live State 를 OEE 로 보강 — projector 미러 State(관측)의 무버에 누적기 OEE(계산)를 채운다.
|
|
81
|
-
* live-모드 publishEntityData 가 buildEntityDeltas 전에 적용 →
|
|
81
|
+
* live-모드 publishEntityData 가 buildEntityDeltas 전에 적용 → equipment data(tag) payload 에 oee 포함
|
|
82
82
|
* (sim 은 커널이 채움 — 이로써 live 도 동일 payload 형상). 순수(입력 불변, 얕은 복사).
|
|
83
83
|
*/
|
|
84
84
|
function withLiveOee(state, acc, nowMs) {
|
|
85
|
-
if (!state?.
|
|
85
|
+
if (!state?.equipment)
|
|
86
86
|
return state;
|
|
87
|
-
return { ...state,
|
|
87
|
+
return { ...state, equipment: state.equipment.map(m => ({ ...m, oee: acc.oee(m.id, nowMs) })) };
|
|
88
88
|
}
|
|
89
89
|
//# sourceMappingURL=oee-accumulator.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oee-accumulator.js","sourceRoot":"","sources":["../../server/engine/oee-accumulator.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"oee-accumulator.js","sourceRoot":"","sources":["../../server/engine/oee-accumulator.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * OEE 이벤트 누적기 (순수·근사) — live-모드 OEE 입력 생산의 한 경로(face2-inbound-live §1.1, option ②).\n *\n * 문제: OEE 공식(computeOee)은 sim/live 공유되나, 입력 카운터(runMs/downMs/…)는 미러 State·이벤트에 없다\n * (emitEquipment 는 status/location/motion 만 방출). → live 는 카운터를 별도 생산해야 한다.\n * 이 누적기: equipment.status 전이 스트림으로 무버별 시간-in-상태를 누적해 카운터를 근사 생산.\n *\n * 정확: downMs(고장 시간 = down 구간 합) · quality(good/scrap = quality.output 델타). 근사·한계:\n * - busyMs 를 runMs 로(setup 분리 불가 — 이벤트에 setup 없음) → performance 낙관·availability 는 setup 손실 미반영.\n * - quality.output 이 없으면(예: WMS/YMS) quality=1(품질 개념 없음).\n * 시간축 = Date.parse(eventTime)(ms) — 구간 차분은 sim/실시간 무관하게 정확(고정 오프셋 상쇄).\n */\nimport { computeOee } from '@operato/twin-kernel'\nimport type { CanonicalEnvelope, OeeCounters, OeeMetrics } from '@operato/twin-kernel'\n\ninterface Acc {\n status?: string\n lastMs: number\n startMs: number\n busyMs: number\n downMs: number\n goodCount: number\n scrapCount: number\n}\n\nexport class OeeAccumulator {\n private acc = new Map<string, Acc>()\n private maxMs = 0 // 최신 이벤트 시각 — nowMs 기본값(현재 계측 시점)\n\n private ensure(moverId: string, t: number): Acc {\n let a = this.acc.get(moverId)\n if (!a) {\n a = { lastMs: t, startMs: t, busyMs: 0, downMs: 0, goodCount: 0, scrapCount: 0 }\n this.acc.set(moverId, a)\n }\n return a\n }\n\n /** equipment.status(시간-in-상태) + quality.output(양품/불량) 소비. 다른 eventType 무시. */\n apply(env: CanonicalEnvelope): void {\n const t = Date.parse(env.eventTime)\n if (t > this.maxMs) this.maxMs = t\n if (env.eventType === 'quality.output') {\n const q = env.data as { moverId?: string; goodCount?: number; scrapCount?: number }\n if (!q?.moverId) return\n const a = this.ensure(q.moverId, t)\n a.goodCount = q.goodCount ?? a.goodCount // 누적값 최신화(good/scrap 은 무버 누적)\n a.scrapCount = q.scrapCount ?? a.scrapCount\n return\n }\n if (env.eventType !== 'equipment.status') return\n const d = env.data as { moverId?: string; status?: string }\n if (!d?.moverId) return\n const a = this.ensure(d.moverId, t)\n const dt = Math.max(0, t - a.lastMs)\n if (a.status === 'busy') a.busyMs += dt\n else if (a.status === 'down') a.downMs += dt\n a.status = d.status\n a.lastMs = t\n }\n\n /** 무버 OEE 카운터 — 마지막 상태 구간을 nowMs(기본=최신 이벤트 시각)까지 flush. busy→run 근사, setup=0. */\n counters(moverId: string, nowMs: number = this.maxMs): OeeCounters {\n const a = this.acc.get(moverId)\n if (!a) return { runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, scrapCount: 0, metricsSinceMs: 0 }\n const dt = Math.max(0, nowMs - a.lastMs)\n let busyMs = a.busyMs\n let downMs = a.downMs\n if (a.status === 'busy') busyMs += dt\n else if (a.status === 'down') downMs += dt\n return { runMs: busyMs, setupMs: 0, downMs, goodCount: a.goodCount, scrapCount: a.scrapCount, metricsSinceMs: a.startMs }\n }\n\n /** 공유 공식으로 근사 OEE 산출 — 정확한 OEE 는 실 텔레메트리(option ①). nowMs 기본=최신 이벤트 시각. */\n oee(moverId: string, nowMs: number = this.maxMs): OeeMetrics {\n return computeOee(this.counters(moverId, nowMs), nowMs)\n }\n}\n\n/**\n * live State 를 OEE 로 보강 — projector 미러 State(관측)의 무버에 누적기 OEE(계산)를 채운다.\n * live-모드 publishEntityData 가 buildEntityDeltas 전에 적용 → equipment data(tag) payload 에 oee 포함\n * (sim 은 커널이 채움 — 이로써 live 도 동일 payload 형상). 순수(입력 불변, 얕은 복사).\n */\nexport function withLiveOee<S extends { equipment?: any[] }>(state: S, acc: OeeAccumulator, nowMs?: number): S {\n if (!state?.equipment) return state\n return { ...state, equipment: state.equipment.map(m => ({ ...m, oee: acc.oee(m.id, nowMs) })) }\n}\n"]}
|
|
@@ -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":";AAAA;;;;;;;;GAQG;;AA8CH,wCAiCC;AAxDD,MAAM,OAAO,GAAG,CAAI,IAA8B,EAAE,GAAiC,EAAuB,EAAE;IAC5G,MAAM,GAAG,GAAG,IAAI,GAAG,EAAkB,CAAA;IACrC,KAAK,MAAM,CAAC,IAAI,IAAI,IAAI,EAAE,EAAE,CAAC;QAC3B,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAA;QAChB,IAAI,CAAC;YAAE,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IAC1C,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC,CAAA;AAED,MAAM,KAAK,GAAG,CAAC,IAA4C,EAAe,EAAE,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAEjJ,SAAS,OAAO,CAAC,MAA2B,EAAE,KAA0B;IACtE,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,EAAE,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;IACrE,OAAO,IAAI;SACR,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC;SACtI,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,CAAA;AAC/B,CAAC;AAED;;;;GAIG;AACH,SAAgB,cAAc,CAAC,MAAuB,EAAE,KAAU;IAChE,MAAM,eAAe,GAAG,MAAM,EAAE,SAAS,IAAI,EAAE,CAAA;IAC/C,MAAM,eAAe,GAAG,MAAM,EAAE,SAAS,IAAI,EAAE,CAAA;IAC/C,MAAM,cAAc,GAAG,KAAK,EAAE,SAAS,IAAI,EAAE,CAAA;IAC7C,MAAM,cAAc,GAAG,KAAK,EAAE,SAAS,IAAI,EAAE,CAAA;IAE7C,MAAM,iBAAiB,GAAG,KAAK,CAAC,eAAe,CAAC,CAAA;IAChD,MAAM,gBAAgB,GAAG,KAAK,CAAC,cAAc,CAAC,CAAA;IAC9C,MAAM,kBAAkB,GAAG,KAAK,CAAC,eAAe,CAAC,CAAA;IACjD,MAAM,iBAAiB,GAAG,KAAK,CAAC,cAAc,CAAC,CAAA;IAE/C,MAAM,KAAK,GAAG,CAAI,CAAS,EAAE,CAAS,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAA;IAC/E,MAAM,cAAc,GAAG,KAAK,CAAC,iBAAiB,EAAE,gBAAgB,CAAC,CAAA;IACjE,MAAM,gBAAgB,GAAG,KAAK,CAAC,gBAAgB,EAAE,iBAAiB,CAAC,CAAA;IACnE,MAAM,cAAc,GAAG,KAAK,CAAC,kBAAkB,EAAE,iBAAiB,CAAC,CAAA;IACnE,MAAM,gBAAgB,GAAG,KAAK,CAAC,iBAAiB,EAAE,kBAAkB,CAAC,CAAA;IAErE;;;OAGG;IACH,MAAM,cAAc,GAClB,CAAC,cAAc,IAAI,CAAC,gBAAgB,IAAI,CAAC,cAAc,IAAI,CAAC,gBAAgB,IAAI,CAAC,CAAC,MAAM,CAAA;IAE1F,OAAO;QACL,aAAa,EAAE,OAAO,CAAC,OAAO,CAAC,eAAe,EAAE,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,cAAc,EAAE,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACjH,cAAc,EAAE,OAAO,CAAC,OAAO,CAAC,eAAe,EAAE,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,cAAc,EAAE,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAClH,cAAc;QACd,gBAAgB;QAChB,cAAc;QACd,gBAAgB;QAChB,cAAc;KACf,CAAA;AACH,CAAC","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"]}
|