@things-factory/headless-twin 10.1.37 → 10.1.38

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 (34) hide show
  1. package/dist-server/engine/twin-engine.js +3 -2
  2. package/dist-server/engine/twin-engine.js.map +1 -1
  3. package/dist-server/engine/twin-state-channel.d.ts +14 -0
  4. package/dist-server/engine/twin-state-channel.js +31 -0
  5. package/dist-server/engine/twin-state-channel.js.map +1 -0
  6. package/dist-server/service/reference/reference-progress-channel.d.ts +14 -0
  7. package/dist-server/service/reference/reference-progress-channel.js +24 -0
  8. package/dist-server/service/reference/reference-progress-channel.js.map +1 -0
  9. package/dist-server/service/reference/reference-progress-subscription.d.ts +2 -3
  10. package/dist-server/service/reference/reference-progress-subscription.js +14 -6
  11. package/dist-server/service/reference/reference-progress-subscription.js.map +1 -1
  12. package/dist-server/service/reference/reference-resolver.js +3 -6
  13. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  14. package/dist-server/service/twin-forecast/gap-analytics.d.ts +10 -1
  15. package/dist-server/service/twin-forecast/gap-analytics.js +15 -6
  16. package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
  17. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  18. package/dist-server/service/twin-state/twin-state-subscription.d.ts +2 -3
  19. package/dist-server/service/twin-state/twin-state-subscription.js +20 -13
  20. package/dist-server/service/twin-state/twin-state-subscription.js.map +1 -1
  21. package/package.json +6 -6
  22. package/server/engine/twin-engine.ts +6 -5
  23. package/server/engine/twin-state-channel.ts +39 -0
  24. package/server/service/reference/reference-progress-channel.ts +26 -0
  25. package/server/service/reference/reference-progress-subscription.ts +15 -10
  26. package/server/service/reference/reference-resolver.ts +6 -6
  27. package/server/service/twin-forecast/gap-analytics.ts +15 -6
  28. package/server/service/twin-forecast/twin-forecast-query.ts +1 -1
  29. package/server/service/twin-state/twin-state-subscription.ts +22 -19
  30. package/test/boot-resume.test.ts +4 -1
  31. package/test/gap-analytics.test.ts +25 -1
  32. package/test/open-doors.test.ts +197 -0
  33. package/test/twin-state-channel.test.ts +111 -0
  34. package/tsconfig.tsbuildinfo +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"gap-analytics.js","sourceRoot":"","sources":["../../../server/service/twin-forecast/gap-analytics.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 예측 정확도 진단 — SPC·정확도·편향·보정 계산의 canonical 구현(서버 소유).\n * 프레젠테이션(클라)이 아니라 여기에서 한 번 계산해 twinForecastGapTrend 가 돌려준다 → API·twin-ai·경보·다른 UI 재사용, 서버 테스트 가능.\n *\n * 두 축(독립):\n * 안정성(stability) — 오차 폭이 일정한가(SPC I-chart: 관리한계 이탈=shift, 단조 추세=drift).\n * 편향(bias) — 평균 오차(CL)가 목표 0 에서 벗어났나(CL 이 관리폭 절반 이상 이탈 또는 추적신호 큼).\n * 관리도는 자기 평균(CL)을 중심으로 잡으므로 \"안정적\"이 \"무편향\"을 뜻하지 않는다 — 그래서 별도 축.\n */\n\n/** SPC I-chart 표본 수 — 신뢰할 만한 관리한계의 통설 최소(20~25) 하한. 서버가 관장(표현 계층 아님). */\nexport const GAP_SAMPLES = 24\n\nexport interface GapPoint {\n t: number\n p50: number\n min: number\n max: number\n actual: number\n error: number // 실제 − 예측(p50). 양수 = 과소예측(예측이 낮음).\n inBand: boolean\n naiveAbsError: number\n modelAbsError: number\n}\n\n/** 합산 결과 + **무엇을 못 넣었는지.** 알리지 않고 줄이지 않는다. */\nexport interface AggregateResult {\n points: GapPoint[]\n /** 한 트윈이라도 재지 못해 통째로 뺀 vantage 수. */\n droppedVantages: number\n}\n\n/**\n * 공동배치(co-located) 트윈들의 점 배열을 공장 단위 한 시계열로 합산.\n *\n * ── 예전에 무엇이 어긋나 있었나 (2026-08-08) ─────────────────────────────────\n * \"동일 부팅 → vantage 정렬\" 을 **가정**하고 배열 인덱스끼리 더했다. 그 가정은 세 곳에서 깨졌다:\n *\n * ① 격자를 트윈마다 자기 저널의 첫·마지막 이벤트로 만들었다. 저널 구간이 다르면 같은 인덱스가\n * 서로 다른 시각이다. **부팅이 같아도** 어긋난다.\n * ② 한 vantage 를 못 재면 `continue` 로 건너뛰고 밀어 담았다. 그 순간부터 인덱스가 한 칸씩 밀린다.\n * ③ 길이를 `max` 로 잡아, 뒤쪽 인덱스는 남은 트윈만 더해졌다 — 공장 합계 곡선이 꼬리에서\n * 아래로 꺾인다. 공장과 아무 상관 없는 이유로.\n *\n * 셋 다 오류를 내지 않았다. 그럴듯한 곡선이 나오고, 그 곡선으로 예측 정확도를 판정했다.\n *\n * ── 지금 ────────────────────────────────────────────────────────────────────\n * 격자는 밖에서 하나를 만들어 모두에게 같은 것을 주고(`sharedGrid`), 못 잰 자리는 `null` 로\n * 남긴다. 여기서는 **모든 트윈이 값을 가진 vantage 만** 더한다. 하나라도 비면 그 시각의\n * 공장 총량은 알 수 없는 것이지, 그 트윈이 0 을 낸 것이 아니다.\n *\n * 뺀 개수는 돌려준다 — 화면이 \"24점 중 18점\" 이라고 말할 수 있어야 한다.\n */\nexport function aggregatePoints(per: (GapPoint | null)[][]): AggregateResult {\n if (!per.length) return { points: [], droppedVantages: 0 }\n const n = Math.max(0, ...per.map(pts => pts.length))\n const out: GapPoint[] = []\n let droppedVantages = 0\n for (let i = 0; i < n; i++) {\n const slot = per.map(pts => pts[i])\n if (slot.some(pt => !pt)) { droppedVantages++; continue }\n const pts = slot as GapPoint[]\n const sum = (f: (p: GapPoint) => number) => pts.reduce((a, p) => a + f(p), 0)\n const p50 = sum(p => p.p50)\n const min = sum(p => p.min)\n const max = sum(p => p.max)\n const actual = sum(p => p.actual)\n out.push({\n t: pts[0].t, // 격자가 하나이므로 어느 트윈에서 읽어도 같다.\n p50, min, max, actual,\n error: actual - p50,\n inBand: actual >= min && actual <= max,\n naiveAbsError: sum(p => p.naiveAbsError),\n modelAbsError: sum(p => p.modelAbsError)\n })\n }\n return { points: out, droppedVantages }\n}\n\n/** 안정성 축 + 편향 축 판정(관리도). errors = 실제−예측 시계열. */\nexport function computeSpc(errors: number[]): any {\n const n = errors.length\n if (n < 4) return { insufficient: true }\n const cl = errors.reduce((a, b) => a + b, 0) / n\n const mr: number[] = []\n for (let i = 1; i < n; i++) mr.push(Math.abs(errors[i] - errors[i - 1]))\n const mrBar = mr.reduce((a, b) => a + b, 0) / mr.length\n const sigma = mrBar / 1.128 || Math.max(1e-6, Math.abs(cl) * 0.01) // d2(n=2)=1.128\n const ucl = cl + 3 * sigma, lcl = cl - 3 * sigma\n // 축1 안정성 — 관리한계 이탈(shift) 또는 단조 추세(drift).\n const ooc = errors.map((e, i) => (e > ucl || e < lcl ? i : -1)).filter(i => i >= 0)\n let up = 1, down = 1, trend = false\n for (let i = 1; i < n; i++) { up = errors[i] > errors[i - 1] ? up + 1 : 1; down = errors[i] < errors[i - 1] ? down + 1 : 1; if (up >= 6 || down >= 6) trend = true }\n const instability = ooc.length ? 'shift' : trend ? 'drift' : null\n // 축2 편향 — 평균 오차가 0 에서 이탈. |CL| > 0.5σ 또는 추적신호(누적오차/평균절대오차) > 4.\n const mad = errors.reduce((s, e) => s + Math.abs(e), 0) / n || 1\n const trackingSignal = errors.reduce((a, b) => a + b, 0) / mad\n const biased = Math.abs(cl) > 0.5 * sigma || Math.abs(trackingSignal) > 4\n return { cl, ucl, lcl, sigma, n, ooc, instability, stable: !instability, biased, biasSign: cl >= 0 ? 'under' : 'over', trackingSignal }\n}\n\n/** 집계 지표 — 정확도(1−WMAPE)·편향(MPE)·예측구간 적중·FVA(예측 가치). */\nexport function gapMetrics(pts: GapPoint[]): any {\n const sumAbsErr = pts.reduce((s, x) => s + Math.abs(x.error), 0)\n const sumActual = pts.reduce((s, x) => s + Math.abs(x.actual), 0)\n const wmape = sumActual ? sumAbsErr / sumActual : 0\n const accuracy = Math.max(0, Math.round((1 - wmape) * 100))\n const mpe = pts.length ? pts.reduce((s, x) => s + x.error, 0) / pts.length : 0\n const coverage = pts.length ? pts.filter(x => x.inBand).length / pts.length : 0\n // 예측 가치(FVA) — \"현재값 그대로\"(naive) 대비 오차 감소. fva=감소 총량, fvaPct=naive 오차를 몇 % 줄였나(가치의 크기).\n const sumNaive = pts.reduce((s, x) => s + x.naiveAbsError, 0)\n const sumModel = pts.reduce((s, x) => s + x.modelAbsError, 0)\n const fva = sumNaive - sumModel\n const fvaPct = sumNaive > 0 ? Math.round((1 - sumModel / sumNaive) * 100) : 0\n return { accuracy, mpe, coverage, inBandCount: pts.filter(x => x.inBand).length, total: pts.length, fva, fvaPct }\n}\n\n/** 학습(자기보정) 예상 — 평균 오차(편향)를 걷어냈을 때 기대되는 정확도·커버리지. accBefore/covBefore 는 원본 지표에서. */\nexport function learnedCalibration(pts: GapPoint[], metrics: any): any {\n if (pts.length < 3) return null\n const errs = pts.map(p => p.error)\n const biasShift = errs.reduce((a, b) => a + b, 0) / errs.length // 학습된 편향(이만큼 예측을 올림/내림)\n const corrected = errs.map(e => e - biasShift) // 편향 제거 후 잔차\n const absC = corrected.map(Math.abs).sort((a, b) => a - b)\n const halfWidth = absC[Math.floor(absC.length * 0.8)] ?? (absC[absC.length - 1] || 1) // 80% 커버리지 밴드\n const sumActual = pts.reduce((s, p) => s + Math.abs(p.actual), 0) || 1\n const accAfter = Math.max(0, Math.round((1 - corrected.reduce((s, e) => s + Math.abs(e), 0) / sumActual) * 100))\n const covAfter = corrected.filter(e => Math.abs(e) <= halfWidth).length\n return { biasShift: Math.round(biasShift), halfWidth: Math.round(halfWidth), accBefore: metrics.accuracy, accAfter, covBefore: metrics.inBandCount, covAfter, n: pts.length }\n}\n"]}
1
+ {"version":3,"file":"gap-analytics.js","sourceRoot":"","sources":["../../../server/service/twin-forecast/gap-analytics.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;AA6CH,0CAwBC;AAGD,gCAmBC;AAYD,gCAYC;AAGD,gDAYC;AAhID,yEAAyE;AAC5D,QAAA,WAAW,GAAG,EAAE,CAAA;AAqB7B;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,SAAgB,eAAe,CAAC,GAA0B;IACxD,IAAI,CAAC,GAAG,CAAC,MAAM;QAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,eAAe,EAAE,CAAC,EAAE,CAAA;IAC1D,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAA;IACpD,MAAM,GAAG,GAAe,EAAE,CAAA;IAC1B,IAAI,eAAe,GAAG,CAAC,CAAA;IACvB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3B,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;QACnC,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;YAAC,eAAe,EAAE,CAAC;YAAC,SAAQ;QAAC,CAAC;QACzD,MAAM,GAAG,GAAG,IAAkB,CAAA;QAC9B,MAAM,GAAG,GAAG,CAAC,CAA0B,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;QAC7E,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;QAC3B,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;QAC3B,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;QAC3B,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;QACjC,GAAG,CAAC,IAAI,CAAC;YACP,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,4BAA4B;YACzC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM;YACrB,KAAK,EAAE,MAAM,GAAG,GAAG;YACnB,MAAM,EAAE,MAAM,IAAI,GAAG,IAAI,MAAM,IAAI,GAAG;YACtC,aAAa,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC;YACxC,aAAa,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC;SACzC,CAAC,CAAA;IACJ,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,eAAe,EAAE,CAAA;AACzC,CAAC;AAED,gDAAgD;AAChD,SAAgB,UAAU,CAAC,MAAgB;IACzC,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,CAAA;IACvB,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,CAAA;IACxC,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAA;IAChD,MAAM,EAAE,GAAa,EAAE,CAAA;IACvB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;QAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;IACxE,MAAM,KAAK,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,MAAM,CAAA;IACvD,MAAM,KAAK,GAAG,KAAK,GAAG,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,CAAA,CAAC,gBAAgB;IACnF,MAAM,GAAG,GAAG,EAAE,GAAG,CAAC,GAAG,KAAK,EAAE,GAAG,GAAG,EAAE,GAAG,CAAC,GAAG,KAAK,CAAA;IAChD,2CAA2C;IAC3C,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;IACnF,IAAI,EAAE,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAA;IACnC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAAC,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAAC,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAAC,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC;YAAE,KAAK,GAAG,IAAI,CAAA;IAAC,CAAC;IACpK,MAAM,WAAW,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAA;IACjE,gEAAgE;IAChE,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IAChE,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,GAAG,CAAA;IAC9D,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,GAAG,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,GAAG,CAAC,CAAA;IACzE,OAAO,EAAE,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,EAAE,cAAc,EAAE,CAAA;AACzI,CAAC;AAED;;;;;;;;;GASG;AACH,SAAgB,UAAU,CAAC,GAAe;IACxC,MAAM,SAAS,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAA;IAChE,MAAM,SAAS,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAA;IACjE,MAAM,QAAQ,GAAG,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,SAAS,GAAG,SAAS,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IAClG,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAA;IAC9E,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAA;IAC/E,uFAAuF;IACvF,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,aAAa,EAAE,CAAC,CAAC,CAAA;IAC7D,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,aAAa,EAAE,CAAC,CAAC,CAAA;IAC7D,MAAM,GAAG,GAAG,QAAQ,GAAG,QAAQ,CAAA;IAC/B,MAAM,MAAM,GAAG,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,QAAQ,GAAG,QAAQ,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IAChF,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,CAAA;AACnH,CAAC;AAED,oFAAoF;AACpF,SAAgB,kBAAkB,CAAC,GAAe,EAAE,OAAY;IAC9D,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAA;IAC/B,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAA;IAClC,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAA,CAAC,wBAAwB;IACxF,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,SAAS,CAAC,CAAA,CAAC,aAAa;IAC5D,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IAC1D,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA,CAAC,cAAc;IACpG,0GAA0G;IAC1G,MAAM,SAAS,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAA;IACjE,MAAM,QAAQ,GAAG,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,SAAS,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IACvI,MAAM,QAAQ,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,MAAM,CAAA;IACvE,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,CAAC,WAAW,EAAE,QAAQ,EAAE,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,CAAA;AAC/K,CAAC","sourcesContent":["/*\n * 예측 정확도 진단 — SPC·정확도·편향·보정 계산의 canonical 구현(서버 소유).\n * 프레젠테이션(클라)이 아니라 여기에서 한 번 계산해 twinForecastGapTrend 가 돌려준다 → API·twin-ai·경보·다른 UI 재사용, 서버 테스트 가능.\n *\n * 두 축(독립):\n * 안정성(stability) — 오차 폭이 일정한가(SPC I-chart: 관리한계 이탈=shift, 단조 추세=drift).\n * 편향(bias) — 평균 오차(CL)가 목표 0 에서 벗어났나(CL 이 관리폭 절반 이상 이탈 또는 추적신호 큼).\n * 관리도는 자기 평균(CL)을 중심으로 잡으므로 \"안정적\"이 \"무편향\"을 뜻하지 않는다 — 그래서 별도 축.\n */\n\n/** SPC I-chart 표본 수 — 신뢰할 만한 관리한계의 통설 최소(20~25) 하한. 서버가 관장(표현 계층 아님). */\nexport const GAP_SAMPLES = 24\n\nexport interface GapPoint {\n t: number\n p50: number\n min: number\n max: number\n actual: number\n error: number // 실제 − 예측(p50). 양수 = 과소예측(예측이 낮음).\n inBand: boolean\n naiveAbsError: number\n modelAbsError: number\n}\n\n/** 합산 결과 + **무엇을 못 넣었는지.** 알리지 않고 줄이지 않는다. */\nexport interface AggregateResult {\n points: GapPoint[]\n /** 한 트윈이라도 재지 못해 통째로 뺀 vantage 수. */\n droppedVantages: number\n}\n\n/**\n * 공동배치(co-located) 트윈들의 점 배열을 공장 단위 한 시계열로 합산.\n *\n * ── 예전에 무엇이 어긋나 있었나 (2026-08-08) ─────────────────────────────────\n * \"동일 부팅 → vantage 정렬\" 을 **가정**하고 배열 인덱스끼리 더했다. 그 가정은 세 곳에서 깨졌다:\n *\n * ① 격자를 트윈마다 자기 저널의 첫·마지막 이벤트로 만들었다. 저널 구간이 다르면 같은 인덱스가\n * 서로 다른 시각이다. **부팅이 같아도** 어긋난다.\n * ② 한 vantage 를 못 재면 `continue` 로 건너뛰고 밀어 담았다. 그 순간부터 인덱스가 한 칸씩 밀린다.\n * ③ 길이를 `max` 로 잡아, 뒤쪽 인덱스는 남은 트윈만 더해졌다 — 공장 합계 곡선이 꼬리에서\n * 아래로 꺾인다. 공장과 아무 상관 없는 이유로.\n *\n * 셋 다 오류를 내지 않았다. 그럴듯한 곡선이 나오고, 그 곡선으로 예측 정확도를 판정했다.\n *\n * ── 지금 ────────────────────────────────────────────────────────────────────\n * 격자는 밖에서 하나를 만들어 모두에게 같은 것을 주고(`sharedGrid`), 못 잰 자리는 `null` 로\n * 남긴다. 여기서는 **모든 트윈이 값을 가진 vantage 만** 더한다. 하나라도 비면 그 시각의\n * 공장 총량은 알 수 없는 것이지, 그 트윈이 0 을 낸 것이 아니다.\n *\n * 뺀 개수는 돌려준다 — 화면이 \"24점 중 18점\" 이라고 말할 수 있어야 한다.\n */\nexport function aggregatePoints(per: (GapPoint | null)[][]): AggregateResult {\n if (!per.length) return { points: [], droppedVantages: 0 }\n const n = Math.max(0, ...per.map(pts => pts.length))\n const out: GapPoint[] = []\n let droppedVantages = 0\n for (let i = 0; i < n; i++) {\n const slot = per.map(pts => pts[i])\n if (slot.some(pt => !pt)) { droppedVantages++; continue }\n const pts = slot as GapPoint[]\n const sum = (f: (p: GapPoint) => number) => pts.reduce((a, p) => a + f(p), 0)\n const p50 = sum(p => p.p50)\n const min = sum(p => p.min)\n const max = sum(p => p.max)\n const actual = sum(p => p.actual)\n out.push({\n t: pts[0].t, // 격자가 하나이므로 어느 트윈에서 읽어도 같다.\n p50, min, max, actual,\n error: actual - p50,\n inBand: actual >= min && actual <= max,\n naiveAbsError: sum(p => p.naiveAbsError),\n modelAbsError: sum(p => p.modelAbsError)\n })\n }\n return { points: out, droppedVantages }\n}\n\n/** 안정성 축 + 편향 축 판정(관리도). errors = 실제−예측 시계열. */\nexport function computeSpc(errors: number[]): any {\n const n = errors.length\n if (n < 4) return { insufficient: true }\n const cl = errors.reduce((a, b) => a + b, 0) / n\n const mr: number[] = []\n for (let i = 1; i < n; i++) mr.push(Math.abs(errors[i] - errors[i - 1]))\n const mrBar = mr.reduce((a, b) => a + b, 0) / mr.length\n const sigma = mrBar / 1.128 || Math.max(1e-6, Math.abs(cl) * 0.01) // d2(n=2)=1.128\n const ucl = cl + 3 * sigma, lcl = cl - 3 * sigma\n // 축1 안정성 — 관리한계 이탈(shift) 또는 단조 추세(drift).\n const ooc = errors.map((e, i) => (e > ucl || e < lcl ? i : -1)).filter(i => i >= 0)\n let up = 1, down = 1, trend = false\n for (let i = 1; i < n; i++) { up = errors[i] > errors[i - 1] ? up + 1 : 1; down = errors[i] < errors[i - 1] ? down + 1 : 1; if (up >= 6 || down >= 6) trend = true }\n const instability = ooc.length ? 'shift' : trend ? 'drift' : null\n // 축2 편향 — 평균 오차가 0 에서 이탈. |CL| > 0.5σ 또는 추적신호(누적오차/평균절대오차) > 4.\n const mad = errors.reduce((s, e) => s + Math.abs(e), 0) / n || 1\n const trackingSignal = errors.reduce((a, b) => a + b, 0) / mad\n const biased = Math.abs(cl) > 0.5 * sigma || Math.abs(trackingSignal) > 4\n return { cl, ucl, lcl, sigma, n, ooc, instability, stable: !instability, biased, biasSign: cl >= 0 ? 'under' : 'over', trackingSignal }\n}\n\n/**\n * 집계 지표 — 정확도(1−WMAPE)·편향(MPE)·예측구간 적중·FVA(예측 가치).\n *\n * A percentage whose denominator is zero is **not measured**, and says so with `null` (2026-09-24).\n * When every actual in the window is 0 — the line stood still, or the metric was not observed — WMAPE\n * is undefined. It used to become 0, so the card read \"100% accurate\" beside \"0 of 6 in band, mean\n * error −20\". Likewise `fvaPct` when the naive error is 0: 0 read as \"the forecast adds nothing\".\n * The absolute measures (mean error, in-band count, fva) are real in that window and stay. The two\n * neighbours below already refuse on thin evidence (`computeSpc` · `learnedCalibration`).\n */\nexport function gapMetrics(pts: GapPoint[]): any {\n const sumAbsErr = pts.reduce((s, x) => s + Math.abs(x.error), 0)\n const sumActual = pts.reduce((s, x) => s + Math.abs(x.actual), 0)\n const accuracy = sumActual > 0 ? Math.max(0, Math.round((1 - sumAbsErr / sumActual) * 100)) : null\n const mpe = pts.length ? pts.reduce((s, x) => s + x.error, 0) / pts.length : 0\n const coverage = pts.length ? pts.filter(x => x.inBand).length / pts.length : 0\n // 예측 가치(FVA) — \"현재값 그대로\"(naive) 대비 오차 감소. fva=감소 총량, fvaPct=naive 오차를 몇 % 줄였나(가치의 크기).\n const sumNaive = pts.reduce((s, x) => s + x.naiveAbsError, 0)\n const sumModel = pts.reduce((s, x) => s + x.modelAbsError, 0)\n const fva = sumNaive - sumModel\n const fvaPct = sumNaive > 0 ? Math.round((1 - sumModel / sumNaive) * 100) : null\n return { accuracy, mpe, coverage, inBandCount: pts.filter(x => x.inBand).length, total: pts.length, fva, fvaPct }\n}\n\n/** 학습(자기보정) 예상 — 평균 오차(편향)를 걷어냈을 때 기대되는 정확도·커버리지. accBefore/covBefore 는 원본 지표에서. */\nexport function learnedCalibration(pts: GapPoint[], metrics: any): any {\n if (pts.length < 3) return null\n const errs = pts.map(p => p.error)\n const biasShift = errs.reduce((a, b) => a + b, 0) / errs.length // 학습된 편향(이만큼 예측을 올림/내림)\n const corrected = errs.map(e => e - biasShift) // 편향 제거 후 잔차\n const absC = corrected.map(Math.abs).sort((a, b) => a - b)\n const halfWidth = absC[Math.floor(absC.length * 0.8)] ?? (absC[absC.length - 1] || 1) // 80% 커버리지 밴드\n /* The bias is learnable with every actual at 0; the accuracy before and after is not (see gapMetrics). */\n const sumActual = pts.reduce((s, p) => s + Math.abs(p.actual), 0)\n const accAfter = sumActual > 0 ? Math.max(0, Math.round((1 - corrected.reduce((s, e) => s + Math.abs(e), 0) / sumActual) * 100)) : null\n const covAfter = corrected.filter(e => Math.abs(e) <= halfWidth).length\n return { biasShift: Math.round(biasShift), halfWidth: Math.round(halfWidth), accBefore: metrics.accuracy, accAfter, covBefore: metrics.inBandCount, covAfter, n: pts.length }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"twin-forecast-query.js","sourceRoot":"","sources":["../../../server/service/twin-forecast/twin-forecast-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { twinLog } from '../../engine/log.js'\nimport { hydratedDate } from '../../engine/read-time.js'\nimport { Arg, Ctx, Int, Mutation, Query, Resolver, Directive} from 'type-graphql'\n\nimport { getRepository, ScalarObject } from '@things-factory/shell'\n\nimport { TwinEngine } from '../../engine/index.js'\nimport { readSpecCoverage } from '../../engine/spec-coverage.js'\nimport { describeModelBasis, modelBasisChanged, type ModelBasis } from '../../engine/model-basis.js'\nimport { TwinEvent } from '../twin-event/twin-event.js'\nimport { GAP_SAMPLES, aggregatePoints, computeSpc, gapMetrics, learnedCalibration, type GapPoint } from './gap-analytics.js'\nimport { FORECAST_METRICS, type MetricFn } from './forecast-metrics.js'\nimport { forecastStepMs } from './forecast-tuning.js'\n\n/* 커널 예측 함수 — CJS require(twin-engine 과 동일 방식). */\nconst { monteCarloForecastAsync } = require('@operato/twin-kernel')\n\n/**\n * 이벤트 루프에 자리를 내주는 방법 — **커널이 아니라 여기가 안다.**\n *\n * `setImmediate` 는 대기 중인 I/O 다음 차례에 돌아온다. 마이크로태스크(`Promise.resolve()`)로는\n * 자리가 가지 않는다 — 그러면 예측이 도는 동안 라이브 틱도 HTTP 도 여전히 멈춘다.\n *\n * ── 내준 시간은 **부하가 아니다** ───────────────────────────────────────────\n * 부하 계기는 「이 트윈이 루프를 얼마나 붙잡았나」를 재는 자다. 양보를 넣은 뒤로 벽시계 시간에는\n * 남에게 넘긴 시간이 섞이는데, 그것까지 부하로 적으면 계기가 거짓말을 한다 — 하필 이 문제를 짚어낸\n * 그 계기다. 그래서 내준 시간을 따로 세어 빼고 적는다.\n */\nfunction breather() {\n let yieldedMs = 0\n return {\n fn: () =>\n new Promise<void>(resolve => {\n const t = performance.now()\n setImmediate(() => {\n yieldedMs += performance.now() - t\n resolve()\n })\n }),\n get ms() {\n return yieldedMs\n }\n }\n}\n\n/*\n * 트윈 예측 채널 — 상향 query. 트윈의 \"예측(predict)\" 면(설계 §3.5, ADR-0009).\n * 커널 monteCarloForecast(현재에서 N fork → 지표 분포) + 단일 fork 예측 궤적. 원본 live 무간섭(fork 격리).\n * metric 은 도메인-일반(스냅샷 파생), scenario 는 미래 자극(기본=계속 입고, 실 시스템은 인스턴스/설정 파생).\n */\nconst GTIN = 'urn:epc:idpat:sgtin:0614141.012345.*'\nconst DEFAULT_SCENARIO = {\n seed: 7,\n generators: [\n { kind: 'inbound-arrival', rate: { distribution: 'poisson', meanPerHour: 60 }, content: { skuMix: [{ gtin: GTIN, weight: 1 }], qtyPerLine: { min: 4, max: 12 } } }\n ]\n}\n/*\n * 지표 표는 `forecast-metrics.ts` 가 소유한다 — 여기 있던 것을 옮겼다(시험이 이 파일을 불러올 수 없다).\n *\n * **모르는 지표를 `items` 로 갈음하지 않는다.** 예전에는 `METRICS[metric] ?? METRICS.items` 였다.\n * 에너지 트윈에서 `peakKW` 를 물으면 재고 곡선이 그려지고, 재고가 없으니 **0 이 평평하게** 나왔다 —\n * 화면은 그것을 「피크가 0」으로 읽는다. 지어낸 답보다 모른다고 말하는 편이 낫다.\n */\nfunction metricFn(metric?: string): MetricFn | null {\n if (!metric) return FORECAST_METRICS.items\n return FORECAST_METRICS[metric] ?? null\n}\n\n/* 학습된 보정 저장(P3.2, 인메모리) — key=domain:instance:metric:horizon → {biasShift, halfWidth, …}.\n twinLearnCalibration 이 갭에서 학습해 저장하고, twinForecast 가 있으면 예측에 상시 적용(라이브 보정).\n 데모 트윈은 재부팅 시 저널 리셋이라 세션 스코프로 충분 — DB 영속(TwinForecastScore)은 후속. */\nconst CALIBRATION = new Map<\n string,\n { biasShift: number; halfWidth: number; at: string; samples: number; accBefore: number; accAfter: number; basis?: ModelBasis }\n>()\nconst calKey = (d: string, i: string, m: string, h: number) => `${d}:${i}:${m}:${h}`\n\n/**\n * 이 트윈이 백테스트할 수 있는 구간 — [첫 이벤트, 마지막 이벤트 − H].\n * 지평 H 만큼 앞을 봐야 \"그때 예측한 것\"과 \"실제로 벌어진 것\"을 맞대 볼 수 있으므로 끝에서 H 를 뺀다.\n */\nasync function gapWindow(domainId: string, instanceId: string, H: number): Promise<{ t0: number; lastVantage: number } | null> {\n /*\n * ── **양 끝을 집계로 묻는다** (2026-08-28) ──────────────────────────────────\n *\n * 예전에는 `findOne({ where: { domain: { id } }, order: { eventTime } })` 두 번이었다. 관계를 가진\n * 엔티티에서 `findOne` 은 두 단계 질의가 되고 **안쪽에 상한이 없다** — 그 트윈의 저널을 전부 만든 뒤\n * 한 줄을 고른다. 승화푸드는 337만 행이고, 그것을 두 번 했다.\n *\n * `MIN`/`MAX` 는 `(domain, instance, event_time)` 색인의 양 끝을 집으므로 각각 한 줄이다.\n * 이 파일의 다른 자리와 같은 규율이다(§`twin-engine` 의 `tipOf`).\n */\n const repo = getRepository(TwinEvent)\n const edge = (agg: 'MIN' | 'MAX') =>\n repo\n .createQueryBuilder('e')\n .select(`${agg}(e.eventTime)`, 'at')\n .where('e.domain = :domainId', { domainId })\n .andWhere('e.instanceId = :instanceId', { instanceId })\n .getRawOne<{ at: unknown }>()\n const [firstRow, lastRow] = await Promise.all([edge('MIN'), edge('MAX')])\n /* 드라이버가 준 값은 시간대가 없을 수 있다 — 컬럼 변환을 지나게 한다(§`hydratedDate`). */\n const first = hydratedDate(repo, 'eventTime', firstRow?.at)\n const last = hydratedDate(repo, 'eventTime', lastRow?.at)\n if (!first || !last) return null\n const t0 = first.getTime()\n const lastVantage = last.getTime() - H\n return lastVantage > t0 ? { t0, lastVantage } : null\n}\n\n/**\n * 갭 point 계산(공용) — 주어진 vantage 각각에서 forecast-at-T vs actual-at-T+H(결정적 백테스트).\n *\n * **vantage 는 밖에서 준다.** 예전에는 이 함수가 자기 저널의 첫·마지막 이벤트로 격자를 스스로 만들었다.\n * 그러면 트윈마다 격자가 달라져, 공동배치 합산에서 `points[3]` 끼리 더할 때 **서로 다른 시각의 값**이\n * 더해졌다(자세한 사연은 `aggregatePoints` 주석).\n *\n * 반환 배열은 격자와 **같은 길이**이고, 그 vantage 를 재지 못했으면 그 자리에 `null` 을 둔다.\n * 건너뛰고 밀어 담으면 배열 인덱스가 시간축이기를 그만둔다.\n */\nasync function gapPoints(domainId: string, instanceId: string, m: (s: any) => number, H: number, grid: number[], scen: any): Promise<(any | null)[]> {\n const points: (any | null)[] = []\n for (const T of grid) {\n const k = await TwinEngine.buildForecastKernelAt(domainId, instanceId, new Date(T).toISOString()).catch(() => null)\n if (!k) { points.push(null); continue }\n const dist = await monteCarloForecastAsync(k, { runs: 12, horizonMs: H, scenario: scen, tickMs: forecastStepMs(H), metric: m, yieldFn: breather().fn })\n const naive = m(k.getSnapshot())\n const st = await TwinEngine.recover(domainId, instanceId, undefined, new Date(T + H).toISOString()).catch(() => null)\n if (!st) { points.push(null); continue }\n const a = m(st)\n const p50 = dist?.p50 ?? naive\n points.push({ t: T, p50, min: dist?.min ?? p50, max: dist?.max ?? p50, actual: a, error: a - p50, inBand: a >= (dist?.min ?? -Infinity) && a <= (dist?.max ?? Infinity), naiveAbsError: Math.abs(a - naive), modelAbsError: Math.abs(a - p50) })\n }\n return points\n}\n\n/**\n * 여러 트윈이 **함께** 백테스트할 수 있는 구간과 그 위의 vantage 격자.\n *\n * 겹치는 구간은 각자의 구간을 교집합한 것 — 시작은 가장 늦은 t0, 끝은 가장 이른 lastVantage.\n * 한 트윈이라도 그 시각에 저널이 없으면 공장 총량을 말할 수 없기 때문이다.\n * 겹치는 구간이 없으면 `null` — \"0\" 이 아니라 \"잴 수 없음\" 이다.\n */\nfunction sharedGrid(windows: { t0: number; lastVantage: number }[], N: number): number[] | null {\n if (!windows.length || N < 2) return null\n const t0 = Math.max(...windows.map(w => w.t0))\n const lastVantage = Math.min(...windows.map(w => w.lastVantage))\n if (!(lastVantage > t0)) return null\n return Array.from({ length: N }, (_, i) => t0 + ((lastVantage - t0) * i) / (N - 1))\n}\n\n@Resolver()\nexport class TwinForecastQuery {\n @Query(returns => ScalarObject, {\n nullable: true,\n description: 'Monte-Carlo forecast of a metric over a horizon (twin prediction faculty) + a single-fork predicted trajectory. Live instance is untouched (forks are isolated).'\n })\n async twinForecast(\n @Arg('instanceId') instanceId: string,\n @Arg('metric', { nullable: true }) metric: string,\n @Arg('runs', type => Int, { nullable: true }) runs: number,\n @Arg('horizonMs', type => Int, { nullable: true }) horizonMs: number,\n @Arg('scenario', type => ScalarObject, { nullable: true }) scenario: any,\n @Arg('at', { nullable: true }) at: string,\n @Ctx() context: ResolverContext\n ): Promise<any> {\n /* vantage(at) 기준 예측 — 히스토리 노브가 과거 T 면 \"그때 서서 본 미래\"(백테스트).\n at 있으면 T 시점 커널을 저널로 재구성한다(그건 우회가 아니라 시간여행의 본질).\n 없으면 **지금 그 커널**을 쓴다 — sim 이든 live 든 인스턴스가 커널이다(통합 P2). */\n const domainId = context.state.domain.id\n const k = at\n ? await TwinEngine.buildForecastKernelAt(domainId, instanceId, at)\n : TwinEngine.owns(domainId, instanceId)\n ? TwinEngine.kernel(domainId, instanceId)\n : undefined\n if (!k) return null\n\n const m = metricFn(metric)\n /* 이 트윈이 못 세는 지표 — 빈 예측이 아니라 「그 지표는 여기 없다」고 말한다. */\n if (!m) return { metric, unsupportedMetric: true, distribution: null, trajectory: [] }\n const H = horizonMs ?? 300000\n const R = runs ?? 30\n /*\n * 틱 크기는 **지평선이 정한다**(`forecast-tuning`). 1초 고정이던 시절 5분 지평선이 회차마다\n * 300틱이었고, 30회차면 9,000틱·28.5초였다 — 실측으로 5초 단위와 답이 같다.\n */\n const step = forecastStepMs(H)\n const scen = scenario ?? DEFAULT_SCENARIO\n\n /*\n * 분포: N개 확률적 미래 표본(fork 마다 seed 변주, 원본 보존).\n *\n * **이것이 요청 처리 중 메인 이벤트 루프에서 동기로 수행된다.** 기본값으로 fork 30개 × 300틱 = 9,000틱.\n * 계기판에 남겨 두지 않으면 \"왜 서버가 굳었나\" 를 나중에 짚을 수 없다(실제로 한 번 겪었다).\n */\n const tMc = performance.now()\n const mcBreath = breather()\n const distribution = await monteCarloForecastAsync(k, { runs: R, horizonMs: H, scenario: scen, tickMs: step, metric: m, yieldFn: mcBreath.fn })\n TwinEngine.recordLoad(domainId, instanceId, 'forecast', performance.now() - tMc - mcBreath.ms)\n TwinEngine.countForks(domainId, instanceId, R)\n\n /* 학습된 보정 상시 적용(P3.2) — twinLearnCalibration 이 저장한 편향·밴드가 있으면 예측을 현실로 보정(라이브가 실제로 나아짐).\n 갭트렌드(gapPoints)는 raw 모델을 그대로 측정(학습 신호 보존) — 보정은 표시용 forecast 에만. */\n const stored = CALIBRATION.get(calKey(domainId, instanceId, metric ?? 'items', H))\n /*\n * **배운 모델에만 적용한다.** 보정은 \"그때 모델이 이만큼 치우쳐 있다\" 를 배운 것이다. 소요시간\n * 추정기(실측·거리)나 공정 명세가 바뀌어 모델이 달라지면 그 편향은 더 이상 사실이 아니고, 그대로\n * 밀어 주면 이중 보정이 된다. 그래서 지문을 비교하고, 다르면 적용하지 않되 **그 사실을 응답에\n * 싣는다** — 오류 없이 사라지면 사용자는 학습이 왜 안 듣는지 알 수 없다(다시 배우면 된다).\n * 지문을 알 수 없을 때는 비교하지 않는다(모르는 것을 근거로 보정을 끊지 않는다).\n */\n const basis = await TwinEngine.modelBasis(domainId, instanceId)\n const stale = !!stored && modelBasisChanged(stored.basis, basis)\n if (stale) {\n twinLog(\n `[twin-forecast] \"${instanceId}\": calibration not applied — model changed since it was learned.` +\n ` learned on: ${describeModelBasis(stored!.basis)} / now: ${describeModelBasis(basis)}`\n )\n }\n /*\n * 분위수 — 표본에서 직접 뽑는다.\n *\n * 커널이 표본 배열을 그대로 돌려주므로(forecast.ts) 여기서 정렬해 p05·p95 를 낸다. 이것이 없으면\n * 화면이 보여 줄 수 있는 폭은 관측 **최소·최대** 뿐인데, 그건 신뢰구간이 아니라 극단값 두 개다.\n * \"운이 나쁜 5% 에서는 이만큼\" 이라고 말하려면 분위수여야 한다. 표본이 적으면(runs 가 작으면)\n * 분위수도 거칠다 — 그래서 표본 수를 함께 싣는다(sampleCount). 숫자만 주고 근거를 감추지 않는다.\n */\n if (distribution) {\n const samples: number[] = Array.isArray(distribution.samples) ? distribution.samples : []\n if (samples.length) {\n const sorted = [...samples].sort((a, b) => a - b)\n const pct = (p: number) => sorted[Math.min(sorted.length - 1, Math.floor(p * sorted.length))]\n distribution.p05 = pct(0.05)\n distribution.p95 = pct(0.95)\n }\n distribution.sampleCount = samples.length\n }\n\n const cal = stored && !stale ? stored : undefined\n if (cal && distribution) {\n /*\n * `p50` 은 커널 계약상 항상 있다(`MonteCarloResult.p50: number`). 그래도 메우지 않는다 —\n * 예전에는 `?? 0` 이었는데, 만약 비면 보정된 p50 이 **편향값 그 자체**가 되고 min·max 는\n * 그 둘레로 그려진다. 예측이 아닌 숫자가 예측의 자리에 앉는데 아무 신호가 없다.\n */\n if (typeof distribution.p50 !== 'number') throw new Error('forecast distribution has no p50 — kernel contract broken')\n const p50c = distribution.p50 + cal.biasShift\n distribution.p50 = Math.round(p50c)\n distribution.min = Math.round(p50c - cal.halfWidth)\n distribution.max = Math.round(p50c + cal.halfWidth)\n /*\n * 분위수는 **옮기기만** 한다. 보정이 배운 것은 \"모델이 이만큼 치우쳐 있다\"(위치)이지 분포의\n * 모양이 아니다. p50 처럼 밀어 두어야 p05~p95 폭이 표본이 말한 그대로 남는다.\n * (min·max 는 위에서 학습 밴드로 대체된다 — 보정이 걸리면 그 둘의 뜻이 '관측 극단' 에서\n * '학습 밴드' 로 바뀐다는 뜻이다. 화면이 둘을 같은 것으로 그리면 안 된다.)\n */\n if (distribution.p05 !== undefined) distribution.p05 = Math.round(distribution.p05 + cal.biasShift)\n if (distribution.p95 !== undefined) distribution.p95 = Math.round(distribution.p95 + cal.biasShift)\n if (distribution.p90 !== undefined) distribution.p90 = Math.round(distribution.p90 + cal.biasShift)\n distribution.calibrated = true\n }\n\n /*\n * 예측 궤적(예측선): 단일 fork 를 horizon 까지 실행하며 지표 기록. 이것도 잰다.\n *\n * 여기도 자리를 내준다 — 실측으로 이 한 회차가 1.3초였다(300틱 + 스냅샷 300번). 분포만큼은\n * 아니지만, 라이브 틱 간격(1초)보다 길게 붙잡으면 그 사이 트윈들이 한 박자씩 밀린다.\n */\n const tTraj = performance.now()\n const trajBreath = breather()\n const fc = k.fork()\n fc.scenario.load({ ...scen, seed: 1 })\n fc.scenario.start()\n const trajectory: { t: number; value: number }[] = []\n for (let t = 0; t < H; t += step) {\n fc.tick(step)\n trajectory.push({ t: t + step, value: m(fc.getSnapshot()) })\n /* 틱마다 내주면 양보 자체가 비용이 된다 — 라이브 틱 간격보다 훨씬 짧은 단위로 끊는다. */\n if (trajectory.length % 25 === 0) await trajBreath.fn()\n }\n\n TwinEngine.recordLoad(domainId, instanceId, 'forkTick', performance.now() - tTraj - trajBreath.ms)\n TwinEngine.countForks(domainId, instanceId, 1)\n\n const current = m(k.getSnapshot()) // vantage T 의 실제값(= naive '그대로 유지' 예측의 기준)\n\n /* 실측 오버레이 + 채점(P1) — vantage 가 과거(at)일 때만: T 이후 실제로 일어난 metric 을 T→min(now,T+H) 로 샘플.\n 현재 이후는 실측이 없음(미래). recover(T+t) 로 각 샘플 상태 재구성해 metric 추출. 비용=샘플 수 만큼 replay. */\n const actual: { t: number; value: number }[] = []\n let scored: any = null\n if (at) {\n const T = Date.parse(at)\n const endMs = Math.min(Date.now(), T + H)\n const span = endMs - T\n if (Number.isFinite(T) && span > 0) {\n const SAMPLES = 8\n const dt = span / SAMPLES\n for (let i = 1; i <= SAMPLES; i++) {\n const st = await TwinEngine.recover(domainId, instanceId, undefined, new Date(T + i * dt).toISOString()).catch(() => null)\n if (st) actual.push({ t: i * dt, value: m(st) })\n }\n /* 지평선이 지났으면(T+H ≤ now) 그 지점 실측 vs 예측 채점(단일 vantage). p50 대비 오차·밴드 적중·FVA(naive=현재값 유지 대비). */\n if (T + H <= Date.now() && actual.length) {\n const a = actual[actual.length - 1].value\n const p50 = distribution?.p50 ?? current\n scored = {\n actual: a,\n p50,\n error: a - p50, // 부호 = 편향 방향(+ 실제가 더 많음)\n inBand: a >= (distribution?.min ?? -Infinity) && a <= (distribution?.max ?? Infinity),\n modelAbsError: Math.abs(a - p50),\n naiveAbsError: Math.abs(a - current) // naive = \"그대로 유지\"(현재값). FVA = naive - model\n }\n }\n }\n }\n\n /*\n * 시뮬 명세 자기보고를 함께 낸다 — **상수로 계산한 예측과 현장 명세로 계산한 예측이 화면에서 같아\n * 보이지 않게.** 코드·수치만 싣고 사람이 읽는 문장은 클라가 만든다(언어 중립).\n * 커널이 이 API 를 갖지 않은 버전이면 키를 만들지 않는다(모르는 것을 \"전부 기본값\" 이라 단정 금지).\n */\n /* 보고는 **실제로 실행한 fork** 에서 읽는다 — 부모 커널(k)은 tick 하지 않으므로(예측은 fork 에서\n * 수행된다) 부모에서 읽으면 씨앗으로 세운 예측에서는 늘 비어 있다. 숫자를 낸 주체가 보고해야 한다. */\n const specCoverage = readSpecCoverage(fc) ?? readSpecCoverage(k)\n return {\n metric: metric ?? 'items', horizonMs: H, runs: R, current, distribution, trajectory, actual, scored,\n /* 얼마나 잘게 굴렸나 — 숫자만 주고 근거를 감추지 않는다(같은 규율로 `specCoverage` 도 싣는다). */\n tickMs: step,\n /*\n * **가정이 어떻게 됐나** — 걸린 개입·거절된 개입·덮어쓴 선언.\n *\n * 이것을 감추면 두 가지가 들어온 것이 없어진다: 걸리지 않은 가정(「걸었는데 왜 안 바뀌나」)과, 덮어쓴\n * 선언(사용자가 what-if 결과를 트윈의 현재 상태로 읽는다). 실제로 실행한 fork(`fc`)에서 읽는다 —\n * 가정을 겪은 것은 그쪽이다.\n */\n ...(typeof (fc as any).interventionOutcomes === 'function'\n ? (() => {\n const outcomes = (fc as any).interventionOutcomes()\n return outcomes.length ? { interventions: outcomes } : {}\n })()\n : {}),\n ...(typeof (fc as any).declarationOverrides === 'function'\n ? (() => {\n const overrides = (fc as any).declarationOverrides()\n return overrides.length ? { overrides } : {}\n })()\n : {}),\n calibrated: !!cal,\n /* 학습해 뒀지만 모델이 바뀌어 적용하지 않았다 — 사용자가 다시 배울 수 있게 알린다. */\n ...(stale ? { calibrationStale: true } : {}),\n ...(specCoverage ? { specCoverage } : {}),\n /*\n * **계보에 구멍이 몇 칸 생겼나** — 개체를 잇지 못한 채 넘어간 공정 수.\n *\n * ── 왜 이 값을 예측 응답에 싣나 (2026-08-24) ──────────────────────────────\n * 미러의 오더는 확보분을 갖지 않는다(원본이 「어느 개체가 잡혀 있나」를 말하지 않는 것이 그\n * 시스템의 정상이다). 커널은 그 자리에서 **던지지 않고 넘긴다** — 단계를 넘는 것은 공정의\n * 진행이고 개체 번호가 필요 없다. 대신 넘긴 횟수를 센다.\n *\n * 그런데 그 카운터는 **fork 안에** 있고 fork 는 버려진다. 여기서 싣지 않으면 아무에게도 닿지\n * 않는다 — 리듀서가 오래 세어 온 값을 스냅샷이 떨어뜨려 그것을 읽도록 쓰인 검사가 영원히\n * 아무 신호도 내지 않았던 것과 **같은 실수**다. 세어 놓고 내보내지 않으면 안 센 것과 같다.\n *\n * 왜 화면이 이것을 알아야 하나: 계보는 **회수 범위**를 정하는 사슬이다. 구멍이 있는데 온전한\n * 것처럼 보이면 회수가 알리지 않고 좁아진다. 식품에서 그것은 되돌릴 수 없다.\n *\n * 두 곳에서 읽어 **큰 값**을 낸다 — 분포(회차 R개의 최대)와 궤적 fork 는 **둘 다 실제로 실행됐다**.\n * 한쪽만 보면 다른 쪽이 겪은 구멍을 잃는다.\n */\n ...(() => {\n const gaps = Math.max(\n (distribution as any)?.stepsWithoutMaterial ?? 0,\n (fc as any)?.getSnapshot?.()?.stepsWithoutMaterial ?? 0\n )\n /* 0 은 싣지 않는다 — 「구멍이 없다」와 「이 축을 모른다」를 화면이 구별할 수 있게. */\n return gaps > 0 ? { stepsWithoutMaterial: gaps } : {}\n })()\n }\n }\n\n /*\n * 갭 트렌드(P2) — N개 과거 vantage 각각에서 \"그때 예측 vs 그 지평선의 실제\"를 재계산(결정적 백테스트).\n * 지평선이 이미 지난 vantage 만([t0, tLast−H]) → actual 확정. 온디맨드 재계산(누적/영속은 긴 기간용 후속 최적화).\n * 비용 = N × (buildForecastKernelAt replay + monteCarlo + recover replay). N·runs 를 낮게 bound. per-instance(클라가 co-located 합산).\n */\n @Query(returns => ScalarObject, {\n nullable: true,\n description:\n 'Forecast-accuracy gap diagnosis (canonical, server-computed): for N past vantages recompute forecast-at-T vs actual-at-T+h (deterministic backtest), aggregate co-located instances, and return points + SPC verdict (stability/bias) + metrics (accuracy/coverage/FVA) + learned-calibration projection. Client only renders.'\n })\n async twinForecastGapTrend(\n @Arg('instanceId') instanceId: string,\n @Arg('metric', { nullable: true }) metric: string,\n @Arg('horizonMs', type => Int, { nullable: true }) horizonMs: number,\n @Arg('samples', type => Int, { nullable: true }) samples: number,\n @Arg('scenario', type => ScalarObject, { nullable: true }) scenario: any,\n @Ctx() context: ResolverContext,\n @Arg('instanceIds', type => [String], { nullable: true }) instanceIds?: string[]\n ): Promise<any> {\n const domainId = context.state.domain.id\n const m = metricFn(metric)\n const H = horizonMs ?? 300000\n if (!m) return { metric, unsupportedMetric: true, horizonMs: H, samples: 0, points: [], spc: computeSpc([]), metrics: null, learn: null }\n const N = Math.min(30, Math.max(2, samples ?? GAP_SAMPLES)) // 표본은 서버가 관장(SPC 신뢰 하한 ~20).\n const scen = scenario ?? DEFAULT_SCENARIO\n // 공동배치(co-located) 집계도 서버에서 — 공장 단위 진단이 canonical. 미지정 시 단건.\n const ids = instanceIds?.length ? instanceIds : [instanceId]\n\n /*\n * 격자를 **먼저 하나** 만든다. 트윈마다 자기 저널로 격자를 만들면 같은 인덱스가 서로 다른\n * 시각이 되어, 합산이 다른 시점의 값을 더한다(`aggregatePoints` 주석 참조).\n *\n * 저널이 없거나 지평 H 를 담을 만큼 길지 않은 트윈은 **백테스트에 참여할 수 없다.** 그런 트윈은\n * 빼되 누구를 뺐는지 돌려준다 — 알리지 않고 빼면 화면은 그 트윈까지 포함한 공장 총량으로 읽는다.\n */\n const windows = await Promise.all(ids.map(async id => ({ id, w: await gapWindow(domainId, id, H) })))\n const usable = windows.filter(x => x.w)\n const excluded = windows.filter(x => !x.w).map(x => x.id)\n const grid = sharedGrid(usable.map(x => x.w!), N)\n if (!grid) {\n /* 겹치는 구간이 없다 — 잴 수 없는 것이지 정확도가 0 인 것이 아니다. */\n return { metric: metric ?? 'items', horizonMs: H, samples: N, points: [], spc: computeSpc([]), metrics: null, learn: null,\n coverage: { instances: ids.length, measured: 0, excluded, vantages: N, used: 0, dropped: 0, reason: 'no-common-window' } }\n }\n\n const per = await Promise.all(usable.map(x => gapPoints(domainId, x.id, m, H, grid, scen)))\n const { points, droppedVantages } = aggregatePoints(per as (GapPoint | null)[][])\n const metrics = points.length ? gapMetrics(points) : null\n const spc = computeSpc(points.map(p => p.error))\n const learn = metrics ? learnedCalibration(points, metrics) : null\n return { metric: metric ?? 'items', horizonMs: H, samples: N, points, spc, metrics, learn,\n /* 이 숫자가 **무엇으로 계산됐는지**. 24점 중 18점이면 화면이 그렇게 말해야 한다. */\n coverage: { instances: ids.length, measured: usable.length, excluded, vantages: N, used: points.length, dropped: droppedVantages } }\n }\n\n /*\n * 학습(P3.2) — 갭에서 편향·밴드를 학습해 저장하면, 이후 twinForecast 가 상시 적용(라이브 예측이 실제로 보정됨).\n * assimilation-lite: 잔차(innovation)를 되먹여 추정치를 현실로. per-instance(클라가 co-located 합산).\n * 반환: 학습된 보정 + before/after 정확도(효과 정량화). 저장은 인메모리(세션) — DB 영속은 후속.\n */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, { description: 'Learn a forecast calibration (bias + band) from the gap trend and persist it so future forecasts apply it. Returns the calibration + before/after accuracy.' })\n async twinLearnCalibration(\n @Arg('instanceId') instanceId: string,\n @Arg('metric', { nullable: true }) metric: string,\n @Arg('horizonMs', type => Int, { nullable: true }) horizonMs: number,\n @Ctx() context: ResolverContext,\n @Arg('clear', type => Boolean, { nullable: true }) clear?: boolean\n ): Promise<any> {\n const domainId = context.state.domain.id\n if (!TwinEngine.owns(domainId, instanceId)) return { ok: false } // 테넌트 격리\n const m = metricFn(metric)\n if (!m) return { ok: false, reason: 'unsupported-metric', metric }\n const H = horizonMs ?? 300000\n // 보정 해제 — 저장된 calibration 제거(이후 예측은 raw 로 복귀). 학습을 되돌리는 정직한 off 스위치.\n if (clear) {\n CALIBRATION.delete(calKey(domainId, instanceId, metric ?? 'items', H))\n return { ok: true, cleared: true, metric: metric ?? 'items', horizonMs: H }\n }\n // 표본·계산을 gap-analytics 로 일원화(gapTrend 진단과 동일 N·동일 수식 → 카드 예상치와 실제 보정 일치).\n /* 학습은 단건이라 격자도 이 트윈의 구간 하나다. 저널이 지평 H 를 담을 만큼 길지 않으면 배울 것이 없다. */\n const w = await gapWindow(domainId, instanceId, H)\n const grid = w ? sharedGrid([w], GAP_SAMPLES) : null\n if (!grid) return { ok: false, reason: 'insufficient-history' }\n /* 못 잰 vantage 는 `null` 로 온다 — 0 으로 세면 편향 학습이 그만큼 끌려간다. */\n const pts = (await gapPoints(domainId, instanceId, m, H, grid, DEFAULT_SCENARIO)).filter(Boolean) as GapPoint[]\n if (pts.length < 3) return { ok: false, reason: 'insufficient-history' }\n const learn = learnedCalibration(pts, gapMetrics(pts))\n /* 학습한 모델의 지문을 함께 남긴다 — 이후 모델이 바뀌면 이 보정은 적용되지 않는다. */\n const basis = await TwinEngine.modelBasis(domainId, instanceId)\n const cal = {\n biasShift: learn.biasShift,\n halfWidth: learn.halfWidth,\n at: '',\n samples: pts.length,\n accBefore: learn.accBefore,\n accAfter: learn.accAfter,\n ...(basis ? { basis } : {})\n }\n CALIBRATION.set(calKey(domainId, instanceId, metric ?? 'items', H), cal)\n return { ok: true, metric: metric ?? 'items', horizonMs: H, biasShift: cal.biasShift, halfWidth: cal.halfWidth, samples: pts.length, accBefore: cal.accBefore, accAfter: cal.accAfter }\n }\n}\n"]}
1
+ {"version":3,"file":"twin-forecast-query.js","sourceRoot":"","sources":["../../../server/service/twin-forecast/twin-forecast-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { twinLog } from '../../engine/log.js'\nimport { hydratedDate } from '../../engine/read-time.js'\nimport { Arg, Ctx, Int, Mutation, Query, Resolver, Directive} from 'type-graphql'\n\nimport { getRepository, ScalarObject } from '@things-factory/shell'\n\nimport { TwinEngine } from '../../engine/index.js'\nimport { readSpecCoverage } from '../../engine/spec-coverage.js'\nimport { describeModelBasis, modelBasisChanged, type ModelBasis } from '../../engine/model-basis.js'\nimport { TwinEvent } from '../twin-event/twin-event.js'\nimport { GAP_SAMPLES, aggregatePoints, computeSpc, gapMetrics, learnedCalibration, type GapPoint } from './gap-analytics.js'\nimport { FORECAST_METRICS, type MetricFn } from './forecast-metrics.js'\nimport { forecastStepMs } from './forecast-tuning.js'\n\n/* 커널 예측 함수 — CJS require(twin-engine 과 동일 방식). */\nconst { monteCarloForecastAsync } = require('@operato/twin-kernel')\n\n/**\n * 이벤트 루프에 자리를 내주는 방법 — **커널이 아니라 여기가 안다.**\n *\n * `setImmediate` 는 대기 중인 I/O 다음 차례에 돌아온다. 마이크로태스크(`Promise.resolve()`)로는\n * 자리가 가지 않는다 — 그러면 예측이 도는 동안 라이브 틱도 HTTP 도 여전히 멈춘다.\n *\n * ── 내준 시간은 **부하가 아니다** ───────────────────────────────────────────\n * 부하 계기는 「이 트윈이 루프를 얼마나 붙잡았나」를 재는 자다. 양보를 넣은 뒤로 벽시계 시간에는\n * 남에게 넘긴 시간이 섞이는데, 그것까지 부하로 적으면 계기가 거짓말을 한다 — 하필 이 문제를 짚어낸\n * 그 계기다. 그래서 내준 시간을 따로 세어 빼고 적는다.\n */\nfunction breather() {\n let yieldedMs = 0\n return {\n fn: () =>\n new Promise<void>(resolve => {\n const t = performance.now()\n setImmediate(() => {\n yieldedMs += performance.now() - t\n resolve()\n })\n }),\n get ms() {\n return yieldedMs\n }\n }\n}\n\n/*\n * 트윈 예측 채널 — 상향 query. 트윈의 \"예측(predict)\" 면(설계 §3.5, ADR-0009).\n * 커널 monteCarloForecast(현재에서 N fork → 지표 분포) + 단일 fork 예측 궤적. 원본 live 무간섭(fork 격리).\n * metric 은 도메인-일반(스냅샷 파생), scenario 는 미래 자극(기본=계속 입고, 실 시스템은 인스턴스/설정 파생).\n */\nconst GTIN = 'urn:epc:idpat:sgtin:0614141.012345.*'\nconst DEFAULT_SCENARIO = {\n seed: 7,\n generators: [\n { kind: 'inbound-arrival', rate: { distribution: 'poisson', meanPerHour: 60 }, content: { skuMix: [{ gtin: GTIN, weight: 1 }], qtyPerLine: { min: 4, max: 12 } } }\n ]\n}\n/*\n * 지표 표는 `forecast-metrics.ts` 가 소유한다 — 여기 있던 것을 옮겼다(시험이 이 파일을 불러올 수 없다).\n *\n * **모르는 지표를 `items` 로 갈음하지 않는다.** 예전에는 `METRICS[metric] ?? METRICS.items` 였다.\n * 에너지 트윈에서 `peakKW` 를 물으면 재고 곡선이 그려지고, 재고가 없으니 **0 이 평평하게** 나왔다 —\n * 화면은 그것을 「피크가 0」으로 읽는다. 지어낸 답보다 모른다고 말하는 편이 낫다.\n */\nfunction metricFn(metric?: string): MetricFn | null {\n if (!metric) return FORECAST_METRICS.items\n return FORECAST_METRICS[metric] ?? null\n}\n\n/* 학습된 보정 저장(P3.2, 인메모리) — key=domain:instance:metric:horizon → {biasShift, halfWidth, …}.\n twinLearnCalibration 이 갭에서 학습해 저장하고, twinForecast 가 있으면 예측에 상시 적용(라이브 보정).\n 데모 트윈은 재부팅 시 저널 리셋이라 세션 스코프로 충분 — DB 영속(TwinForecastScore)은 후속. */\nconst CALIBRATION = new Map<\n string,\n { biasShift: number; halfWidth: number; at: string; samples: number; accBefore: number | null; accAfter: number | null; basis?: ModelBasis }\n>()\nconst calKey = (d: string, i: string, m: string, h: number) => `${d}:${i}:${m}:${h}`\n\n/**\n * 이 트윈이 백테스트할 수 있는 구간 — [첫 이벤트, 마지막 이벤트 − H].\n * 지평 H 만큼 앞을 봐야 \"그때 예측한 것\"과 \"실제로 벌어진 것\"을 맞대 볼 수 있으므로 끝에서 H 를 뺀다.\n */\nasync function gapWindow(domainId: string, instanceId: string, H: number): Promise<{ t0: number; lastVantage: number } | null> {\n /*\n * ── **양 끝을 집계로 묻는다** (2026-08-28) ──────────────────────────────────\n *\n * 예전에는 `findOne({ where: { domain: { id } }, order: { eventTime } })` 두 번이었다. 관계를 가진\n * 엔티티에서 `findOne` 은 두 단계 질의가 되고 **안쪽에 상한이 없다** — 그 트윈의 저널을 전부 만든 뒤\n * 한 줄을 고른다. 승화푸드는 337만 행이고, 그것을 두 번 했다.\n *\n * `MIN`/`MAX` 는 `(domain, instance, event_time)` 색인의 양 끝을 집으므로 각각 한 줄이다.\n * 이 파일의 다른 자리와 같은 규율이다(§`twin-engine` 의 `tipOf`).\n */\n const repo = getRepository(TwinEvent)\n const edge = (agg: 'MIN' | 'MAX') =>\n repo\n .createQueryBuilder('e')\n .select(`${agg}(e.eventTime)`, 'at')\n .where('e.domain = :domainId', { domainId })\n .andWhere('e.instanceId = :instanceId', { instanceId })\n .getRawOne<{ at: unknown }>()\n const [firstRow, lastRow] = await Promise.all([edge('MIN'), edge('MAX')])\n /* 드라이버가 준 값은 시간대가 없을 수 있다 — 컬럼 변환을 지나게 한다(§`hydratedDate`). */\n const first = hydratedDate(repo, 'eventTime', firstRow?.at)\n const last = hydratedDate(repo, 'eventTime', lastRow?.at)\n if (!first || !last) return null\n const t0 = first.getTime()\n const lastVantage = last.getTime() - H\n return lastVantage > t0 ? { t0, lastVantage } : null\n}\n\n/**\n * 갭 point 계산(공용) — 주어진 vantage 각각에서 forecast-at-T vs actual-at-T+H(결정적 백테스트).\n *\n * **vantage 는 밖에서 준다.** 예전에는 이 함수가 자기 저널의 첫·마지막 이벤트로 격자를 스스로 만들었다.\n * 그러면 트윈마다 격자가 달라져, 공동배치 합산에서 `points[3]` 끼리 더할 때 **서로 다른 시각의 값**이\n * 더해졌다(자세한 사연은 `aggregatePoints` 주석).\n *\n * 반환 배열은 격자와 **같은 길이**이고, 그 vantage 를 재지 못했으면 그 자리에 `null` 을 둔다.\n * 건너뛰고 밀어 담으면 배열 인덱스가 시간축이기를 그만둔다.\n */\nasync function gapPoints(domainId: string, instanceId: string, m: (s: any) => number, H: number, grid: number[], scen: any): Promise<(any | null)[]> {\n const points: (any | null)[] = []\n for (const T of grid) {\n const k = await TwinEngine.buildForecastKernelAt(domainId, instanceId, new Date(T).toISOString()).catch(() => null)\n if (!k) { points.push(null); continue }\n const dist = await monteCarloForecastAsync(k, { runs: 12, horizonMs: H, scenario: scen, tickMs: forecastStepMs(H), metric: m, yieldFn: breather().fn })\n const naive = m(k.getSnapshot())\n const st = await TwinEngine.recover(domainId, instanceId, undefined, new Date(T + H).toISOString()).catch(() => null)\n if (!st) { points.push(null); continue }\n const a = m(st)\n const p50 = dist?.p50 ?? naive\n points.push({ t: T, p50, min: dist?.min ?? p50, max: dist?.max ?? p50, actual: a, error: a - p50, inBand: a >= (dist?.min ?? -Infinity) && a <= (dist?.max ?? Infinity), naiveAbsError: Math.abs(a - naive), modelAbsError: Math.abs(a - p50) })\n }\n return points\n}\n\n/**\n * 여러 트윈이 **함께** 백테스트할 수 있는 구간과 그 위의 vantage 격자.\n *\n * 겹치는 구간은 각자의 구간을 교집합한 것 — 시작은 가장 늦은 t0, 끝은 가장 이른 lastVantage.\n * 한 트윈이라도 그 시각에 저널이 없으면 공장 총량을 말할 수 없기 때문이다.\n * 겹치는 구간이 없으면 `null` — \"0\" 이 아니라 \"잴 수 없음\" 이다.\n */\nfunction sharedGrid(windows: { t0: number; lastVantage: number }[], N: number): number[] | null {\n if (!windows.length || N < 2) return null\n const t0 = Math.max(...windows.map(w => w.t0))\n const lastVantage = Math.min(...windows.map(w => w.lastVantage))\n if (!(lastVantage > t0)) return null\n return Array.from({ length: N }, (_, i) => t0 + ((lastVantage - t0) * i) / (N - 1))\n}\n\n@Resolver()\nexport class TwinForecastQuery {\n @Query(returns => ScalarObject, {\n nullable: true,\n description: 'Monte-Carlo forecast of a metric over a horizon (twin prediction faculty) + a single-fork predicted trajectory. Live instance is untouched (forks are isolated).'\n })\n async twinForecast(\n @Arg('instanceId') instanceId: string,\n @Arg('metric', { nullable: true }) metric: string,\n @Arg('runs', type => Int, { nullable: true }) runs: number,\n @Arg('horizonMs', type => Int, { nullable: true }) horizonMs: number,\n @Arg('scenario', type => ScalarObject, { nullable: true }) scenario: any,\n @Arg('at', { nullable: true }) at: string,\n @Ctx() context: ResolverContext\n ): Promise<any> {\n /* vantage(at) 기준 예측 — 히스토리 노브가 과거 T 면 \"그때 서서 본 미래\"(백테스트).\n at 있으면 T 시점 커널을 저널로 재구성한다(그건 우회가 아니라 시간여행의 본질).\n 없으면 **지금 그 커널**을 쓴다 — sim 이든 live 든 인스턴스가 커널이다(통합 P2). */\n const domainId = context.state.domain.id\n const k = at\n ? await TwinEngine.buildForecastKernelAt(domainId, instanceId, at)\n : TwinEngine.owns(domainId, instanceId)\n ? TwinEngine.kernel(domainId, instanceId)\n : undefined\n if (!k) return null\n\n const m = metricFn(metric)\n /* 이 트윈이 못 세는 지표 — 빈 예측이 아니라 「그 지표는 여기 없다」고 말한다. */\n if (!m) return { metric, unsupportedMetric: true, distribution: null, trajectory: [] }\n const H = horizonMs ?? 300000\n const R = runs ?? 30\n /*\n * 틱 크기는 **지평선이 정한다**(`forecast-tuning`). 1초 고정이던 시절 5분 지평선이 회차마다\n * 300틱이었고, 30회차면 9,000틱·28.5초였다 — 실측으로 5초 단위와 답이 같다.\n */\n const step = forecastStepMs(H)\n const scen = scenario ?? DEFAULT_SCENARIO\n\n /*\n * 분포: N개 확률적 미래 표본(fork 마다 seed 변주, 원본 보존).\n *\n * **이것이 요청 처리 중 메인 이벤트 루프에서 동기로 수행된다.** 기본값으로 fork 30개 × 300틱 = 9,000틱.\n * 계기판에 남겨 두지 않으면 \"왜 서버가 굳었나\" 를 나중에 짚을 수 없다(실제로 한 번 겪었다).\n */\n const tMc = performance.now()\n const mcBreath = breather()\n const distribution = await monteCarloForecastAsync(k, { runs: R, horizonMs: H, scenario: scen, tickMs: step, metric: m, yieldFn: mcBreath.fn })\n TwinEngine.recordLoad(domainId, instanceId, 'forecast', performance.now() - tMc - mcBreath.ms)\n TwinEngine.countForks(domainId, instanceId, R)\n\n /* 학습된 보정 상시 적용(P3.2) — twinLearnCalibration 이 저장한 편향·밴드가 있으면 예측을 현실로 보정(라이브가 실제로 나아짐).\n 갭트렌드(gapPoints)는 raw 모델을 그대로 측정(학습 신호 보존) — 보정은 표시용 forecast 에만. */\n const stored = CALIBRATION.get(calKey(domainId, instanceId, metric ?? 'items', H))\n /*\n * **배운 모델에만 적용한다.** 보정은 \"그때 모델이 이만큼 치우쳐 있다\" 를 배운 것이다. 소요시간\n * 추정기(실측·거리)나 공정 명세가 바뀌어 모델이 달라지면 그 편향은 더 이상 사실이 아니고, 그대로\n * 밀어 주면 이중 보정이 된다. 그래서 지문을 비교하고, 다르면 적용하지 않되 **그 사실을 응답에\n * 싣는다** — 오류 없이 사라지면 사용자는 학습이 왜 안 듣는지 알 수 없다(다시 배우면 된다).\n * 지문을 알 수 없을 때는 비교하지 않는다(모르는 것을 근거로 보정을 끊지 않는다).\n */\n const basis = await TwinEngine.modelBasis(domainId, instanceId)\n const stale = !!stored && modelBasisChanged(stored.basis, basis)\n if (stale) {\n twinLog(\n `[twin-forecast] \"${instanceId}\": calibration not applied — model changed since it was learned.` +\n ` learned on: ${describeModelBasis(stored!.basis)} / now: ${describeModelBasis(basis)}`\n )\n }\n /*\n * 분위수 — 표본에서 직접 뽑는다.\n *\n * 커널이 표본 배열을 그대로 돌려주므로(forecast.ts) 여기서 정렬해 p05·p95 를 낸다. 이것이 없으면\n * 화면이 보여 줄 수 있는 폭은 관측 **최소·최대** 뿐인데, 그건 신뢰구간이 아니라 극단값 두 개다.\n * \"운이 나쁜 5% 에서는 이만큼\" 이라고 말하려면 분위수여야 한다. 표본이 적으면(runs 가 작으면)\n * 분위수도 거칠다 — 그래서 표본 수를 함께 싣는다(sampleCount). 숫자만 주고 근거를 감추지 않는다.\n */\n if (distribution) {\n const samples: number[] = Array.isArray(distribution.samples) ? distribution.samples : []\n if (samples.length) {\n const sorted = [...samples].sort((a, b) => a - b)\n const pct = (p: number) => sorted[Math.min(sorted.length - 1, Math.floor(p * sorted.length))]\n distribution.p05 = pct(0.05)\n distribution.p95 = pct(0.95)\n }\n distribution.sampleCount = samples.length\n }\n\n const cal = stored && !stale ? stored : undefined\n if (cal && distribution) {\n /*\n * `p50` 은 커널 계약상 항상 있다(`MonteCarloResult.p50: number`). 그래도 메우지 않는다 —\n * 예전에는 `?? 0` 이었는데, 만약 비면 보정된 p50 이 **편향값 그 자체**가 되고 min·max 는\n * 그 둘레로 그려진다. 예측이 아닌 숫자가 예측의 자리에 앉는데 아무 신호가 없다.\n */\n if (typeof distribution.p50 !== 'number') throw new Error('forecast distribution has no p50 — kernel contract broken')\n const p50c = distribution.p50 + cal.biasShift\n distribution.p50 = Math.round(p50c)\n distribution.min = Math.round(p50c - cal.halfWidth)\n distribution.max = Math.round(p50c + cal.halfWidth)\n /*\n * 분위수는 **옮기기만** 한다. 보정이 배운 것은 \"모델이 이만큼 치우쳐 있다\"(위치)이지 분포의\n * 모양이 아니다. p50 처럼 밀어 두어야 p05~p95 폭이 표본이 말한 그대로 남는다.\n * (min·max 는 위에서 학습 밴드로 대체된다 — 보정이 걸리면 그 둘의 뜻이 '관측 극단' 에서\n * '학습 밴드' 로 바뀐다는 뜻이다. 화면이 둘을 같은 것으로 그리면 안 된다.)\n */\n if (distribution.p05 !== undefined) distribution.p05 = Math.round(distribution.p05 + cal.biasShift)\n if (distribution.p95 !== undefined) distribution.p95 = Math.round(distribution.p95 + cal.biasShift)\n if (distribution.p90 !== undefined) distribution.p90 = Math.round(distribution.p90 + cal.biasShift)\n distribution.calibrated = true\n }\n\n /*\n * 예측 궤적(예측선): 단일 fork 를 horizon 까지 실행하며 지표 기록. 이것도 잰다.\n *\n * 여기도 자리를 내준다 — 실측으로 이 한 회차가 1.3초였다(300틱 + 스냅샷 300번). 분포만큼은\n * 아니지만, 라이브 틱 간격(1초)보다 길게 붙잡으면 그 사이 트윈들이 한 박자씩 밀린다.\n */\n const tTraj = performance.now()\n const trajBreath = breather()\n const fc = k.fork()\n fc.scenario.load({ ...scen, seed: 1 })\n fc.scenario.start()\n const trajectory: { t: number; value: number }[] = []\n for (let t = 0; t < H; t += step) {\n fc.tick(step)\n trajectory.push({ t: t + step, value: m(fc.getSnapshot()) })\n /* 틱마다 내주면 양보 자체가 비용이 된다 — 라이브 틱 간격보다 훨씬 짧은 단위로 끊는다. */\n if (trajectory.length % 25 === 0) await trajBreath.fn()\n }\n\n TwinEngine.recordLoad(domainId, instanceId, 'forkTick', performance.now() - tTraj - trajBreath.ms)\n TwinEngine.countForks(domainId, instanceId, 1)\n\n const current = m(k.getSnapshot()) // vantage T 의 실제값(= naive '그대로 유지' 예측의 기준)\n\n /* 실측 오버레이 + 채점(P1) — vantage 가 과거(at)일 때만: T 이후 실제로 일어난 metric 을 T→min(now,T+H) 로 샘플.\n 현재 이후는 실측이 없음(미래). recover(T+t) 로 각 샘플 상태 재구성해 metric 추출. 비용=샘플 수 만큼 replay. */\n const actual: { t: number; value: number }[] = []\n let scored: any = null\n if (at) {\n const T = Date.parse(at)\n const endMs = Math.min(Date.now(), T + H)\n const span = endMs - T\n if (Number.isFinite(T) && span > 0) {\n const SAMPLES = 8\n const dt = span / SAMPLES\n for (let i = 1; i <= SAMPLES; i++) {\n const st = await TwinEngine.recover(domainId, instanceId, undefined, new Date(T + i * dt).toISOString()).catch(() => null)\n if (st) actual.push({ t: i * dt, value: m(st) })\n }\n /* 지평선이 지났으면(T+H ≤ now) 그 지점 실측 vs 예측 채점(단일 vantage). p50 대비 오차·밴드 적중·FVA(naive=현재값 유지 대비). */\n if (T + H <= Date.now() && actual.length) {\n const a = actual[actual.length - 1].value\n const p50 = distribution?.p50 ?? current\n scored = {\n actual: a,\n p50,\n error: a - p50, // 부호 = 편향 방향(+ 실제가 더 많음)\n inBand: a >= (distribution?.min ?? -Infinity) && a <= (distribution?.max ?? Infinity),\n modelAbsError: Math.abs(a - p50),\n naiveAbsError: Math.abs(a - current) // naive = \"그대로 유지\"(현재값). FVA = naive - model\n }\n }\n }\n }\n\n /*\n * 시뮬 명세 자기보고를 함께 낸다 — **상수로 계산한 예측과 현장 명세로 계산한 예측이 화면에서 같아\n * 보이지 않게.** 코드·수치만 싣고 사람이 읽는 문장은 클라가 만든다(언어 중립).\n * 커널이 이 API 를 갖지 않은 버전이면 키를 만들지 않는다(모르는 것을 \"전부 기본값\" 이라 단정 금지).\n */\n /* 보고는 **실제로 실행한 fork** 에서 읽는다 — 부모 커널(k)은 tick 하지 않으므로(예측은 fork 에서\n * 수행된다) 부모에서 읽으면 씨앗으로 세운 예측에서는 늘 비어 있다. 숫자를 낸 주체가 보고해야 한다. */\n const specCoverage = readSpecCoverage(fc) ?? readSpecCoverage(k)\n return {\n metric: metric ?? 'items', horizonMs: H, runs: R, current, distribution, trajectory, actual, scored,\n /* 얼마나 잘게 굴렸나 — 숫자만 주고 근거를 감추지 않는다(같은 규율로 `specCoverage` 도 싣는다). */\n tickMs: step,\n /*\n * **가정이 어떻게 됐나** — 걸린 개입·거절된 개입·덮어쓴 선언.\n *\n * 이것을 감추면 두 가지가 들어온 것이 없어진다: 걸리지 않은 가정(「걸었는데 왜 안 바뀌나」)과, 덮어쓴\n * 선언(사용자가 what-if 결과를 트윈의 현재 상태로 읽는다). 실제로 실행한 fork(`fc`)에서 읽는다 —\n * 가정을 겪은 것은 그쪽이다.\n */\n ...(typeof (fc as any).interventionOutcomes === 'function'\n ? (() => {\n const outcomes = (fc as any).interventionOutcomes()\n return outcomes.length ? { interventions: outcomes } : {}\n })()\n : {}),\n ...(typeof (fc as any).declarationOverrides === 'function'\n ? (() => {\n const overrides = (fc as any).declarationOverrides()\n return overrides.length ? { overrides } : {}\n })()\n : {}),\n calibrated: !!cal,\n /* 학습해 뒀지만 모델이 바뀌어 적용하지 않았다 — 사용자가 다시 배울 수 있게 알린다. */\n ...(stale ? { calibrationStale: true } : {}),\n ...(specCoverage ? { specCoverage } : {}),\n /*\n * **계보에 구멍이 몇 칸 생겼나** — 개체를 잇지 못한 채 넘어간 공정 수.\n *\n * ── 왜 이 값을 예측 응답에 싣나 (2026-08-24) ──────────────────────────────\n * 미러의 오더는 확보분을 갖지 않는다(원본이 「어느 개체가 잡혀 있나」를 말하지 않는 것이 그\n * 시스템의 정상이다). 커널은 그 자리에서 **던지지 않고 넘긴다** — 단계를 넘는 것은 공정의\n * 진행이고 개체 번호가 필요 없다. 대신 넘긴 횟수를 센다.\n *\n * 그런데 그 카운터는 **fork 안에** 있고 fork 는 버려진다. 여기서 싣지 않으면 아무에게도 닿지\n * 않는다 — 리듀서가 오래 세어 온 값을 스냅샷이 떨어뜨려 그것을 읽도록 쓰인 검사가 영원히\n * 아무 신호도 내지 않았던 것과 **같은 실수**다. 세어 놓고 내보내지 않으면 안 센 것과 같다.\n *\n * 왜 화면이 이것을 알아야 하나: 계보는 **회수 범위**를 정하는 사슬이다. 구멍이 있는데 온전한\n * 것처럼 보이면 회수가 알리지 않고 좁아진다. 식품에서 그것은 되돌릴 수 없다.\n *\n * 두 곳에서 읽어 **큰 값**을 낸다 — 분포(회차 R개의 최대)와 궤적 fork 는 **둘 다 실제로 실행됐다**.\n * 한쪽만 보면 다른 쪽이 겪은 구멍을 잃는다.\n */\n ...(() => {\n const gaps = Math.max(\n (distribution as any)?.stepsWithoutMaterial ?? 0,\n (fc as any)?.getSnapshot?.()?.stepsWithoutMaterial ?? 0\n )\n /* 0 은 싣지 않는다 — 「구멍이 없다」와 「이 축을 모른다」를 화면이 구별할 수 있게. */\n return gaps > 0 ? { stepsWithoutMaterial: gaps } : {}\n })()\n }\n }\n\n /*\n * 갭 트렌드(P2) — N개 과거 vantage 각각에서 \"그때 예측 vs 그 지평선의 실제\"를 재계산(결정적 백테스트).\n * 지평선이 이미 지난 vantage 만([t0, tLast−H]) → actual 확정. 온디맨드 재계산(누적/영속은 긴 기간용 후속 최적화).\n * 비용 = N × (buildForecastKernelAt replay + monteCarlo + recover replay). N·runs 를 낮게 bound. per-instance(클라가 co-located 합산).\n */\n @Query(returns => ScalarObject, {\n nullable: true,\n description:\n 'Forecast-accuracy gap diagnosis (canonical, server-computed): for N past vantages recompute forecast-at-T vs actual-at-T+h (deterministic backtest), aggregate co-located instances, and return points + SPC verdict (stability/bias) + metrics (accuracy/coverage/FVA) + learned-calibration projection. Client only renders.'\n })\n async twinForecastGapTrend(\n @Arg('instanceId') instanceId: string,\n @Arg('metric', { nullable: true }) metric: string,\n @Arg('horizonMs', type => Int, { nullable: true }) horizonMs: number,\n @Arg('samples', type => Int, { nullable: true }) samples: number,\n @Arg('scenario', type => ScalarObject, { nullable: true }) scenario: any,\n @Ctx() context: ResolverContext,\n @Arg('instanceIds', type => [String], { nullable: true }) instanceIds?: string[]\n ): Promise<any> {\n const domainId = context.state.domain.id\n const m = metricFn(metric)\n const H = horizonMs ?? 300000\n if (!m) return { metric, unsupportedMetric: true, horizonMs: H, samples: 0, points: [], spc: computeSpc([]), metrics: null, learn: null }\n const N = Math.min(30, Math.max(2, samples ?? GAP_SAMPLES)) // 표본은 서버가 관장(SPC 신뢰 하한 ~20).\n const scen = scenario ?? DEFAULT_SCENARIO\n // 공동배치(co-located) 집계도 서버에서 — 공장 단위 진단이 canonical. 미지정 시 단건.\n const ids = instanceIds?.length ? instanceIds : [instanceId]\n\n /*\n * 격자를 **먼저 하나** 만든다. 트윈마다 자기 저널로 격자를 만들면 같은 인덱스가 서로 다른\n * 시각이 되어, 합산이 다른 시점의 값을 더한다(`aggregatePoints` 주석 참조).\n *\n * 저널이 없거나 지평 H 를 담을 만큼 길지 않은 트윈은 **백테스트에 참여할 수 없다.** 그런 트윈은\n * 빼되 누구를 뺐는지 돌려준다 — 알리지 않고 빼면 화면은 그 트윈까지 포함한 공장 총량으로 읽는다.\n */\n const windows = await Promise.all(ids.map(async id => ({ id, w: await gapWindow(domainId, id, H) })))\n const usable = windows.filter(x => x.w)\n const excluded = windows.filter(x => !x.w).map(x => x.id)\n const grid = sharedGrid(usable.map(x => x.w!), N)\n if (!grid) {\n /* 겹치는 구간이 없다 — 잴 수 없는 것이지 정확도가 0 인 것이 아니다. */\n return { metric: metric ?? 'items', horizonMs: H, samples: N, points: [], spc: computeSpc([]), metrics: null, learn: null,\n coverage: { instances: ids.length, measured: 0, excluded, vantages: N, used: 0, dropped: 0, reason: 'no-common-window' } }\n }\n\n const per = await Promise.all(usable.map(x => gapPoints(domainId, x.id, m, H, grid, scen)))\n const { points, droppedVantages } = aggregatePoints(per as (GapPoint | null)[][])\n const metrics = points.length ? gapMetrics(points) : null\n const spc = computeSpc(points.map(p => p.error))\n const learn = metrics ? learnedCalibration(points, metrics) : null\n return { metric: metric ?? 'items', horizonMs: H, samples: N, points, spc, metrics, learn,\n /* 이 숫자가 **무엇으로 계산됐는지**. 24점 중 18점이면 화면이 그렇게 말해야 한다. */\n coverage: { instances: ids.length, measured: usable.length, excluded, vantages: N, used: points.length, dropped: droppedVantages } }\n }\n\n /*\n * 학습(P3.2) — 갭에서 편향·밴드를 학습해 저장하면, 이후 twinForecast 가 상시 적용(라이브 예측이 실제로 보정됨).\n * assimilation-lite: 잔차(innovation)를 되먹여 추정치를 현실로. per-instance(클라가 co-located 합산).\n * 반환: 학습된 보정 + before/after 정확도(효과 정량화). 저장은 인메모리(세션) — DB 영속은 후속.\n */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, { description: 'Learn a forecast calibration (bias + band) from the gap trend and persist it so future forecasts apply it. Returns the calibration + before/after accuracy.' })\n async twinLearnCalibration(\n @Arg('instanceId') instanceId: string,\n @Arg('metric', { nullable: true }) metric: string,\n @Arg('horizonMs', type => Int, { nullable: true }) horizonMs: number,\n @Ctx() context: ResolverContext,\n @Arg('clear', type => Boolean, { nullable: true }) clear?: boolean\n ): Promise<any> {\n const domainId = context.state.domain.id\n if (!TwinEngine.owns(domainId, instanceId)) return { ok: false } // 테넌트 격리\n const m = metricFn(metric)\n if (!m) return { ok: false, reason: 'unsupported-metric', metric }\n const H = horizonMs ?? 300000\n // 보정 해제 — 저장된 calibration 제거(이후 예측은 raw 로 복귀). 학습을 되돌리는 정직한 off 스위치.\n if (clear) {\n CALIBRATION.delete(calKey(domainId, instanceId, metric ?? 'items', H))\n return { ok: true, cleared: true, metric: metric ?? 'items', horizonMs: H }\n }\n // 표본·계산을 gap-analytics 로 일원화(gapTrend 진단과 동일 N·동일 수식 → 카드 예상치와 실제 보정 일치).\n /* 학습은 단건이라 격자도 이 트윈의 구간 하나다. 저널이 지평 H 를 담을 만큼 길지 않으면 배울 것이 없다. */\n const w = await gapWindow(domainId, instanceId, H)\n const grid = w ? sharedGrid([w], GAP_SAMPLES) : null\n if (!grid) return { ok: false, reason: 'insufficient-history' }\n /* 못 잰 vantage 는 `null` 로 온다 — 0 으로 세면 편향 학습이 그만큼 끌려간다. */\n const pts = (await gapPoints(domainId, instanceId, m, H, grid, DEFAULT_SCENARIO)).filter(Boolean) as GapPoint[]\n if (pts.length < 3) return { ok: false, reason: 'insufficient-history' }\n const learn = learnedCalibration(pts, gapMetrics(pts))\n /* 학습한 모델의 지문을 함께 남긴다 — 이후 모델이 바뀌면 이 보정은 적용되지 않는다. */\n const basis = await TwinEngine.modelBasis(domainId, instanceId)\n const cal = {\n biasShift: learn.biasShift,\n halfWidth: learn.halfWidth,\n at: '',\n samples: pts.length,\n accBefore: learn.accBefore,\n accAfter: learn.accAfter,\n ...(basis ? { basis } : {})\n }\n CALIBRATION.set(calKey(domainId, instanceId, metric ?? 'items', H), cal)\n return { ok: true, metric: metric ?? 'items', horizonMs: H, biasShift: cal.biasShift, halfWidth: cal.halfWidth, samples: pts.length, accBefore: cal.accBefore, accAfter: cal.accAfter }\n }\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import { type TwinStatePayload } from '../../engine/twin-state-channel.js';
1
2
  export declare class TwinStateMessage {
2
3
  instanceId: string;
3
4
  kind: string;
@@ -5,7 +6,5 @@ export declare class TwinStateMessage {
5
6
  payload?: any;
6
7
  }
7
8
  export declare class TwinStateSubscription {
8
- twinState(payload: {
9
- twinState: TwinStateMessage;
10
- }, instanceId: string): TwinStateMessage;
9
+ twinState(payload: TwinStatePayload, instanceId: string): TwinStateMessage;
11
10
  }
@@ -6,6 +6,7 @@ const graphql_yoga_1 = require("graphql-yoga");
6
6
  const type_graphql_1 = require("type-graphql");
7
7
  const shell_1 = require("@things-factory/shell");
8
8
  const index_js_1 = require("../../engine/index.js");
9
+ const twin_state_channel_js_1 = require("../../engine/twin-state-channel.js");
9
10
  /* State 채널 메시지 — snapshot | delta | clock 멀티플렉싱(커널 SubscriptionMessage 대응). */
10
11
  let TwinStateMessage = class TwinStateMessage {
11
12
  };
@@ -31,7 +32,15 @@ exports.TwinStateMessage = TwinStateMessage = tslib_1.__decorate([
31
32
  ], TwinStateMessage);
32
33
  /*
33
34
  * 트윈 State 구독 — state-register/data-resolver 패턴 미러.
34
- * 구독 즉시 현재 snapshot 발행(process.nextTick) 후 delta/clock 스트림. instanceId 로 필터.
35
+ * 구독 즉시 현재 snapshot 발행(process.nextTick) 후 delta/clock 스트림.
36
+ *
37
+ * The tenant is asked while streaming, not only at subscribe time (2026-09-24). Ownership used to be
38
+ * checked once and the stream filtered on instanceId alone; the same id in two tenants is the normal
39
+ * case (`runtime-key.ts`), so other tenants' deltas came through, and the snapshot sent to a new
40
+ * subscriber reached every tenant subscribed to that id. Messages now carry their domain and the filter
41
+ * compares it (`engine/twin-state-channel.ts`). A twin the caller does not own is refused rather than
42
+ * given an empty stream — an empty stream cannot tell "nothing changed" from "you may not see this"
43
+ * (shell's `data` throws too).
35
44
  */
36
45
  let TwinStateSubscription = class TwinStateSubscription {
37
46
  twinState(payload, instanceId) {
@@ -41,23 +50,21 @@ let TwinStateSubscription = class TwinStateSubscription {
41
50
  exports.TwinStateSubscription = TwinStateSubscription;
42
51
  tslib_1.__decorate([
43
52
  (0, type_graphql_1.Subscription)(returns => TwinStateMessage, {
44
- subscribe: ({ args, context }) => {
53
+ subscribe: async ({ args, context }) => {
45
54
  const { instanceId } = args;
46
- const domainId = context?.state?.domain?.id;
47
- /* 테넌트 격리 — 이 도메인 소유 인스턴스가 아니면 스냅샷도 스트림도 흘리지 않는다(빈 구독). */
48
- if (!domainId || !index_js_1.TwinEngine.owns(domainId, instanceId)) {
49
- return (0, graphql_yoga_1.pipe)(shell_1.pubsub.subscribe('twin-state'), (0, graphql_yoga_1.filter)(() => false));
50
- }
51
- /* 구독 순간 현재 상태를 먼저 발행(late 구독자도 snapshot→delta 순서 보장). */
55
+ const { domain, user } = context?.state ?? {};
56
+ await (0, shell_1.assertDomainSubscribeAllowed)(domain, user, (d, u) => process.superUserGranted(d, u));
57
+ /* One refusal for both cases, so the answer does not reveal whether another tenant has such a twin. */
58
+ if (!index_js_1.TwinEngine.owns(domain.id, instanceId))
59
+ throw new Error(`twin instance not running in this tenant: ${instanceId}`);
60
+ /* Current state first (a late subscriber still sees snapshot → delta). It reaches this tenant's subscribers only. */
52
61
  process.nextTick(() => {
53
- const snap = index_js_1.TwinEngine.snapshot(domainId, instanceId);
62
+ const snap = index_js_1.TwinEngine.snapshot(domain.id, instanceId);
54
63
  if (snap) {
55
- shell_1.pubsub.publish('twin-state', {
56
- twinState: { instanceId, kind: 'snapshot', revision: snap.revision, payload: snap }
57
- });
64
+ shell_1.pubsub.publish(twin_state_channel_js_1.TWIN_STATE_TOPIC, (0, twin_state_channel_js_1.twinStatePayload)(domain.id, { instanceId, kind: 'snapshot', revision: snap.revision, payload: snap }));
58
65
  }
59
66
  });
60
- return (0, graphql_yoga_1.pipe)(shell_1.pubsub.subscribe('twin-state'), (0, graphql_yoga_1.filter)((payload) => payload.twinState.instanceId === instanceId));
67
+ return (0, graphql_yoga_1.pipe)(shell_1.pubsub.subscribe(twin_state_channel_js_1.TWIN_STATE_TOPIC), (0, graphql_yoga_1.filter)((0, twin_state_channel_js_1.twinStateFilter)(domain.id, instanceId)));
61
68
  }
62
69
  }),
63
70
  tslib_1.__param(0, (0, type_graphql_1.Root)()),
@@ -1 +1 @@
1
- {"version":3,"file":"twin-state-subscription.js","sourceRoot":"","sources":["../../../server/service/twin-state/twin-state-subscription.ts"],"names":[],"mappings":";;;;AAAA,+CAA2C;AAC3C,+CAAwF;AAExF,iDAA4D;AAE5D,oDAAkD;AAElD,+EAA+E;AAExE,IAAM,gBAAgB,GAAtB,MAAM,gBAAgB;CAY5B,CAAA;AAZY,4CAAgB;AAE3B;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,mDAAmD,EAAE,CAAC;;oDAC1D;AAGlB;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,yCAAyC,EAAE,CAAC;;8CACtD;AAGZ;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,2BAA2B,EAAE,CAAC;;kDAChE;AAGjB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,oBAAY,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,uFAAuF,EAAE,CAAC;;iDACzI;2BAXF,gBAAgB;IAD5B,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,mEAAmE,EAAE,CAAC;GACpF,gBAAgB,CAY5B;AAED;;;GAGG;AAEI,IAAM,qBAAqB,GAA3B,MAAM,qBAAqB;IA2BhC,SAAS,CAAS,OAAwC,EAAqB,UAAkB;QAC/F,OAAO,OAAO,CAAC,SAAS,CAAA;IAC1B,CAAC;CACF,CAAA;AA9BY,sDAAqB;AA2BhC;IA1BC,IAAA,2BAAY,EAAC,OAAO,CAAC,EAAE,CAAC,gBAAgB,EAAE;QACzC,SAAS,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE;YAC/B,MAAM,EAAE,UAAU,EAAE,GAAG,IAAI,CAAA;YAC3B,MAAM,QAAQ,GAAI,OAAe,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,CAAA;YAEpD,0DAA0D;YAC1D,IAAI,CAAC,QAAQ,IAAI,CAAC,qBAAU,CAAC,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,EAAE,CAAC;gBACxD,OAAO,IAAA,mBAAI,EAAC,cAAM,CAAC,SAAS,CAAC,YAAY,CAAC,EAAE,IAAA,qBAAM,EAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,CAAA;YAClE,CAAC;YAED,yDAAyD;YACzD,OAAO,CAAC,QAAQ,CAAC,GAAG,EAAE;gBACpB,MAAM,IAAI,GAAG,qBAAU,CAAC,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAA;gBACtD,IAAI,IAAI,EAAE,CAAC;oBACT,cAAM,CAAC,OAAO,CAAC,YAAY,EAAE;wBAC3B,SAAS,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE;qBACpF,CAAC,CAAA;gBACJ,CAAC;YACH,CAAC,CAAC,CAAA;YAEF,OAAO,IAAA,mBAAI,EACT,cAAM,CAAC,SAAS,CAAC,YAAY,CAAC,EAC9B,IAAA,qBAAM,EAAC,CAAC,OAAwC,EAAE,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,UAAU,KAAK,UAAU,CAAC,CAClG,CAAA;QACH,CAAC;KACF,CAAC;IACS,mBAAA,IAAA,mBAAI,GAAE,CAAA;IAA4C,mBAAA,IAAA,kBAAG,EAAC,YAAY,CAAC,CAAA;;;4CAAsB,gBAAgB;sDAEnH;gCA7BU,qBAAqB;IADjC,IAAA,uBAAQ,GAAE;GACE,qBAAqB,CA8BjC","sourcesContent":["import { filter, pipe } from 'graphql-yoga'\nimport { Arg, Field, Int, ObjectType, Resolver, Root, Subscription } from 'type-graphql'\n\nimport { pubsub, ScalarObject } from '@things-factory/shell'\n\nimport { TwinEngine } from '../../engine/index.js'\n\n/* State 채널 메시지 — snapshot | delta | clock 멀티플렉싱(커널 SubscriptionMessage 대응). */\n@ObjectType({ description: 'Twin State channel message multiplexing snapshot | delta | clock.' })\nexport class TwinStateMessage {\n @Field({ description: 'Twin runtime instance id this message belongs to.' })\n instanceId: string\n\n @Field({ description: 'Message kind: snapshot | delta | clock.' })\n kind: string\n\n @Field(type => Int, { nullable: true, description: 'Monotonic state revision.' })\n revision?: number\n\n @Field(type => ScalarObject, { nullable: true, description: 'Payload — snapshot: StateSnapshot / delta: CanonicalEnvelope / clock: { simClockMs }.' })\n payload?: any\n}\n\n/*\n * 트윈 State 구독 — state-register/data-resolver 패턴 미러.\n * 구독 즉시 현재 snapshot 발행(process.nextTick) 후 delta/clock 스트림. instanceId 로 필터.\n */\n@Resolver()\nexport class TwinStateSubscription {\n @Subscription(returns => TwinStateMessage, {\n subscribe: ({ args, context }) => {\n const { instanceId } = args\n const domainId = (context as any)?.state?.domain?.id\n\n /* 테넌트 격리 — 이 도메인 소유 인스턴스가 아니면 스냅샷도 스트림도 흘리지 않는다(빈 구독). */\n if (!domainId || !TwinEngine.owns(domainId, instanceId)) {\n return pipe(pubsub.subscribe('twin-state'), filter(() => false))\n }\n\n /* 구독 순간 현재 상태를 먼저 발행(late 구독자도 snapshot→delta 순서 보장). */\n process.nextTick(() => {\n const snap = TwinEngine.snapshot(domainId, instanceId)\n if (snap) {\n pubsub.publish('twin-state', {\n twinState: { instanceId, kind: 'snapshot', revision: snap.revision, payload: snap }\n })\n }\n })\n\n return pipe(\n pubsub.subscribe('twin-state'),\n filter((payload: { twinState: TwinStateMessage }) => payload.twinState.instanceId === instanceId)\n )\n }\n })\n twinState(@Root() payload: { twinState: TwinStateMessage }, @Arg('instanceId') instanceId: string): TwinStateMessage {\n return payload.twinState\n }\n}\n"]}
1
+ {"version":3,"file":"twin-state-subscription.js","sourceRoot":"","sources":["../../../server/service/twin-state/twin-state-subscription.ts"],"names":[],"mappings":";;;;AAAA,+CAA2C;AAC3C,+CAAwF;AAExF,iDAA0F;AAE1F,oDAAkD;AAClD,8EAA+H;AAE/H,+EAA+E;AAExE,IAAM,gBAAgB,GAAtB,MAAM,gBAAgB;CAY5B,CAAA;AAZY,4CAAgB;AAE3B;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,mDAAmD,EAAE,CAAC;;oDAC1D;AAGlB;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,yCAAyC,EAAE,CAAC;;8CACtD;AAGZ;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,2BAA2B,EAAE,CAAC;;kDAChE;AAGjB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,oBAAY,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,uFAAuF,EAAE,CAAC;;iDACzI;2BAXF,gBAAgB;IAD5B,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,mEAAmE,EAAE,CAAC;GACpF,gBAAgB,CAY5B;AAED;;;;;;;;;;;GAWG;AAEI,IAAM,qBAAqB,GAA3B,MAAM,qBAAqB;IAqBhC,SAAS,CAAS,OAAyB,EAAqB,UAAkB;QAChF,OAAO,OAAO,CAAC,SAA6B,CAAA;IAC9C,CAAC;CACF,CAAA;AAxBY,sDAAqB;AAqBhC;IApBC,IAAA,2BAAY,EAAC,OAAO,CAAC,EAAE,CAAC,gBAAgB,EAAE;QACzC,SAAS,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE;YACrC,MAAM,EAAE,UAAU,EAAE,GAAG,IAAI,CAAA;YAC3B,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAI,OAAe,EAAE,KAAK,IAAI,EAAE,CAAA;YACtD,MAAM,IAAA,oCAA4B,EAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;YAE1F,uGAAuG;YACvG,IAAI,CAAC,qBAAU,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,UAAU,CAAC;gBAAE,MAAM,IAAI,KAAK,CAAC,6CAA6C,UAAU,EAAE,CAAC,CAAA;YAEvH,qHAAqH;YACrH,OAAO,CAAC,QAAQ,CAAC,GAAG,EAAE;gBACpB,MAAM,IAAI,GAAG,qBAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,EAAE,UAAU,CAAC,CAAA;gBACvD,IAAI,IAAI,EAAE,CAAC;oBACT,cAAM,CAAC,OAAO,CAAC,wCAAgB,EAAE,IAAA,wCAAgB,EAAC,MAAM,CAAC,EAAE,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;gBACzI,CAAC;YACH,CAAC,CAAC,CAAA;YAEF,OAAO,IAAA,mBAAI,EAAC,cAAM,CAAC,SAAS,CAAC,wCAAgB,CAAC,EAAE,IAAA,qBAAM,EAAC,IAAA,uCAAe,EAAC,MAAM,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC,CAAC,CAAA;QACjG,CAAC;KACF,CAAC;IACS,mBAAA,IAAA,mBAAI,GAAE,CAAA;IAA6B,mBAAA,IAAA,kBAAG,EAAC,YAAY,CAAC,CAAA;;;4CAAsB,gBAAgB;sDAEpG;gCAvBU,qBAAqB;IADjC,IAAA,uBAAQ,GAAE;GACE,qBAAqB,CAwBjC","sourcesContent":["import { filter, pipe } from 'graphql-yoga'\nimport { Arg, Field, Int, ObjectType, Resolver, Root, Subscription } from 'type-graphql'\n\nimport { assertDomainSubscribeAllowed, pubsub, ScalarObject } from '@things-factory/shell'\n\nimport { TwinEngine } from '../../engine/index.js'\nimport { TWIN_STATE_TOPIC, twinStateFilter, twinStatePayload, type TwinStatePayload } from '../../engine/twin-state-channel.js'\n\n/* State 채널 메시지 — snapshot | delta | clock 멀티플렉싱(커널 SubscriptionMessage 대응). */\n@ObjectType({ description: 'Twin State channel message multiplexing snapshot | delta | clock.' })\nexport class TwinStateMessage {\n @Field({ description: 'Twin runtime instance id this message belongs to.' })\n instanceId: string\n\n @Field({ description: 'Message kind: snapshot | delta | clock.' })\n kind: string\n\n @Field(type => Int, { nullable: true, description: 'Monotonic state revision.' })\n revision?: number\n\n @Field(type => ScalarObject, { nullable: true, description: 'Payload — snapshot: StateSnapshot / delta: CanonicalEnvelope / clock: { simClockMs }.' })\n payload?: any\n}\n\n/*\n * 트윈 State 구독 — state-register/data-resolver 패턴 미러.\n * 구독 즉시 현재 snapshot 발행(process.nextTick) 후 delta/clock 스트림.\n *\n * The tenant is asked while streaming, not only at subscribe time (2026-09-24). Ownership used to be\n * checked once and the stream filtered on instanceId alone; the same id in two tenants is the normal\n * case (`runtime-key.ts`), so other tenants' deltas came through, and the snapshot sent to a new\n * subscriber reached every tenant subscribed to that id. Messages now carry their domain and the filter\n * compares it (`engine/twin-state-channel.ts`). A twin the caller does not own is refused rather than\n * given an empty stream — an empty stream cannot tell \"nothing changed\" from \"you may not see this\"\n * (shell's `data` throws too).\n */\n@Resolver()\nexport class TwinStateSubscription {\n @Subscription(returns => TwinStateMessage, {\n subscribe: async ({ args, context }) => {\n const { instanceId } = args\n const { domain, user } = (context as any)?.state ?? {}\n await assertDomainSubscribeAllowed(domain, user, (d, u) => process.superUserGranted(d, u))\n\n /* One refusal for both cases, so the answer does not reveal whether another tenant has such a twin. */\n if (!TwinEngine.owns(domain.id, instanceId)) throw new Error(`twin instance not running in this tenant: ${instanceId}`)\n\n /* Current state first (a late subscriber still sees snapshot → delta). It reaches this tenant's subscribers only. */\n process.nextTick(() => {\n const snap = TwinEngine.snapshot(domain.id, instanceId)\n if (snap) {\n pubsub.publish(TWIN_STATE_TOPIC, twinStatePayload(domain.id, { instanceId, kind: 'snapshot', revision: snap.revision, payload: snap }))\n }\n })\n\n return pipe(pubsub.subscribe(TWIN_STATE_TOPIC), filter(twinStateFilter(domain.id, instanceId)))\n }\n })\n twinState(@Root() payload: TwinStatePayload, @Arg('instanceId') instanceId: string): TwinStateMessage {\n return payload.twinState as TwinStateMessage\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/headless-twin",
3
- "version": "10.1.37",
3
+ "version": "10.1.38",
4
4
  "main": "dist-server/index.js",
5
5
  "things-factory": true,
6
6
  "author": "heartyoh <heartyoh@hatiolab.com>",
@@ -29,11 +29,11 @@
29
29
  "dependencies": {
30
30
  "@operato/ops-contract": "^0.9.33",
31
31
  "@operato/twin-kernel": "^0.11.23",
32
- "@things-factory/auth-base": "^10.1.37",
33
- "@things-factory/cache-service": "^10.1.37",
32
+ "@things-factory/auth-base": "^10.1.38",
33
+ "@things-factory/cache-service": "^10.1.38",
34
34
  "@things-factory/env": "^10.1.20",
35
- "@things-factory/ingest": "^10.1.37",
36
- "@things-factory/shell": "^10.1.37"
35
+ "@things-factory/ingest": "^10.1.38",
36
+ "@things-factory/shell": "^10.1.38"
37
37
  },
38
- "gitHead": "d8a69e5c8803a4b9d3289df0cf38e161a207db61"
38
+ "gitHead": "b947349fe1c27d11038b592ceadaba336f14785b"
39
39
  }
@@ -24,6 +24,7 @@ import { itemResolverFor } from '../service/twin-subject/item-resolver.js'
24
24
  import { planLiveContinuity, planWarmStart, unwrapState } from './warm-start.js'
25
25
  import { applyDeclarationLayers } from './local-declarations.js'
26
26
  import { isOfDomain, parseRuntimeKey, runtimeKey } from './runtime-key.js'
27
+ import { TWIN_STATE_TOPIC, twinStatePayload } from './twin-state-channel.js'
27
28
  import { routeCommand } from './command-routing.js'
28
29
  import { TwinInstance } from '../service/twin-instance/twin-instance.js'
29
30
  /* 자극의 집은 원본이다(ADR-0029) — 그 행을 읽고 쓴다. */
@@ -1704,8 +1705,8 @@ export class TwinEngine {
1704
1705
  /* State 채널: runtime.subscribe(snapshot→delta→clock) → pubsub 브로드캐스팅(구독 리졸버가 instanceId 필터). */
1705
1706
  const sub = runtime.subscribe((msg: SubscriptionMessage) => {
1706
1707
  this.publishGuarded(
1707
- 'twin-state',
1708
- { twinState: { instanceId: id, kind: msg.kind, revision: (msg as any).revision, payload: msg } },
1708
+ TWIN_STATE_TOPIC,
1709
+ twinStatePayload(domainId, { instanceId: id, kind: msg.kind, revision: (msg as any).revision, payload: msg }),
1709
1710
  `twin-state:${id}`
1710
1711
  )
1711
1712
  /* 영속 + 라이브 바인딩 브리지: delta 를 저널 버퍼에 담고 + 엔티티별 data(tag:) publish → 보드 컴포넌트 라이브. */
@@ -2353,8 +2354,8 @@ export class TwinEngine {
2353
2354
  // scheduleRefresh(250ms 디바운스)→pollLive 로 되물어봄. 스냅샷(O(state))은 보는 사람이 물을 때만 1회 계산.
2354
2355
  // (여기서 payload 로 스냅샷을 실으면 아무도 안 읽는데 tick 마다 통째로 떠서 순수 낭비 — 신호만 보낸다.)
2355
2356
  this.publishGuarded(
2356
- 'twin-state',
2357
- { twinState: { instanceId: inst.id, kind: 'delta', revision: inst.revision ?? 0 } },
2357
+ TWIN_STATE_TOPIC,
2358
+ twinStatePayload(inst.domainId, { instanceId: inst.id, kind: 'delta', revision: inst.revision ?? 0 }),
2358
2359
  `twin-state:${inst.id}`
2359
2360
  )
2360
2361
  }
@@ -3641,7 +3642,7 @@ export class TwinEngine {
3641
3642
  * 그래도 이 가드를 남기는 이유는 둘이다. `publish` 가 직접 던지는 경우(직렬화 실패 등)를 잡고,
3642
3643
  * 못 보낸 태그의 시그니처를 되돌린다 — 남겨 두면 그 태그가 오류 없이 영원히 낡은 값을 보인다.
3643
3644
  */
3644
- private static publishGuarded(channel: 'data' | 'twin-state', payload: any, what: string): boolean {
3645
+ private static publishGuarded(channel: 'data' | typeof TWIN_STATE_TOPIC, payload: any, what: string): boolean {
3645
3646
  try {
3646
3647
  pubsub.publish(channel as any, payload)
3647
3648
  return true
@@ -0,0 +1,39 @@
1
+ /*
2
+ * The `twin-state` channel — **a message says whose twin it is, and the filter asks.** Pure.
3
+ *
4
+ * ── What went wrong (2026-09-24) ─────────────────────────────────────────────
5
+ * A twin is `(domain, instanceId)` (`runtime-key.ts`), and the same instanceId in two tenants is the
6
+ * expected case — reference source names such as `sap-ewm-1710` are vocabulary tenants share. The
7
+ * channel had not caught up: publishers sent `{ twinState: { instanceId, … } }` with no domain, and the
8
+ * subscription filtered on instanceId alone. Ownership was checked once, when subscribing. So a tenant
9
+ * watching its own `sap-ewm-1710` also received the other tenant's deltas, and the snapshot sent to a
10
+ * new subscriber went to every subscriber of that id in every tenant.
11
+ *
12
+ * The rule is shell's `data` channel's (the message carries its domain, the filter compares it with the
13
+ * key), compared by id because the twin's key already is. Every sender builds its payload here, and
14
+ * `twin-state-channel.test.ts` fails if a sender builds one anywhere else.
15
+ */
16
+
17
+ export const TWIN_STATE_TOPIC = 'twin-state'
18
+
19
+ export interface TwinStateMessageShape {
20
+ instanceId: string
21
+ kind: string
22
+ revision?: number
23
+ payload?: unknown
24
+ }
25
+
26
+ export interface TwinStatePayload {
27
+ domainId: string
28
+ twinState: TwinStateMessageShape
29
+ }
30
+
31
+ export function twinStatePayload(domainId: string, twinState: TwinStateMessageShape): TwinStatePayload {
32
+ if (!domainId) throw new Error('twinStatePayload: domainId is required — a twin state message always belongs to a tenant')
33
+ return { domainId, twinState }
34
+ }
35
+
36
+ /** Passes only this tenant's messages about this twin. */
37
+ export function twinStateFilter(domainId: string, instanceId: string) {
38
+ return (p: TwinStatePayload | undefined): boolean => !!p && p.domainId === domainId && p.twinState?.instanceId === instanceId
39
+ }
@@ -0,0 +1,26 @@
1
+ /*
2
+ * The `twin-reference-progress` channel — **progress says whose import it is, and the filter asks.** Pure.
3
+ *
4
+ * The subscription filtered on `source` alone and checked only that the caller had some domain, so
5
+ * anyone who knew a source name — and names like `sap-ewm-1710` are shared between tenants — received
6
+ * another tenant's import progress, including its failure text. Same fix and same rule as
7
+ * `engine/twin-state-channel.ts`.
8
+ */
9
+
10
+ export const REFERENCE_PROGRESS_TOPIC = 'twin-reference-progress'
11
+
12
+ export interface ReferenceProgressPayload<M extends { source: string } = { source: string }> {
13
+ domainId: string
14
+ twinReferenceProgress: M
15
+ }
16
+
17
+ export function referenceProgressPayload<M extends { source: string }>(domainId: string, message: M): ReferenceProgressPayload<M> {
18
+ if (!domainId) throw new Error('referenceProgressPayload: domainId is required — import progress always belongs to a tenant')
19
+ return { domainId, twinReferenceProgress: message }
20
+ }
21
+
22
+ /** Passes only this tenant's progress for this source. */
23
+ export function referenceProgressFilter(domainId: string, source: string) {
24
+ return (p: ReferenceProgressPayload | undefined): boolean =>
25
+ !!p && p.domainId === domainId && p.twinReferenceProgress?.source === source
26
+ }
@@ -8,10 +8,12 @@
8
8
  * 낱말이 표준이므로 화면은 **단계마다 번역 하나**만 두면 모든 커넥터를 보여 준다.
9
9
  */
10
10
  import { Arg, Field, Int, ObjectType, Resolver, Root, Subscription } from 'type-graphql'
11
- import { pubsub } from '@things-factory/shell'
11
+ import { assertDomainSubscribeAllowed, getRepository, pubsub } from '@things-factory/shell'
12
12
  import { filter, pipe } from 'graphql-yoga'
13
13
 
14
14
  import { REFERENCE_STEPS } from './reference-progress.js'
15
+ import { REFERENCE_PROGRESS_TOPIC, referenceProgressFilter, type ReferenceProgressPayload } from './reference-progress-channel.js'
16
+ import { TwinReference } from './twin-reference.js'
15
17
 
16
18
  @ObjectType({ description: 'One step of progress while a reference is being imported into twins.' })
17
19
  export class TwinReferenceProgressMessage {
@@ -52,20 +54,23 @@ export class TwinReferenceProgressMessage {
52
54
  @Resolver()
53
55
  export class TwinReferenceProgressSubscription {
54
56
  @Subscription(returns => TwinReferenceProgressMessage, {
55
- subscribe: ({ args, context }) => {
57
+ /*
58
+ * Tenancy is asked twice (2026-09-24): the source must be one of the caller's references, and every
59
+ * message must be the caller's domain's. It used to filter on `source` alone after checking only that
60
+ * the caller had a domain — and source names are shared between tenants.
61
+ */
62
+ subscribe: async ({ args, context }) => {
56
63
  const { source } = args
57
- const domainId = (context as any)?.state?.domain?.id
58
- /* 테넌트 격리 — 도메인이 없으면 빈 구독이다(다른 테넌트의 진행을 흘리지 않는다). */
59
- if (!domainId) return pipe(pubsub.subscribe('twin-reference-progress'), filter(() => false))
60
- return pipe(
61
- pubsub.subscribe('twin-reference-progress'),
62
- filter((p: { twinReferenceProgress: TwinReferenceProgressMessage }) => p.twinReferenceProgress.source === source)
63
- )
64
+ const { domain, user } = (context as any)?.state ?? {}
65
+ await assertDomainSubscribeAllowed(domain, user, (d, u) => process.superUserGranted(d, u))
66
+ const owned = await getRepository(TwinReference).findOne({ where: { domain: { id: domain.id }, source } })
67
+ if (!owned) throw new Error(`reference not found in this tenant: ${source}`)
68
+ return pipe(pubsub.subscribe(REFERENCE_PROGRESS_TOPIC), filter(referenceProgressFilter(domain.id, source)))
64
69
  },
65
70
  description: 'Progress while importing a reference — standard step names, so one translation per step covers every connector.'
66
71
  })
67
72
  twinReferenceProgress(
68
- @Root() payload: { twinReferenceProgress: TwinReferenceProgressMessage },
73
+ @Root() payload: ReferenceProgressPayload<TwinReferenceProgressMessage>,
69
74
  @Arg('source') source: string
70
75
  ): TwinReferenceProgressMessage {
71
76
  return payload.twinReferenceProgress