@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.
Files changed (161) hide show
  1. package/README.md +37 -24
  2. package/dist-server/engine/entity-delta.d.ts +13 -1
  3. package/dist-server/engine/entity-delta.js +130 -16
  4. package/dist-server/engine/entity-delta.js.map +1 -1
  5. package/dist-server/engine/index.d.ts +4 -0
  6. package/dist-server/engine/index.js +4 -0
  7. package/dist-server/engine/index.js.map +1 -1
  8. package/dist-server/engine/kpi-fold.d.ts +20 -3
  9. package/dist-server/engine/kpi-fold.js +45 -53
  10. package/dist-server/engine/kpi-fold.js.map +1 -1
  11. package/dist-server/engine/kpi-query.d.ts +16 -0
  12. package/dist-server/engine/kpi-query.js +129 -19
  13. package/dist-server/engine/kpi-query.js.map +1 -1
  14. package/dist-server/engine/kpi-target.d.ts +141 -0
  15. package/dist-server/engine/kpi-target.js +137 -0
  16. package/dist-server/engine/kpi-target.js.map +1 -0
  17. package/dist-server/engine/live-attentions.d.ts +4 -2
  18. package/dist-server/engine/live-attentions.js +5 -1
  19. package/dist-server/engine/live-attentions.js.map +1 -1
  20. package/dist-server/engine/measured-estimator.d.ts +50 -0
  21. package/dist-server/engine/measured-estimator.js +78 -0
  22. package/dist-server/engine/measured-estimator.js.map +1 -0
  23. package/dist-server/engine/model-basis.d.ts +23 -0
  24. package/dist-server/engine/model-basis.js +100 -0
  25. package/dist-server/engine/model-basis.js.map +1 -0
  26. package/dist-server/engine/oee-accumulator.d.ts +2 -2
  27. package/dist-server/engine/oee-accumulator.js +4 -4
  28. package/dist-server/engine/oee-accumulator.js.map +1 -1
  29. package/dist-server/engine/spec-coverage.d.ts +49 -0
  30. package/dist-server/engine/spec-coverage.js +60 -0
  31. package/dist-server/engine/spec-coverage.js.map +1 -0
  32. package/dist-server/engine/structure-diff.d.ts +25 -0
  33. package/dist-server/engine/structure-diff.js +63 -0
  34. package/dist-server/engine/structure-diff.js.map +1 -0
  35. package/dist-server/engine/travel-estimator.d.ts +57 -0
  36. package/dist-server/engine/travel-estimator.js +120 -0
  37. package/dist-server/engine/travel-estimator.js.map +1 -0
  38. package/dist-server/engine/twin-engine.d.ts +181 -18
  39. package/dist-server/engine/twin-engine.js +545 -96
  40. package/dist-server/engine/twin-engine.js.map +1 -1
  41. package/dist-server/engine/warm-start.d.ts +39 -0
  42. package/dist-server/engine/warm-start.js +37 -0
  43. package/dist-server/engine/warm-start.js.map +1 -0
  44. package/dist-server/index.js +8 -0
  45. package/dist-server/index.js.map +1 -1
  46. package/dist-server/service/index.d.ts +4 -2
  47. package/dist-server/service/index.js +21 -14
  48. package/dist-server/service/index.js.map +1 -1
  49. package/dist-server/service/reference/reference-live.js +2 -1
  50. package/dist-server/service/reference/reference-live.js.map +1 -1
  51. package/dist-server/service/reference/reference-master.d.ts +307 -1
  52. package/dist-server/service/reference/reference-master.js +96 -8
  53. package/dist-server/service/reference/reference-master.js.map +1 -1
  54. package/dist-server/service/reference/reference-resolver.js +3 -3
  55. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  56. package/dist-server/service/reference/template-registry.d.ts +1 -1
  57. package/dist-server/service/reference/template-registry.js.map +1 -1
  58. package/dist-server/service/twin-attention/twin-attention-query.js +1 -1
  59. package/dist-server/service/twin-attention/twin-attention-query.js.map +1 -1
  60. package/dist-server/service/twin-control/twin-control-mutation.js +1 -1
  61. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  62. package/dist-server/service/twin-event/backfill-keys.d.ts +11 -0
  63. package/dist-server/service/twin-event/backfill-keys.js +63 -0
  64. package/dist-server/service/twin-event/backfill-keys.js.map +1 -0
  65. package/dist-server/service/twin-event/twin-event-keys.d.ts +35 -0
  66. package/dist-server/service/twin-event/twin-event-keys.js +95 -0
  67. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -0
  68. package/dist-server/service/twin-event/twin-event-type.d.ts +6 -0
  69. package/dist-server/service/twin-event/twin-event-type.js +32 -0
  70. package/dist-server/service/twin-event/twin-event-type.js.map +1 -0
  71. package/dist-server/service/twin-event/twin-event.d.ts +16 -0
  72. package/dist-server/service/twin-event/twin-event.js +50 -0
  73. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  74. package/dist-server/service/twin-forecast/twin-forecast-query.js +50 -7
  75. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  76. package/dist-server/service/twin-instance/twin-instance.js +1 -1
  77. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  78. package/dist-server/service/twin-journal/twin-journal-query.d.ts +28 -0
  79. package/dist-server/service/twin-journal/twin-journal-query.js +127 -2
  80. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  81. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +1 -1
  82. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +4 -3
  83. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  84. package/dist-server/service/twin-space/twin-space-area.js +1 -1
  85. package/dist-server/service/twin-space/twin-space-area.js.map +1 -1
  86. package/dist-server/service/twin-space/twin-space-resolver.js +3 -3
  87. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  88. package/dist-server/service/twin-space/twin-space.d.ts +11 -0
  89. package/dist-server/service/twin-space/twin-space.js +5 -0
  90. package/dist-server/service/twin-space/twin-space.js.map +1 -1
  91. package/dist-server/service/twin-structure/index.d.ts +2 -0
  92. package/dist-server/service/twin-structure/index.js +6 -0
  93. package/dist-server/service/twin-structure/index.js.map +1 -0
  94. package/dist-server/service/twin-structure/twin-structure.d.ts +28 -0
  95. package/dist-server/service/twin-structure/twin-structure.js +97 -0
  96. package/dist-server/service/twin-structure/twin-structure.js.map +1 -0
  97. package/dist-server/service/twin-target/index.d.ts +4 -0
  98. package/dist-server/service/twin-target/index.js +8 -0
  99. package/dist-server/service/twin-target/index.js.map +1 -0
  100. package/dist-server/service/twin-target/twin-target-resolver.d.ts +7 -0
  101. package/dist-server/service/twin-target/twin-target-resolver.js +143 -0
  102. package/dist-server/service/twin-target/twin-target-resolver.js.map +1 -0
  103. package/dist-server/service/twin-target/twin-target.d.ts +25 -0
  104. package/dist-server/service/twin-target/twin-target.js +112 -0
  105. package/dist-server/service/twin-target/twin-target.js.map +1 -0
  106. package/dist-server/tsconfig.tsbuildinfo +1 -1
  107. package/package.json +6 -6
  108. package/server/engine/entity-delta.ts +135 -15
  109. package/server/engine/index.ts +4 -0
  110. package/server/engine/kpi-fold.ts +62 -59
  111. package/server/engine/kpi-query.ts +140 -19
  112. package/server/engine/kpi-target.ts +226 -0
  113. package/server/engine/live-attentions.ts +12 -2
  114. package/server/engine/measured-estimator.ts +91 -0
  115. package/server/engine/model-basis.ts +94 -0
  116. package/server/engine/oee-accumulator.ts +5 -5
  117. package/server/engine/spec-coverage.ts +85 -0
  118. package/server/engine/structure-diff.ts +88 -0
  119. package/server/engine/travel-estimator.ts +133 -0
  120. package/server/engine/twin-engine.ts +587 -106
  121. package/server/engine/warm-start.ts +53 -0
  122. package/server/index.ts +9 -0
  123. package/server/service/index.ts +7 -0
  124. package/server/service/reference/reference-live.ts +2 -1
  125. package/server/service/reference/reference-master.ts +383 -10
  126. package/server/service/reference/reference-resolver.ts +3 -3
  127. package/server/service/reference/template-registry.ts +1 -1
  128. package/server/service/twin-attention/twin-attention-query.ts +1 -1
  129. package/server/service/twin-control/twin-control-mutation.ts +1 -1
  130. package/server/service/twin-event/backfill-keys.ts +72 -0
  131. package/server/service/twin-event/twin-event-keys.ts +102 -0
  132. package/server/service/twin-event/twin-event-type.ts +27 -0
  133. package/server/service/twin-event/twin-event.ts +62 -0
  134. package/server/service/twin-forecast/twin-forecast-query.ts +56 -8
  135. package/server/service/twin-instance/twin-instance.ts +1 -1
  136. package/server/service/twin-journal/twin-journal-query.ts +129 -5
  137. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +4 -3
  138. package/server/service/twin-space/twin-space-area.ts +1 -1
  139. package/server/service/twin-space/twin-space-resolver.ts +3 -3
  140. package/server/service/twin-space/twin-space.ts +14 -0
  141. package/server/service/twin-structure/index.ts +3 -0
  142. package/server/service/twin-structure/twin-structure.ts +93 -0
  143. package/server/service/twin-target/index.ts +5 -0
  144. package/server/service/twin-target/twin-target-resolver.ts +124 -0
  145. package/server/service/twin-target/twin-target.ts +97 -0
  146. package/test/capability-mapping.test.ts +5 -5
  147. package/test/duration-estimators.test.ts +144 -0
  148. package/test/entity-delta.test.ts +150 -19
  149. package/test/ingest-bench.test.ts +9 -9
  150. package/test/kpi-fold.test.ts +254 -3
  151. package/test/live-mirror-parity.test.ts +53 -19
  152. package/test/master-to-twin.test.ts +87 -13
  153. package/test/model-basis.test.ts +86 -0
  154. package/test/oee-accumulator.test.ts +9 -9
  155. package/test/scale-twin-bench.test.ts +22 -22
  156. package/test/spec-coverage.test.ts +113 -0
  157. package/test/streamline-e2e.test.ts +10 -10
  158. package/test/structure-revision-db.test.ts +310 -0
  159. package/test/twin-event-keys.test.ts +108 -0
  160. package/test/vocabulary-guard.test.ts +43 -0
  161. package/test/warm-start.test.ts +78 -0
@@ -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 { Arg, Ctx, Int, Mutation, Query, Resolver } from 'type-graphql'\n\nimport { getRepository, ScalarObject } from '@things-factory/shell'\n\nimport { TwinEngine } from '../../engine/index.js'\nimport { TwinEvent } from '../twin-event/twin-event.js'\nimport { GAP_SAMPLES, aggregatePoints, computeSpc, gapMetrics, learnedCalibration, type GapPoint } from './gap-analytics.js'\n\n/* 커널 예측 함수 — CJS require(twin-engine 과 동일 방식). */\nconst { monteCarloForecast } = require('@operato/twin-kernel')\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}\nconst METRICS: Record<string, (s: any) => number> = {\n items: s => (s.items || []).length,\n occupancy: s => (s.nodes || []).reduce((a: number, n: any) => a + (n.occupancy || 0), 0),\n tasks: s => (s.tasks || []).length,\n orders: s => (s.orders || []).length,\n // 이행 지표 — 라이브 예측의 핵심 질문(\"언제 다 나가나·백로그 언제 풀리나\").\n shipped: s => (s.orders || []).filter((o: any) => o.status === 'shipped').length,\n backlog: s => (s.orders || []).filter((o: any) => o.status !== 'shipped' && o.status !== 'cancelled').length\n}\n\n/* 학습된 보정 저장(P3.2, 인메모리) — key=domain:instance:metric:horizon → {biasShift, halfWidth, …}.\n twinLearnCalibration 이 갭에서 학습해 저장하고, twinForecast 가 있으면 예측에 상시 적용(라이브 보정).\n 데모 트윈은 재부팅 시 저널 리셋이라 세션 스코프로 충분 — DB 영속(TwinForecastScore)은 후속. */\nconst CALIBRATION = new Map<string, { biasShift: number; halfWidth: number; at: string; samples: number; accBefore: number; accAfter: number }>()\nconst calKey = (d: string, i: string, m: string, h: number) => `${d}:${i}:${m}:${h}`\n\n/* 갭 point 계산(공용) — N개 과거 vantage 각각 forecast-at-T vs actual-at-T+H(결정적 백테스트). gapTrend query·learn mutation 공용. */\nasync function gapPoints(domainId: string, instanceId: string, m: (s: any) => number, H: number, N: number, scen: any): Promise<any[]> {\n const where = { domain: { id: domainId }, instanceId }\n const repo = getRepository(TwinEvent)\n const first = await repo.findOne({ where, order: { eventTime: 'ASC' } })\n const last = await repo.findOne({ where, order: { eventTime: 'DESC' } })\n if (!first || !last) return []\n const t0 = Date.parse(first.eventTime)\n const lastVantage = Date.parse(last.eventTime) - H\n if (!(lastVantage > t0)) return []\n const points: any[] = []\n for (let i = 0; i < N; i++) {\n const T = t0 + ((lastVantage - t0) * i) / (N - 1)\n const k = await TwinEngine.buildForecastKernelAt(domainId, instanceId, new Date(T).toISOString()).catch(() => null)\n if (!k) continue\n const dist = monteCarloForecast(k, { runs: 12, horizonMs: H, scenario: scen, tickMs: 1000, metric: m })\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) 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@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 면 \"그때 서서 본 미래\"(백테스트, forecast-accuracy-gap 플랜 P0).\n at 있으면 T 시점 커널 재구성(저널 기반, 도메인 스코프). 없으면 지금: 라이브 커널 직접(도메인 소유 시) / buildForecastKernel. */\n const domainId = context.state.domain.id\n const k = at\n ? await TwinEngine.buildForecastKernelAt(domainId, instanceId, at)\n : (TwinEngine.owns(domainId, instanceId) ? TwinEngine.kernel(instanceId) : undefined) ?? (await TwinEngine.buildForecastKernel(domainId, instanceId))\n if (!k) return null\n\n const m = METRICS[metric] ?? METRICS.items\n const H = horizonMs ?? 300000\n const R = runs ?? 30\n const step = 1000\n const scen = scenario ?? DEFAULT_SCENARIO\n\n /* 분포: N개 확률적 미래 표본(fork 마다 seed 변주, 원본 보존). */\n const distribution = monteCarloForecast(k, { runs: R, horizonMs: H, scenario: scen, tickMs: step, metric: m })\n\n /* 학습된 보정 상시 적용(P3.2) — twinLearnCalibration 이 저장한 편향·밴드가 있으면 예측을 현실로 보정(라이브가 실제로 나아짐).\n 갭트렌드(gapPoints)는 raw 모델을 그대로 측정(학습 신호 보존) — 보정은 표시용 forecast 에만. */\n const cal = CALIBRATION.get(calKey(domainId, instanceId, metric ?? 'items', H))\n if (cal && distribution) {\n const p50c = (distribution.p50 ?? 0) + 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 /* 예측 궤적(예측선): 단일 fork 를 horizon 까지 굴리며 지표 기록. */\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\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 return { metric: metric ?? 'items', horizonMs: H, runs: R, current, distribution, trajectory, actual, scored, calibrated: !!cal }\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 = METRICS[metric] ?? METRICS.items\n const H = horizonMs ?? 300000\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 const per = await Promise.all(ids.map(id => gapPoints(domainId, id, m, H, N, scen)))\n const points = aggregatePoints(per as GapPoint[][])\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 }\n\n /*\n * 학습(P3.2) — 갭에서 편향·밴드를 학습해 저장하면, 이후 twinForecast 가 상시 적용(라이브 예측이 실제로 보정됨).\n * assimilation-lite: 잔차(innovation)를 되먹여 추정치를 현실로. per-instance(클라가 co-located 합산).\n * 반환: 학습된 보정 + before/after 정확도(효과 정량화). 저장은 인메모리(세션) — DB 영속은 후속.\n */\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 = METRICS[metric] ?? METRICS.items\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 const pts = await gapPoints(domainId, instanceId, m, H, GAP_SAMPLES, DEFAULT_SCENARIO)\n if (pts.length < 3) return { ok: false, reason: 'insufficient-history' }\n const learn = learnedCalibration(pts, gapMetrics(pts))\n const cal = { biasShift: learn.biasShift, halfWidth: learn.halfWidth, at: '', samples: pts.length, accBefore: learn.accBefore, accAfter: learn.accAfter }\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 { Arg, Ctx, Int, Mutation, Query, Resolver } 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'\n\n/* 커널 예측 함수 — CJS require(twin-engine 과 동일 방식). */\nconst { monteCarloForecast } = require('@operato/twin-kernel')\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}\nconst METRICS: Record<string, (s: any) => number> = {\n items: s => (s.items || []).length,\n occupancy: s => (s.locations || []).reduce((a: number, n: any) => a + (n.occupancy || 0), 0),\n tasks: s => (s.tasks || []).length,\n orders: s => (s.orders || []).length,\n // 이행 지표 — 라이브 예측의 핵심 질문(\"언제 다 나가나·백로그 언제 풀리나\").\n shipped: s => (s.orders || []).filter((o: any) => o.status === 'shipped').length,\n backlog: s => (s.orders || []).filter((o: any) => o.status !== 'shipped' && o.status !== 'cancelled').length\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/* 갭 point 계산(공용) — N개 과거 vantage 각각 forecast-at-T vs actual-at-T+H(결정적 백테스트). gapTrend query·learn mutation 공용. */\nasync function gapPoints(domainId: string, instanceId: string, m: (s: any) => number, H: number, N: number, scen: any): Promise<any[]> {\n const where = { domain: { id: domainId }, instanceId }\n const repo = getRepository(TwinEvent)\n const first = await repo.findOne({ where, order: { eventTime: 'ASC' } })\n const last = await repo.findOne({ where, order: { eventTime: 'DESC' } })\n if (!first || !last) return []\n const t0 = Date.parse(first.eventTime)\n const lastVantage = Date.parse(last.eventTime) - H\n if (!(lastVantage > t0)) return []\n const points: any[] = []\n for (let i = 0; i < N; i++) {\n const T = t0 + ((lastVantage - t0) * i) / (N - 1)\n const k = await TwinEngine.buildForecastKernelAt(domainId, instanceId, new Date(T).toISOString()).catch(() => null)\n if (!k) continue\n const dist = monteCarloForecast(k, { runs: 12, horizonMs: H, scenario: scen, tickMs: 1000, metric: m })\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) 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@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(instanceId)\n : undefined\n if (!k) return null\n\n const m = METRICS[metric] ?? METRICS.items\n const H = horizonMs ?? 300000\n const R = runs ?? 30\n const step = 1000\n const scen = scenario ?? DEFAULT_SCENARIO\n\n /* 분포: N개 확률적 미래 표본(fork 마다 seed 변주, 원본 보존). */\n const distribution = monteCarloForecast(k, { runs: R, horizonMs: H, scenario: scen, tickMs: step, metric: m })\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 console.log(\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 const cal = stored && !stale ? stored : undefined\n if (cal && distribution) {\n const p50c = (distribution.p50 ?? 0) + 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 /* 예측 궤적(예측선): 단일 fork 를 horizon 까지 굴리며 지표 기록. */\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\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 calibrated: !!cal,\n /* 학습해 뒀지만 모델이 바뀌어 적용하지 않았다 — 사용자가 다시 배울 수 있게 알린다. */\n ...(stale ? { calibrationStale: true } : {}),\n ...(specCoverage ? { specCoverage } : {})\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 = METRICS[metric] ?? METRICS.items\n const H = horizonMs ?? 300000\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 const per = await Promise.all(ids.map(id => gapPoints(domainId, id, m, H, N, scen)))\n const points = aggregatePoints(per as GapPoint[][])\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 }\n\n /*\n * 학습(P3.2) — 갭에서 편향·밴드를 학습해 저장하면, 이후 twinForecast 가 상시 적용(라이브 예측이 실제로 보정됨).\n * assimilation-lite: 잔차(innovation)를 되먹여 추정치를 현실로. per-instance(클라가 co-located 합산).\n * 반환: 학습된 보정 + before/after 정확도(효과 정량화). 저장은 인메모리(세션) — DB 영속은 후속.\n */\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 = METRICS[metric] ?? METRICS.items\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 const pts = await gapPoints(domainId, instanceId, m, H, GAP_SAMPLES, DEFAULT_SCENARIO)\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"]}
@@ -46,7 +46,7 @@ tslib_1.__decorate([
46
46
  ], TwinInstance.prototype, "realityMode", void 0);
47
47
  tslib_1.__decorate([
48
48
  (0, typeorm_1.Column)({ type: 'simple-json', nullable: true }),
49
- (0, type_graphql_1.Field)(type => shell_1.ScalarObject, { nullable: true, description: 'BoardDef blueprint (nodes/movers) required to replay events into state.' }),
49
+ (0, type_graphql_1.Field)(type => shell_1.ScalarObject, { nullable: true, description: 'BoardDef blueprint (locations/equipment) required to replay events into state.' }),
50
50
  tslib_1.__metadata("design:type", Object)
51
51
  ], TwinInstance.prototype, "board", void 0);
52
52
  tslib_1.__decorate([
@@ -1 +1 @@
1
- {"version":3,"file":"twin-instance.js","sourceRoot":"","sources":["../../../server/service/twin-instance/twin-instance.ts"],"names":[],"mappings":"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","sourcesContent":["import { CreateDateColumn, Entity, Index, Column, RelationId, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'\nimport { ObjectType, Field, ID, Int } from 'type-graphql'\n\nimport { Domain, ScalarObject } from '@things-factory/shell'\n\n/*\n * TwinInstance — 트윈 인스턴스 레지스트리(재부팅 복구의 앵커).\n * 복구(DB→replay)는 board(마스터 청사진)가 있어야 이벤트를 상태로 되짚을 수 있다 → board·kind 를 영속.\n * 이벤트(TwinEvent)와 달리 가변(status/revision 갱신) — 인스턴스의 \"현재 등록 상태\".\n * board 는 simple-json(멀티DB 이식 — postgres/mysql/sqlite/mssql/oracle 공통).\n * (CLAUDE.md: 모든 @ObjectType/@Field 는 영문 description 필수.)\n */\n@Entity()\n@Index('ix_twin_instance_0', (e: TwinInstance) => [e.domain, e.instanceId], { unique: true })\n@Index('ix_twin_instance_1', (e: TwinInstance) => [e.domain, e.spaceId]) // 공간→인스턴스 1급 질의(공간 중심 존재론)\n@ObjectType({ description: 'Registry of a hosted twin runtime instance — anchors reboot recovery (board + kind).' })\nexport class TwinInstance {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { description: 'Unique identifier of the registry record.' })\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true, description: 'Owning tenant domain.' })\n domain?: Domain\n\n @RelationId((e: TwinInstance) => e.domain)\n domainId?: string\n\n @Column()\n @Field({ description: 'Twin runtime instance id (unique within a domain).' })\n instanceId: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Domain kernel kind: wms | yms | mes.' })\n kind?: string\n\n /*\n * realityMode — 이 트윈의 현실이 어디서 오는가(runtime-state-model §0·§1 \"지속 현실 하나 + 여러 렌즈\"의 ① 선언).\n * 'mirror' = 현실은 외부 실물 시스템. 트윈=순수 렌즈, 재기동 시 실물서 재동기(startLive).\n * 'sim-world' = 트윈이 낳은 타임라인이 곧 진실이며 이어진다 → 재기동 시 저널서 resume(현실 지속).\n * 'sim-experiment' = seed 재현형. 재기동 = 같은 우주 재실행(저널 reset, revision 0) — 백지 시작이 버그가 아니라 선언된 거동.\n * 미선언(레거시)은 'sim-experiment'로 간주(가장 보수적 — 기존 데모 거동 보존).\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: \"Reality source declaration: 'mirror' (external system is truth, resync on reboot) | 'sim-world' (generated timeline is truth and persists, resume on reboot) | 'sim-experiment' (seed-reproducible, resets on reboot). Absent ⇒ treated as sim-experiment.\" })\n realityMode?: string\n\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => ScalarObject, { nullable: true, description: 'BoardDef blueprint (nodes/movers) required to replay events into state.' })\n board?: any\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Owning space id (co-location anchor). Promoted from board.spaceId — first-class \"space → its twins\" query (space-centered ontology).' })\n spaceId?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Owning area id within the space (optional scope). Promoted from board.areaId.' })\n areaId?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Lifecycle status: running | stopped.' })\n status?: string\n\n /*\n * purpose — 이 인스턴스가 운영 트윈인지, 격리된 벤치 사본인지 1급 선언(이름 접두사가 아니라 속성으로 구별).\n * 'bench' 는 규모/부하 실험용 격리 사본 — 목록·정리·안전 가드가 이 속성을 키로 동작(운영 트윈 무오염 보장).\n * 미선언(레거시)은 'operational' 로 간주.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: \"Instance purpose: 'operational' (real twin) | 'bench' (isolated copy for scale/load experiments). Absent ⇒ operational.\" })\n purpose?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'For a bench copy: the source spaceId it was copied from (provenance).' })\n copyOf?: string\n\n @Column({ type: 'int', nullable: true })\n @Field(type => Int, { nullable: true, description: 'Last persisted state revision observed for this instance.' })\n revision?: number\n\n @CreateDateColumn()\n @Field({ nullable: true, description: 'Timestamp when the instance was first registered.' })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true, description: 'Timestamp when the instance registry row was last updated.' })\n updatedAt?: Date\n}\n"]}
1
+ {"version":3,"file":"twin-instance.js","sourceRoot":"","sources":["../../../server/service/twin-instance/twin-instance.ts"],"names":[],"mappings":"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","sourcesContent":["import { CreateDateColumn, Entity, Index, Column, RelationId, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'\nimport { ObjectType, Field, ID, Int } from 'type-graphql'\n\nimport { Domain, ScalarObject } from '@things-factory/shell'\n\n/*\n * TwinInstance — 트윈 인스턴스 레지스트리(재부팅 복구의 앵커).\n * 복구(DB→replay)는 board(마스터 청사진)가 있어야 이벤트를 상태로 되짚을 수 있다 → board·kind 를 영속.\n * 이벤트(TwinEvent)와 달리 가변(status/revision 갱신) — 인스턴스의 \"현재 등록 상태\".\n * board 는 simple-json(멀티DB 이식 — postgres/mysql/sqlite/mssql/oracle 공통).\n * (CLAUDE.md: 모든 @ObjectType/@Field 는 영문 description 필수.)\n */\n@Entity()\n@Index('ix_twin_instance_0', (e: TwinInstance) => [e.domain, e.instanceId], { unique: true })\n@Index('ix_twin_instance_1', (e: TwinInstance) => [e.domain, e.spaceId]) // 공간→인스턴스 1급 질의(공간 중심 존재론)\n@ObjectType({ description: 'Registry of a hosted twin runtime instance — anchors reboot recovery (board + kind).' })\nexport class TwinInstance {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { description: 'Unique identifier of the registry record.' })\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true, description: 'Owning tenant domain.' })\n domain?: Domain\n\n @RelationId((e: TwinInstance) => e.domain)\n domainId?: string\n\n @Column()\n @Field({ description: 'Twin runtime instance id (unique within a domain).' })\n instanceId: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Domain kernel kind: wms | yms | mes.' })\n kind?: string\n\n /*\n * realityMode — 이 트윈의 현실이 어디서 오는가(runtime-state-model §0·§1 \"지속 현실 하나 + 여러 렌즈\"의 ① 선언).\n * 'mirror' = 현실은 외부 실물 시스템. 트윈=순수 렌즈, 재기동 시 실물서 재동기(startLive).\n * 'sim-world' = 트윈이 낳은 타임라인이 곧 진실이며 이어진다 → 재기동 시 저널서 resume(현실 지속).\n * 'sim-experiment' = seed 재현형. 재기동 = 같은 우주 재실행(저널 reset, revision 0) — 백지 시작이 버그가 아니라 선언된 거동.\n * 미선언(레거시)은 'sim-experiment'로 간주(가장 보수적 — 기존 데모 거동 보존).\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: \"Reality source declaration: 'mirror' (external system is truth, resync on reboot) | 'sim-world' (generated timeline is truth and persists, resume on reboot) | 'sim-experiment' (seed-reproducible, resets on reboot). Absent ⇒ treated as sim-experiment.\" })\n realityMode?: string\n\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => ScalarObject, { nullable: true, description: 'BoardDef blueprint (locations/equipment) required to replay events into state.' })\n board?: any\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Owning space id (co-location anchor). Promoted from board.spaceId — first-class \"space → its twins\" query (space-centered ontology).' })\n spaceId?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Owning area id within the space (optional scope). Promoted from board.areaId.' })\n areaId?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Lifecycle status: running | stopped.' })\n status?: string\n\n /*\n * purpose — 이 인스턴스가 운영 트윈인지, 격리된 벤치 사본인지 1급 선언(이름 접두사가 아니라 속성으로 구별).\n * 'bench' 는 규모/부하 실험용 격리 사본 — 목록·정리·안전 가드가 이 속성을 키로 동작(운영 트윈 무오염 보장).\n * 미선언(레거시)은 'operational' 로 간주.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: \"Instance purpose: 'operational' (real twin) | 'bench' (isolated copy for scale/load experiments). Absent ⇒ operational.\" })\n purpose?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'For a bench copy: the source spaceId it was copied from (provenance).' })\n copyOf?: string\n\n @Column({ type: 'int', nullable: true })\n @Field(type => Int, { nullable: true, description: 'Last persisted state revision observed for this instance.' })\n revision?: number\n\n @CreateDateColumn()\n @Field({ nullable: true, description: 'Timestamp when the instance was first registered.' })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true, description: 'Timestamp when the instance registry row was last updated.' })\n updatedAt?: Date\n}\n"]}
@@ -1,6 +1,25 @@
1
+ import { ListParam } from '@things-factory/shell';
1
2
  import { TwinEvent } from '../twin-event/twin-event.js';
3
+ import { TwinEventList } from '../twin-event/twin-event-type.js';
2
4
  export declare class TwinJournalQuery {
3
5
  twinEvents(instanceId: string, fromRevision: number, toRevision: number, eventType: string, limit: number, context: ResolverContext, untilTime?: string): Promise<TwinEvent[]>;
6
+ /**
7
+ * 저널 목록 — things-factory 표준 목록 계약(`ListParam` → `{ items, total }`).
8
+ *
9
+ * ── 왜 `twinEvents` 와 따로 두는가 ─────────────────────────────────────────
10
+ * `twinEvents` 는 배열만 돌려준다. 화면은 자기가 받은 게 전부인지 잘린 건지 알 수 없었고, 그래서
11
+ * 리스트가 **조용히 잘린 채 "이게 전부" 처럼** 보였다(원장 limit 120 이 대표적). 총건수를 함께
12
+ * 주면 "48 / 12,904" 라고 말할 수 있고, 사용자는 좁혀야 한다는 사실을 안다.
13
+ * 기존 호출자를 깨지 않으려고 `twinEvents` 는 그대로 두고 목록 계약을 새로 연다.
14
+ *
15
+ * ── 검색이 진짜인 이유 ─────────────────────────────────────────────────────
16
+ * `searchables` 는 전부 **인덱스 가능한 승격 컬럼**이다(payload JSON 안이 아니라). 프레임워크
17
+ * 질의 빌더는 `searchables` 에 없는 컬럼의 LIKE 를 경고 후 무시한다 — 인덱스 없는 전체 스캔을
18
+ * 막기 위해서다. 그 규율에 맞추려고 검색 축을 컬럼으로 승격했다(`twin-event-keys.ts`).
19
+ *
20
+ * 정렬 기본값은 revision DESC(최신순) — 저널의 자연 순서이자 ix_twin_event_0 가 그대로 타는 축.
21
+ */
22
+ twinEventList(params: ListParam, context: ResolverContext, instanceId?: string, spaceId?: string): Promise<TwinEventList>;
4
23
  /**
5
24
  * 업무 KPI — 시간창 처리량·소요시간·자원 점유. 저널을 **접어서** 만든다(새 계측을 심지 않는다).
6
25
  *
@@ -26,6 +45,15 @@ export declare class TwinJournalQuery {
26
45
  /** 비교 기준 — 같은 길이의 앞 구간(previous)이나 하루 전 같은 시간(yesterday). */
27
46
  compareTo?: string): Promise<any>;
28
47
  twinReplay(instanceId: string, untilRevision: number, untilTime: string, context: ResolverContext): Promise<any>;
48
+ twinCapacity(instanceId: string, spaceId: string,
49
+ /** 하루 몇 대를 낼 것인가 — 화면이 보고 있는 목표에서 나온다(선언된 수요). */
50
+ unitsPerDay: number,
51
+ /**
52
+ * 가용 시간을 잴 기준 주의 시작(ISO, 월요일). 공휴일이 없는 평상주여야 한다 —
53
+ * 공휴일은 연간 가용량을 따로 깎지, 이 공장의 평상시 천장을 정하지 않는다.
54
+ */
55
+ sampleWeekStart: string, context: ResolverContext): Promise<any>;
56
+ twinStructures(instanceId: string, context: ResolverContext): Promise<any>;
29
57
  twinTimeRange(spaceId: string, context: ResolverContext): Promise<{
30
58
  minTime: string | null;
31
59
  maxTime: string | null;
@@ -6,6 +6,7 @@ const typeorm_1 = require("typeorm");
6
6
  const type_graphql_1 = require("type-graphql");
7
7
  const shell_1 = require("@things-factory/shell");
8
8
  const twin_event_js_1 = require("../twin-event/twin-event.js");
9
+ const twin_event_type_js_1 = require("../twin-event/twin-event-type.js");
9
10
  const index_js_1 = require("../../engine/index.js");
10
11
  const kpi_query_js_1 = require("../../engine/kpi-query.js");
11
12
  /*
@@ -31,6 +32,63 @@ let TwinJournalQuery = class TwinJournalQuery {
31
32
  /* 최신순(DESC) 기본 — Ledger/recent 뷰. 범위 필터로 시간여행 구간 조회도 가능. */
32
33
  return (0, shell_1.getRepository)(twin_event_js_1.TwinEvent).find({ where, order: { revision: 'DESC' }, take: limit ?? 200 });
33
34
  }
35
+ /**
36
+ * 저널 목록 — things-factory 표준 목록 계약(`ListParam` → `{ items, total }`).
37
+ *
38
+ * ── 왜 `twinEvents` 와 따로 두는가 ─────────────────────────────────────────
39
+ * `twinEvents` 는 배열만 돌려준다. 화면은 자기가 받은 게 전부인지 잘린 건지 알 수 없었고, 그래서
40
+ * 리스트가 **조용히 잘린 채 "이게 전부" 처럼** 보였다(원장 limit 120 이 대표적). 총건수를 함께
41
+ * 주면 "48 / 12,904" 라고 말할 수 있고, 사용자는 좁혀야 한다는 사실을 안다.
42
+ * 기존 호출자를 깨지 않으려고 `twinEvents` 는 그대로 두고 목록 계약을 새로 연다.
43
+ *
44
+ * ── 검색이 진짜인 이유 ─────────────────────────────────────────────────────
45
+ * `searchables` 는 전부 **인덱스 가능한 승격 컬럼**이다(payload JSON 안이 아니라). 프레임워크
46
+ * 질의 빌더는 `searchables` 에 없는 컬럼의 LIKE 를 경고 후 무시한다 — 인덱스 없는 전체 스캔을
47
+ * 막기 위해서다. 그 규율에 맞추려고 검색 축을 컬럼으로 승격했다(`twin-event-keys.ts`).
48
+ *
49
+ * 정렬 기본값은 revision DESC(최신순) — 저널의 자연 순서이자 ix_twin_event_0 가 그대로 타는 축.
50
+ */
51
+ async twinEventList(params, context, instanceId, spaceId) {
52
+ const { domain } = context.state;
53
+ if (!instanceId && !spaceId)
54
+ throw new Error('either instanceId or spaceId is required');
55
+ /* 테넌트 격리 — twinKpi 와 같은 규약. 남의 트윈은 빈 결과가 아니라 명시 실패로 알린다
56
+ * (조용한 빈 목록은 "권한 없음" 과 "데이터 없음" 을 구분할 수 없게 만든다). */
57
+ if (instanceId && !(await index_js_1.TwinEngine.ownsRegistered(domain.id, instanceId))) {
58
+ throw new Error(`twin instance not found in this tenant: ${instanceId}`);
59
+ }
60
+ /* 대상 해소는 KPI 와 같은 창구를 쓴다 — 같은 공간을 보면서 화면마다 대상이 달라지면 안 된다. */
61
+ const instanceIds = await (0, kpi_query_js_1.resolveTwinTargets)(domain.id, instanceId, spaceId);
62
+ if (instanceIds.length === 0)
63
+ return { items: [], total: 0 };
64
+ /* 페이지 상한 — 화면이 실수로(또는 악의로) 저널 전체를 한 번에 달라고 해도 서버가 버틴다. */
65
+ const limit = Math.min(params.pagination?.limit ?? 100, 500);
66
+ const effective = {
67
+ ...params,
68
+ pagination: { page: params.pagination?.page ?? 1, limit },
69
+ /* 기본 정렬은 시각 내림차순 — 여러 트윈을 합칠 때 공통 축은 revision(트윈별 카운터)이 아니라
70
+ * **시각**이다. revision 으로 섞으면 서로 다른 트윈의 무관한 카운터가 뒤엉킨다. */
71
+ sortings: params.sortings?.length ? params.sortings : [{ name: 'eventTime', desc: true }]
72
+ };
73
+ const qb = (0, shell_1.getQueryBuilderFromListParams)({
74
+ repository: (0, shell_1.getRepository)(twin_event_js_1.TwinEvent),
75
+ params: effective,
76
+ domain,
77
+ /* 전부 인덱스 가능한 승격 컬럼 — 자유 검색어는 서버가 이 축들로 펼친다. */
78
+ searchables: ['epc', 'orderId', 'locationId', 'moverId', 'bizStep', 'eventType'],
79
+ /* 정렬은 인덱스가 받쳐 주는 축으로만 — 큰 저널에서 임의 컬럼 정렬은 전체 정렬 스캔이다. */
80
+ sortables: ['eventTime', 'revision', 'eventType', 'bizStep'],
81
+ defaultLimit: 100,
82
+ maxLimit: 500
83
+ });
84
+ /* 대상 범위는 호출자가 필터로 빼먹을 수 있는 값이 아니다 — 계약상 필수라 여기서 강제한다. */
85
+ qb.andWhere(`${qb.alias}.instanceId IN (:...instanceIds)`, { instanceIds });
86
+ const [items, total] = await qb.getManyAndCount();
87
+ /* 다음 커서는 **이번 페이지의 마지막 행**에서 만든다. 페이지가 상한보다 짧으면 뒤가 없다. */
88
+ const axes = (0, shell_1.cursorSortings)(effective.sortings, 'id');
89
+ const nextCursor = items.length === limit ? (0, shell_1.buildNextCursor)(items[items.length - 1], axes) : undefined;
90
+ return { items, total, nextCursor };
91
+ }
34
92
  /**
35
93
  * 업무 KPI — 시간창 처리량·소요시간·자원 점유. 저널을 **접어서** 만든다(새 계측을 심지 않는다).
36
94
  *
@@ -43,7 +101,7 @@ let TwinJournalQuery = class TwinJournalQuery {
43
101
  async twinKpi(context, spaceId, instanceId, fromTime, toTime, windowMinutes, buckets, lookbackMinutes, groupBy, groupLimit, compareTo) {
44
102
  /* 테넌트 격리 — 트윈 지정이면 소속을 확인한다. 공간 지정이면 대상 해소가 도메인 목록에서
45
103
  * 이뤄지므로(computeTwinKpi) 남의 트윈이 섞이지 않는다. 조용한 빈 결과가 아니라 명시 실패. */
46
- if (instanceId && !index_js_1.TwinEngine.owns(context.state.domain.id, instanceId)) {
104
+ if (instanceId && !(await index_js_1.TwinEngine.ownsRegistered(context.state.domain.id, instanceId))) {
47
105
  throw new Error(`twin instance not found in this tenant: ${instanceId}`);
48
106
  }
49
107
  if (!instanceId && !spaceId)
@@ -67,6 +125,33 @@ let TwinJournalQuery = class TwinJournalQuery {
67
125
  async twinReplay(instanceId, untilRevision, untilTime, context) {
68
126
  return index_js_1.TwinEngine.recover(context.state.domain.id, instanceId, untilRevision ?? undefined, untilTime ?? undefined);
69
127
  }
128
+ async twinCapacity(instanceId, spaceId, unitsPerDay, sampleWeekStart, context) {
129
+ if (instanceId && !(await index_js_1.TwinEngine.ownsRegistered(context.state.domain.id, instanceId))) {
130
+ throw new Error(`twin instance not found in this tenant: ${instanceId}`);
131
+ }
132
+ if (!instanceId && !spaceId)
133
+ throw new Error('either spaceId or instanceId is required');
134
+ if (!(unitsPerDay > 0))
135
+ throw new Error('unitsPerDay must be greater than zero');
136
+ /*
137
+ * 기준 주를 안 주면 **이번 주 월요일**을 쓴다. 그 주에 공휴일이 끼면 천장이 조금 낮게 나오는데,
138
+ * 그것은 조용한 오류가 아니라 **이번 주의 실제 가용 시간**이라 답으로서 정직하다. 평상시 천장을
139
+ * 보려는 호출자는 평상주를 지정한다.
140
+ */
141
+ const base = sampleWeekStart ? Date.parse(sampleWeekStart) : Date.now();
142
+ if (Number.isNaN(base))
143
+ throw new Error(`sampleWeekStart is not a valid ISO time: ${sampleWeekStart}`);
144
+ const d = new Date(base);
145
+ const monday = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() - ((d.getUTCDay() + 6) % 7));
146
+ return index_js_1.TwinEngine.capacity(context.state.domain.id, { instanceId, spaceId }, unitsPerDay, monday);
147
+ }
148
+ async twinStructures(instanceId, context) {
149
+ /* 이력 질문이므로 **등록부**에 소유를 묻는다 — 꺼진 트윈의 이력도 읽을 수 있어야 한다. */
150
+ if (!(await index_js_1.TwinEngine.ownsRegistered(context.state.domain.id, instanceId))) {
151
+ throw new Error(`twin instance not found in this tenant: ${instanceId}`);
152
+ }
153
+ return index_js_1.TwinEngine.structureHistory(context.state.domain.id, instanceId);
154
+ }
70
155
  async twinTimeRange(spaceId, context) {
71
156
  return index_js_1.TwinEngine.timeRange(context.state.domain.id, spaceId);
72
157
  }
@@ -87,9 +172,25 @@ tslib_1.__decorate([
87
172
  tslib_1.__metadata("design:paramtypes", [String, Number, Number, String, Number, Object, String]),
88
173
  tslib_1.__metadata("design:returntype", Promise)
89
174
  ], TwinJournalQuery.prototype, "twinEvents", null);
175
+ tslib_1.__decorate([
176
+ (0, type_graphql_1.Query)(returns => twin_event_type_js_1.TwinEventList, {
177
+ description: 'List twin journal events with the standard list contract (filters, pagination, sortings) plus the total match count. ' +
178
+ 'Scope is either one twin (instanceId) or a whole site (spaceId, which folds in every running operational twin co-located there, the same targets twinKpi uses) — one of the two is required. ' +
179
+ 'Searchable fields are the columns promoted out of the payload: epc (item), orderId, locationId, moverId (equipment), bizStep, eventType. ' +
180
+ 'Returns total so a caller can show an honest "shown of total" count instead of silently truncating a long journal. ' +
181
+ 'Defaults to newest first (revision descending) and to a page of 100; limit is capped at 500 per page.'
182
+ }),
183
+ tslib_1.__param(0, (0, type_graphql_1.Args)(type => shell_1.ListParam)),
184
+ tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
185
+ tslib_1.__param(2, (0, type_graphql_1.Arg)('instanceId', { nullable: true, description: 'Read one twin instance journal.' })),
186
+ tslib_1.__param(3, (0, type_graphql_1.Arg)('spaceId', { nullable: true, description: 'Read every running operational twin co-located in this space, merged on the shared time axis.' })),
187
+ tslib_1.__metadata("design:type", Function),
188
+ tslib_1.__metadata("design:paramtypes", [shell_1.ListParam, Object, String, String]),
189
+ tslib_1.__metadata("design:returntype", Promise)
190
+ ], TwinJournalQuery.prototype, "twinEventList", null);
90
191
  tslib_1.__decorate([
91
192
  (0, type_graphql_1.Query)(returns => shell_1.ScalarObject, {
92
- description: 'Windowed business KPI folded from the event journal, for one space (every running twin in it, folded together — averaging per-twin percentiles would be wrong) or one twin: throughput (completed tasks / orders), lead / work / wait time distributions (p50·p90), and per-resource busy ratio. Distinct from twinMetrics, which reports infrastructure throughput (events per second). When toTime is omitted the window ends at the twin\'s own last recorded event time, not wall clock — a simulated twin lives on its own clock, so asking for \'the last hour\' in real time would always find nothing. The basis used is reported in window.basis. Pass groupBy (resource | taskKind | node | order | area) to break the same window down by that perspective — per-group numbers use the same rules as the totals, so group counts sum to the total, and groups beyond groupLimit are reported as groups.truncated (how many groups) and groups.truncatedTasks (how much work those groups held) rather than silently dropped. Pass compareTo (previous | yesterday) to fold the same-length window shifted back and get comparison.delta (current minus that window); when the comparison window has no records, comparison.measured is false and no delta is produced rather than reporting zero change. Passing groupBy together with compareTo crosses the two: each group carries prevTasks / deltaTasks / deltaWorkP50Ms, groups absent from the comparison window are flagged isNew (no delta, so growth is not implied), and groups that had work before but none now are returned in groups.disappeared so a stopped area is never silently missing from the table. Area breakdown needs board nodes to carry a parent area; when they do not, groups.note says so instead of implying there was no work. When the journal read hits its row cap, eventsCapped is true and every number is a lower bound — narrow the window and ask again rather than reading the smaller figures as a drop in performance. Completions are counted in the window they complete; starts are looked up before the window, and any completion whose start was not found is reported in `unpaired` rather than silently dropped.'
193
+ description: 'Windowed business KPI folded from the event journal, for one space (every running twin in it, folded together — averaging per-twin percentiles would be wrong) or one twin: throughput (completed tasks / orders), lead / work / wait time distributions (p50·p90), and per-resource busy ratio. Distinct from twinMetrics, which reports infrastructure throughput (events per second). When toTime is omitted the window ends at the twin\'s own last recorded event time, not wall clock — a simulated twin lives on its own clock, so asking for \'the last hour\' in real time would always find nothing. The basis used is reported in window.basis. Pass groupBy (resource | taskKind | location | order | area | shift) to break the same window down by that perspective. The shift axis is **computed, not stored**: the journal records only times, and the shift is derived from each completion\'s time plus the declared site shifts and time zone so correcting a shift definition re-counts history correctly instead of leaving it frozen under the old boundaries. Sites that declare no shifts report every group as unknown rather than inventing shift names — per-group numbers use the same rules as the totals, so group counts sum to the total, and groups beyond groupLimit are reported as groups.truncated (how many groups) and groups.truncatedTasks (how much work those groups held) rather than silently dropped. Pass compareTo (previous | yesterday) to fold the same-length window shifted back and get comparison.delta (current minus that window); when the comparison window has no records, comparison.measured is false and no delta is produced rather than reporting zero change. Passing groupBy together with compareTo crosses the two: each group carries prevTasks / deltaTasks / deltaWorkP50Ms, groups absent from the comparison window are flagged isNew (no delta, so growth is not implied), and groups that had work before but none now are returned in groups.disappeared so a stopped area is never silently missing from the table. Area breakdown needs board locations to carry a parent area; when they do not, groups.note says so instead of implying there was no work. When the journal read hits its row cap, eventsCapped is true and every number is a lower bound — narrow the window and ask again rather than reading the smaller figures as a drop in performance. When operational targets are set for the space or instance, `targets` carries the verdicts [{metric, actual, target, direction, met, ratio}] — ratio is normalised so that 1 or more is always good, letting throughput and duration metrics be compared on one scale. A metric with no target, or with no measurement in the window, is absent from `targets` rather than reported as a miss, because a period with no work would otherwise read as a perfect score on duration metrics. Completions are counted in the window they complete; starts are looked up before the window, and any completion whose start was not found is reported in `unpaired` rather than silently dropped.'
93
194
  }),
94
195
  tslib_1.__param(0, (0, type_graphql_1.Ctx)()),
95
196
  tslib_1.__param(1, (0, type_graphql_1.Arg)('spaceId', { nullable: true })),
@@ -119,6 +220,30 @@ tslib_1.__decorate([
119
220
  tslib_1.__metadata("design:paramtypes", [String, Number, String, Object]),
120
221
  tslib_1.__metadata("design:returntype", Promise)
121
222
  ], TwinJournalQuery.prototype, "twinReplay", null);
223
+ tslib_1.__decorate([
224
+ (0, type_graphql_1.Query)(returns => shell_1.ScalarObject, {
225
+ nullable: true,
226
+ description: 'Steady-state capacity of a twin against a declared demand: per-operation supply vs required across equipment, personnel, physical assets and locations, plus the binding constraint and the daily ceiling. Answers "can this plant meet the target?" without running a simulation. Null when the twin is not running.'
227
+ }),
228
+ tslib_1.__param(0, (0, type_graphql_1.Arg)('instanceId', { nullable: true })),
229
+ tslib_1.__param(1, (0, type_graphql_1.Arg)('spaceId', { nullable: true })),
230
+ tslib_1.__param(2, (0, type_graphql_1.Arg)('unitsPerDay', type => type_graphql_1.Float)),
231
+ tslib_1.__param(3, (0, type_graphql_1.Arg)('sampleWeekStart', { nullable: true })),
232
+ tslib_1.__param(4, (0, type_graphql_1.Ctx)()),
233
+ tslib_1.__metadata("design:type", Function),
234
+ tslib_1.__metadata("design:paramtypes", [String, String, Number, String, Object]),
235
+ tslib_1.__metadata("design:returntype", Promise)
236
+ ], TwinJournalQuery.prototype, "twinCapacity", null);
237
+ tslib_1.__decorate([
238
+ (0, type_graphql_1.Query)(returns => shell_1.ScalarObject, {
239
+ description: "Structural revisions a twin has been through, newest last: when each took effect, how many journal events happened under it, and what changed from the previous one (counts by location type and equipment kind). The board itself is not returned — a revision holds the whole plant, but what a person wants is the difference."
240
+ }),
241
+ tslib_1.__param(0, (0, type_graphql_1.Arg)('instanceId')),
242
+ tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
243
+ tslib_1.__metadata("design:type", Function),
244
+ tslib_1.__metadata("design:paramtypes", [String, Object]),
245
+ tslib_1.__metadata("design:returntype", Promise)
246
+ ], TwinJournalQuery.prototype, "twinStructures", null);
122
247
  tslib_1.__decorate([
123
248
  (0, type_graphql_1.Query)(returns => shell_1.ScalarObject, {
124
249
  description: 'Event time range (min/max ISO) across all co-located twins of a space — the history scrubber time axis.'
@@ -1 +1 @@
1
- {"version":3,"file":"twin-journal-query.js","sourceRoot":"","sources":["../../../server/service/twin-journal/twin-journal-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { Between, LessThanOrEqual, MoreThanOrEqual } from 'typeorm'\nimport { Arg, Ctx, Int, Query, Resolver } from 'type-graphql'\n\nimport { getRepository, ScalarObject } from '@things-factory/shell'\n\nimport { TwinEvent } from '../twin-event/twin-event.js'\nimport { TwinEngine } from '../../engine/index.js'\nimport { computeTwinKpi } from '../../engine/kpi-query.js'\n\n/*\n * 저널 읽기 채널 — 상향 query.\n * twinEvents : append-only 이벤트 저널 조회(Ledger/History/Entity360) — instanceId·revision 범위·eventType 필터.\n * twinReplay : 내구 저널을 replay 해 상태 재구성(재부팅 복구·시간여행) — untilRevision 으로 임의 과거.\n * (실시간 스트림은 twinState subscription, 명령은 twin-control mutation. 이건 '질의' 표면.)\n */\n@Resolver()\nexport class TwinJournalQuery {\n @Query(returns => [TwinEvent], {\n description: 'Read the append-only twin event journal (Ledger / History / audit), filtered and ordered by revision.'\n })\n async twinEvents(\n @Arg('instanceId') instanceId: string,\n @Arg('fromRevision', type => Int, { nullable: true }) fromRevision: number,\n @Arg('toRevision', type => Int, { nullable: true }) toRevision: number,\n @Arg('eventType', { nullable: true }) eventType: string,\n @Arg('limit', type => Int, { nullable: true }) limit: number,\n @Ctx() context: ResolverContext,\n @Arg('untilTime', { nullable: true }) untilTime?: string\n ): Promise<TwinEvent[]> {\n const where: any = { domain: { id: context.state.domain.id }, instanceId }\n if (eventType) where.eventType = eventType\n /* 시각 커서(untilTime, ISO) 우선 — eventTime ≤ T(공유 clock). eventTime 은 ISO 문자열 컬럼이라 lexicographic 비교 성립. */\n if (untilTime != null) where.eventTime = LessThanOrEqual(untilTime)\n else if (fromRevision != null && toRevision != null) where.revision = Between(fromRevision, toRevision)\n else if (fromRevision != null) where.revision = MoreThanOrEqual(fromRevision)\n else if (toRevision != null) where.revision = LessThanOrEqual(toRevision)\n\n /* 최신순(DESC) 기본 — Ledger/recent 뷰. 범위 필터로 시간여행 구간 조회도 가능. */\n return getRepository(TwinEvent).find({ where, order: { revision: 'DESC' }, take: limit ?? 200 })\n }\n\n /**\n * 업무 KPI — 시간창 처리량·소요시간·자원 점유. 저널을 **접어서** 만든다(새 계측을 심지 않는다).\n *\n * `twinMetrics`(초당 이벤트 수 = 인프라 부하)와 **다른 것**이다: 이건 \"지난 한 시간에 몇 건 처리했고\n * 한 건에 얼마나 걸렸나\" 다. 이름이 비슷해 헷갈리기 쉬우므로 설명에 명시한다.\n *\n * 완료된 작업의 **시작 이벤트가 창 앞에 있을 수 있어** 조회 구간을 창보다 앞으로 넓힌다(lookback).\n * 그래도 못 찾은 것은 결과의 `unpaired` 로 드러난다 — 평균에서 조용히 빠지면 지표가 거짓이 된다.\n */\n @Query(returns => ScalarObject, {\n description:\n 'Windowed business KPI folded from the event journal, for one space (every running twin in it, folded together — averaging per-twin percentiles would be wrong) or one twin: throughput (completed tasks / orders), lead / work / wait time distributions (p50·p90), and per-resource busy ratio. Distinct from twinMetrics, which reports infrastructure throughput (events per second). When toTime is omitted the window ends at the twin\\'s own last recorded event time, not wall clock — a simulated twin lives on its own clock, so asking for \\'the last hour\\' in real time would always find nothing. The basis used is reported in window.basis. Pass groupBy (resource | taskKind | node | order | area) to break the same window down by that perspective — per-group numbers use the same rules as the totals, so group counts sum to the total, and groups beyond groupLimit are reported as groups.truncated (how many groups) and groups.truncatedTasks (how much work those groups held) rather than silently dropped. Pass compareTo (previous | yesterday) to fold the same-length window shifted back and get comparison.delta (current minus that window); when the comparison window has no records, comparison.measured is false and no delta is produced rather than reporting zero change. Passing groupBy together with compareTo crosses the two: each group carries prevTasks / deltaTasks / deltaWorkP50Ms, groups absent from the comparison window are flagged isNew (no delta, so growth is not implied), and groups that had work before but none now are returned in groups.disappeared so a stopped area is never silently missing from the table. Area breakdown needs board nodes to carry a parent area; when they do not, groups.note says so instead of implying there was no work. When the journal read hits its row cap, eventsCapped is true and every number is a lower bound — narrow the window and ask again rather than reading the smaller figures as a drop in performance. Completions are counted in the window they complete; starts are looked up before the window, and any completion whose start was not found is reported in `unpaired` rather than silently dropped.'\n })\n async twinKpi(\n @Ctx() context: ResolverContext,\n /* 공간이 사용자의 단위다 — 한 트윈만 보려면 instanceId 를 준다(둘 중 하나 필수). */\n @Arg('spaceId', { nullable: true }) spaceId?: string,\n @Arg('instanceId', { nullable: true }) instanceId?: string,\n /** 창 시작·끝(ISO). 미지정이면 최근 1시간. */\n @Arg('fromTime', { nullable: true }) fromTime?: string,\n @Arg('toTime', { nullable: true }) toTime?: string,\n /** 창 길이(분). fromTime 이 있으면 무시. 기본 60. */\n @Arg('windowMinutes', type => Int, { nullable: true }) windowMinutes?: number,\n /** 추세를 위해 창을 N 등분(1~48). 미지정이면 쪼개지 않는다. */\n @Arg('buckets', type => Int, { nullable: true }) buckets?: number,\n /** 시작 이벤트를 찾기 위해 창보다 앞으로 더 읽는 시간(분). 기본 60. */\n @Arg('lookbackMinutes', type => Int, { nullable: true }) lookbackMinutes?: number,\n /** 관점 축 — 같은 창을 자원별·작업종류별·도착지점별·오더별·구역별로 쪼갠다. */\n @Arg('groupBy', { nullable: true }) groupBy?: string,\n /** 축의 상한(기본 20, 최대 100). 넘치면 잘라내고 잘린 수를 알린다. */\n @Arg('groupLimit', type => Int, { nullable: true }) groupLimit?: number,\n /** 비교 기준 — 같은 길이의 앞 구간(previous)이나 하루 전 같은 시간(yesterday). */\n @Arg('compareTo', { nullable: true }) compareTo?: string\n ): Promise<any> {\n /* 테넌트 격리 — 트윈 지정이면 소속을 확인한다. 공간 지정이면 대상 해소가 도메인 목록에서\n * 이뤄지므로(computeTwinKpi) 남의 트윈이 섞이지 않는다. 조용한 빈 결과가 아니라 명시 실패. */\n if (instanceId && !TwinEngine.owns(context.state.domain.id, instanceId)) {\n throw new Error(`twin instance not found in this tenant: ${instanceId}`)\n }\n if (!instanceId && !spaceId) throw new Error('either spaceId or instanceId is required')\n /* 창 해소·조회·폴드는 `computeTwinKpi` 가 소유한다 — 같은 규칙을 AI 도구와 공유해야 한다\n * (두 곳에 적으면 화면과 AI 가 다른 숫자를 말한다). */\n return computeTwinKpi({\n domainId: context.state.domain.id,\n instanceId,\n spaceId,\n fromTime,\n toTime,\n windowMinutes,\n buckets,\n lookbackMinutes,\n groupBy: groupBy as any,\n groupLimit,\n compareTo: compareTo as any\n })\n }\n\n @Query(returns => ScalarObject, {\n nullable: true,\n description: 'Reconstruct twin state from the durable journal (DB → replay). untilTime (ISO) for time-travel on the shared clock (preferred); untilRevision for the lens-internal counter; omit both for latest.'\n })\n async twinReplay(\n @Arg('instanceId') instanceId: string,\n @Arg('untilRevision', type => Int, { nullable: true }) untilRevision: number,\n @Arg('untilTime', { nullable: true }) untilTime: string,\n @Ctx() context: ResolverContext\n ): Promise<any> {\n return TwinEngine.recover(context.state.domain.id, instanceId, untilRevision ?? undefined, untilTime ?? undefined)\n }\n\n @Query(returns => ScalarObject, {\n description: 'Event time range (min/max ISO) across all co-located twins of a space — the history scrubber time axis.'\n })\n async twinTimeRange(\n @Arg('spaceId') spaceId: string,\n @Ctx() context: ResolverContext\n ): Promise<{ minTime: string | null; maxTime: string | null }> {\n return TwinEngine.timeRange(context.state.domain.id, spaceId)\n }\n}\n"]}
1
+ {"version":3,"file":"twin-journal-query.js","sourceRoot":"","sources":["../../../server/service/twin-journal/twin-journal-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { Between, LessThanOrEqual, MoreThanOrEqual } from 'typeorm'\nimport { Arg, Args, Ctx, Float, Int, Query, Resolver } from 'type-graphql'\n\nimport { buildNextCursor, cursorSortings, getQueryBuilderFromListParams, getRepository, ListParam, ScalarObject } from '@things-factory/shell'\n\nimport { TwinEvent } from '../twin-event/twin-event.js'\nimport { TwinEventList } from '../twin-event/twin-event-type.js'\nimport { TwinEngine } from '../../engine/index.js'\nimport { computeTwinKpi, resolveTwinTargets } from '../../engine/kpi-query.js'\n\n/*\n * 저널 읽기 채널 — 상향 query.\n * twinEvents : append-only 이벤트 저널 조회(Ledger/History/Entity360) — instanceId·revision 범위·eventType 필터.\n * twinReplay : 내구 저널을 replay 해 상태 재구성(재부팅 복구·시간여행) — untilRevision 으로 임의 과거.\n * (실시간 스트림은 twinState subscription, 명령은 twin-control mutation. 이건 '질의' 표면.)\n */\n@Resolver()\nexport class TwinJournalQuery {\n @Query(returns => [TwinEvent], {\n description: 'Read the append-only twin event journal (Ledger / History / audit), filtered and ordered by revision.'\n })\n async twinEvents(\n @Arg('instanceId') instanceId: string,\n @Arg('fromRevision', type => Int, { nullable: true }) fromRevision: number,\n @Arg('toRevision', type => Int, { nullable: true }) toRevision: number,\n @Arg('eventType', { nullable: true }) eventType: string,\n @Arg('limit', type => Int, { nullable: true }) limit: number,\n @Ctx() context: ResolverContext,\n @Arg('untilTime', { nullable: true }) untilTime?: string\n ): Promise<TwinEvent[]> {\n const where: any = { domain: { id: context.state.domain.id }, instanceId }\n if (eventType) where.eventType = eventType\n /* 시각 커서(untilTime, ISO) 우선 — eventTime ≤ T(공유 clock). eventTime 은 ISO 문자열 컬럼이라 lexicographic 비교 성립. */\n if (untilTime != null) where.eventTime = LessThanOrEqual(untilTime)\n else if (fromRevision != null && toRevision != null) where.revision = Between(fromRevision, toRevision)\n else if (fromRevision != null) where.revision = MoreThanOrEqual(fromRevision)\n else if (toRevision != null) where.revision = LessThanOrEqual(toRevision)\n\n /* 최신순(DESC) 기본 — Ledger/recent 뷰. 범위 필터로 시간여행 구간 조회도 가능. */\n return getRepository(TwinEvent).find({ where, order: { revision: 'DESC' }, take: limit ?? 200 })\n }\n\n /**\n * 저널 목록 — things-factory 표준 목록 계약(`ListParam` → `{ items, total }`).\n *\n * ── 왜 `twinEvents` 와 따로 두는가 ─────────────────────────────────────────\n * `twinEvents` 는 배열만 돌려준다. 화면은 자기가 받은 게 전부인지 잘린 건지 알 수 없었고, 그래서\n * 리스트가 **조용히 잘린 채 \"이게 전부\" 처럼** 보였다(원장 limit 120 이 대표적). 총건수를 함께\n * 주면 \"48 / 12,904\" 라고 말할 수 있고, 사용자는 좁혀야 한다는 사실을 안다.\n * 기존 호출자를 깨지 않으려고 `twinEvents` 는 그대로 두고 목록 계약을 새로 연다.\n *\n * ── 검색이 진짜인 이유 ─────────────────────────────────────────────────────\n * `searchables` 는 전부 **인덱스 가능한 승격 컬럼**이다(payload JSON 안이 아니라). 프레임워크\n * 질의 빌더는 `searchables` 에 없는 컬럼의 LIKE 를 경고 후 무시한다 — 인덱스 없는 전체 스캔을\n * 막기 위해서다. 그 규율에 맞추려고 검색 축을 컬럼으로 승격했다(`twin-event-keys.ts`).\n *\n * 정렬 기본값은 revision DESC(최신순) — 저널의 자연 순서이자 ix_twin_event_0 가 그대로 타는 축.\n */\n @Query(returns => TwinEventList, {\n description:\n 'List twin journal events with the standard list contract (filters, pagination, sortings) plus the total match count. ' +\n 'Scope is either one twin (instanceId) or a whole site (spaceId, which folds in every running operational twin co-located there, the same targets twinKpi uses) — one of the two is required. ' +\n 'Searchable fields are the columns promoted out of the payload: epc (item), orderId, locationId, moverId (equipment), bizStep, eventType. ' +\n 'Returns total so a caller can show an honest \"shown of total\" count instead of silently truncating a long journal. ' +\n 'Defaults to newest first (revision descending) and to a page of 100; limit is capped at 500 per page.'\n })\n async twinEventList(\n @Args(type => ListParam) params: ListParam,\n @Ctx() context: ResolverContext,\n @Arg('instanceId', { nullable: true, description: 'Read one twin instance journal.' }) instanceId?: string,\n @Arg('spaceId', { nullable: true, description: 'Read every running operational twin co-located in this space, merged on the shared time axis.' }) spaceId?: string\n ): Promise<TwinEventList> {\n const { domain } = context.state\n if (!instanceId && !spaceId) throw new Error('either instanceId or spaceId is required')\n /* 테넌트 격리 — twinKpi 와 같은 규약. 남의 트윈은 빈 결과가 아니라 명시 실패로 알린다\n * (조용한 빈 목록은 \"권한 없음\" 과 \"데이터 없음\" 을 구분할 수 없게 만든다). */\n if (instanceId && !(await TwinEngine.ownsRegistered(domain.id, instanceId))) {\n throw new Error(`twin instance not found in this tenant: ${instanceId}`)\n }\n\n /* 대상 해소는 KPI 와 같은 창구를 쓴다 — 같은 공간을 보면서 화면마다 대상이 달라지면 안 된다. */\n const instanceIds = await resolveTwinTargets(domain.id, instanceId, spaceId)\n if (instanceIds.length === 0) return { items: [], total: 0 }\n\n /* 페이지 상한 — 화면이 실수로(또는 악의로) 저널 전체를 한 번에 달라고 해도 서버가 버틴다. */\n const limit = Math.min(params.pagination?.limit ?? 100, 500)\n const effective: ListParam = {\n ...params,\n pagination: { page: params.pagination?.page ?? 1, limit },\n /* 기본 정렬은 시각 내림차순 — 여러 트윈을 합칠 때 공통 축은 revision(트윈별 카운터)이 아니라\n * **시각**이다. revision 으로 섞으면 서로 다른 트윈의 무관한 카운터가 뒤엉킨다. */\n sortings: params.sortings?.length ? params.sortings : [{ name: 'eventTime', desc: true }]\n }\n\n const qb = getQueryBuilderFromListParams({\n repository: getRepository(TwinEvent),\n params: effective,\n domain,\n /* 전부 인덱스 가능한 승격 컬럼 — 자유 검색어는 서버가 이 축들로 펼친다. */\n searchables: ['epc', 'orderId', 'locationId', 'moverId', 'bizStep', 'eventType'],\n /* 정렬은 인덱스가 받쳐 주는 축으로만 — 큰 저널에서 임의 컬럼 정렬은 전체 정렬 스캔이다. */\n sortables: ['eventTime', 'revision', 'eventType', 'bizStep'],\n defaultLimit: 100,\n maxLimit: 500\n })\n /* 대상 범위는 호출자가 필터로 빼먹을 수 있는 값이 아니다 — 계약상 필수라 여기서 강제한다. */\n qb.andWhere(`${qb.alias}.instanceId IN (:...instanceIds)`, { instanceIds })\n\n const [items, total] = await qb.getManyAndCount()\n\n /* 다음 커서는 **이번 페이지의 마지막 행**에서 만든다. 페이지가 상한보다 짧으면 뒤가 없다. */\n const axes = cursorSortings(effective.sortings, 'id')\n const nextCursor = items.length === limit ? buildNextCursor(items[items.length - 1], axes) : undefined\n\n return { items, total, nextCursor }\n }\n\n /**\n * 업무 KPI — 시간창 처리량·소요시간·자원 점유. 저널을 **접어서** 만든다(새 계측을 심지 않는다).\n *\n * `twinMetrics`(초당 이벤트 수 = 인프라 부하)와 **다른 것**이다: 이건 \"지난 한 시간에 몇 건 처리했고\n * 한 건에 얼마나 걸렸나\" 다. 이름이 비슷해 헷갈리기 쉬우므로 설명에 명시한다.\n *\n * 완료된 작업의 **시작 이벤트가 창 앞에 있을 수 있어** 조회 구간을 창보다 앞으로 넓힌다(lookback).\n * 그래도 못 찾은 것은 결과의 `unpaired` 로 드러난다 — 평균에서 조용히 빠지면 지표가 거짓이 된다.\n */\n @Query(returns => ScalarObject, {\n description:\n 'Windowed business KPI folded from the event journal, for one space (every running twin in it, folded together — averaging per-twin percentiles would be wrong) or one twin: throughput (completed tasks / orders), lead / work / wait time distributions (p50·p90), and per-resource busy ratio. Distinct from twinMetrics, which reports infrastructure throughput (events per second). When toTime is omitted the window ends at the twin\\'s own last recorded event time, not wall clock — a simulated twin lives on its own clock, so asking for \\'the last hour\\' in real time would always find nothing. The basis used is reported in window.basis. Pass groupBy (resource | taskKind | location | order | area | shift) to break the same window down by that perspective. The shift axis is **computed, not stored**: the journal records only times, and the shift is derived from each completion\\'s time plus the declared site shifts and time zone — so correcting a shift definition re-counts history correctly instead of leaving it frozen under the old boundaries. Sites that declare no shifts report every group as unknown rather than inventing shift names — per-group numbers use the same rules as the totals, so group counts sum to the total, and groups beyond groupLimit are reported as groups.truncated (how many groups) and groups.truncatedTasks (how much work those groups held) rather than silently dropped. Pass compareTo (previous | yesterday) to fold the same-length window shifted back and get comparison.delta (current minus that window); when the comparison window has no records, comparison.measured is false and no delta is produced rather than reporting zero change. Passing groupBy together with compareTo crosses the two: each group carries prevTasks / deltaTasks / deltaWorkP50Ms, groups absent from the comparison window are flagged isNew (no delta, so growth is not implied), and groups that had work before but none now are returned in groups.disappeared so a stopped area is never silently missing from the table. Area breakdown needs board locations to carry a parent area; when they do not, groups.note says so instead of implying there was no work. When the journal read hits its row cap, eventsCapped is true and every number is a lower bound — narrow the window and ask again rather than reading the smaller figures as a drop in performance. When operational targets are set for the space or instance, `targets` carries the verdicts [{metric, actual, target, direction, met, ratio}] — ratio is normalised so that 1 or more is always good, letting throughput and duration metrics be compared on one scale. A metric with no target, or with no measurement in the window, is absent from `targets` rather than reported as a miss, because a period with no work would otherwise read as a perfect score on duration metrics. Completions are counted in the window they complete; starts are looked up before the window, and any completion whose start was not found is reported in `unpaired` rather than silently dropped.'\n })\n async twinKpi(\n @Ctx() context: ResolverContext,\n /* 공간이 사용자의 단위다 — 한 트윈만 보려면 instanceId 를 준다(둘 중 하나 필수). */\n @Arg('spaceId', { nullable: true }) spaceId?: string,\n @Arg('instanceId', { nullable: true }) instanceId?: string,\n /** 창 시작·끝(ISO). 미지정이면 최근 1시간. */\n @Arg('fromTime', { nullable: true }) fromTime?: string,\n @Arg('toTime', { nullable: true }) toTime?: string,\n /** 창 길이(분). fromTime 이 있으면 무시. 기본 60. */\n @Arg('windowMinutes', type => Int, { nullable: true }) windowMinutes?: number,\n /** 추세를 위해 창을 N 등분(1~48). 미지정이면 쪼개지 않는다. */\n @Arg('buckets', type => Int, { nullable: true }) buckets?: number,\n /** 시작 이벤트를 찾기 위해 창보다 앞으로 더 읽는 시간(분). 기본 60. */\n @Arg('lookbackMinutes', type => Int, { nullable: true }) lookbackMinutes?: number,\n /** 관점 축 — 같은 창을 자원별·작업종류별·도착지점별·오더별·구역별로 쪼갠다. */\n @Arg('groupBy', { nullable: true }) groupBy?: string,\n /** 축의 상한(기본 20, 최대 100). 넘치면 잘라내고 잘린 수를 알린다. */\n @Arg('groupLimit', type => Int, { nullable: true }) groupLimit?: number,\n /** 비교 기준 — 같은 길이의 앞 구간(previous)이나 하루 전 같은 시간(yesterday). */\n @Arg('compareTo', { nullable: true }) compareTo?: string\n ): Promise<any> {\n /* 테넌트 격리 — 트윈 지정이면 소속을 확인한다. 공간 지정이면 대상 해소가 도메인 목록에서\n * 이뤄지므로(computeTwinKpi) 남의 트윈이 섞이지 않는다. 조용한 빈 결과가 아니라 명시 실패. */\n if (instanceId && !(await TwinEngine.ownsRegistered(context.state.domain.id, instanceId))) {\n throw new Error(`twin instance not found in this tenant: ${instanceId}`)\n }\n if (!instanceId && !spaceId) throw new Error('either spaceId or instanceId is required')\n /* 창 해소·조회·폴드는 `computeTwinKpi` 가 소유한다 — 같은 규칙을 AI 도구와 공유해야 한다\n * (두 곳에 적으면 화면과 AI 가 다른 숫자를 말한다). */\n return computeTwinKpi({\n domainId: context.state.domain.id,\n instanceId,\n spaceId,\n fromTime,\n toTime,\n windowMinutes,\n buckets,\n lookbackMinutes,\n groupBy: groupBy as any,\n groupLimit,\n compareTo: compareTo as any\n })\n }\n\n @Query(returns => ScalarObject, {\n nullable: true,\n description: 'Reconstruct twin state from the durable journal (DB → replay). untilTime (ISO) for time-travel on the shared clock (preferred); untilRevision for the lens-internal counter; omit both for latest.'\n })\n async twinReplay(\n @Arg('instanceId') instanceId: string,\n @Arg('untilRevision', type => Int, { nullable: true }) untilRevision: number,\n @Arg('untilTime', { nullable: true }) untilTime: string,\n @Ctx() context: ResolverContext\n ): Promise<any> {\n return TwinEngine.recover(context.state.domain.id, instanceId, untilRevision ?? undefined, untilTime ?? undefined)\n }\n\n @Query(returns => ScalarObject, {\n nullable: true,\n description:\n 'Steady-state capacity of a twin against a declared demand: per-operation supply vs required across equipment, personnel, physical assets and locations, plus the binding constraint and the daily ceiling. Answers \"can this plant meet the target?\" without running a simulation. Null when the twin is not running.'\n })\n async twinCapacity(\n @Arg('instanceId', { nullable: true }) instanceId: string,\n @Arg('spaceId', { nullable: true }) spaceId: string,\n /** 하루 몇 대를 낼 것인가 — 화면이 보고 있는 목표에서 나온다(선언된 수요). */\n @Arg('unitsPerDay', type => Float) unitsPerDay: number,\n /**\n * 가용 시간을 잴 기준 주의 시작(ISO, 월요일). 공휴일이 없는 평상주여야 한다 —\n * 공휴일은 연간 가용량을 따로 깎지, 이 공장의 평상시 천장을 정하지 않는다.\n */\n @Arg('sampleWeekStart', { nullable: true }) sampleWeekStart: string,\n @Ctx() context: ResolverContext\n ): Promise<any> {\n if (instanceId && !(await TwinEngine.ownsRegistered(context.state.domain.id, instanceId))) {\n throw new Error(`twin instance not found in this tenant: ${instanceId}`)\n }\n if (!instanceId && !spaceId) throw new Error('either spaceId or instanceId is required')\n if (!(unitsPerDay > 0)) throw new Error('unitsPerDay must be greater than zero')\n\n /*\n * 기준 주를 안 주면 **이번 주 월요일**을 쓴다. 그 주에 공휴일이 끼면 천장이 조금 낮게 나오는데,\n * 그것은 조용한 오류가 아니라 **이번 주의 실제 가용 시간**이라 답으로서 정직하다. 평상시 천장을\n * 보려는 호출자는 평상주를 지정한다.\n */\n const base = sampleWeekStart ? Date.parse(sampleWeekStart) : Date.now()\n if (Number.isNaN(base)) throw new Error(`sampleWeekStart is not a valid ISO time: ${sampleWeekStart}`)\n const d = new Date(base)\n const monday = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() - ((d.getUTCDay() + 6) % 7))\n\n return TwinEngine.capacity(context.state.domain.id, { instanceId, spaceId }, unitsPerDay, monday)\n }\n\n @Query(returns => ScalarObject, {\n description:\n \"Structural revisions a twin has been through, newest last: when each took effect, how many journal events happened under it, and what changed from the previous one (counts by location type and equipment kind). The board itself is not returned — a revision holds the whole plant, but what a person wants is the difference.\"\n })\n async twinStructures(@Arg('instanceId') instanceId: string, @Ctx() context: ResolverContext): Promise<any> {\n /* 이력 질문이므로 **등록부**에 소유를 묻는다 — 꺼진 트윈의 이력도 읽을 수 있어야 한다. */\n if (!(await TwinEngine.ownsRegistered(context.state.domain.id, instanceId))) {\n throw new Error(`twin instance not found in this tenant: ${instanceId}`)\n }\n return TwinEngine.structureHistory(context.state.domain.id, instanceId)\n }\n\n @Query(returns => ScalarObject, {\n description: 'Event time range (min/max ISO) across all co-located twins of a space — the history scrubber time axis.'\n })\n async twinTimeRange(\n @Arg('spaceId') spaceId: string,\n @Ctx() context: ResolverContext\n ): Promise<{ minTime: string | null; maxTime: string | null }> {\n return TwinEngine.timeRange(context.state.domain.id, spaceId)\n }\n}\n"]}
@@ -17,7 +17,7 @@ export declare class TwinLifecycleMutation {
17
17
  */
18
18
  twinInstances(context: ResolverContext): Promise<string[]>;
19
19
  /**
20
- * 도메인 어휘 카탈로그 — 커널(레퍼런스) SSOT 를 그대로 노출. { wms:{label,nodeTypes[]}, yms:…, mes:… }.
20
+ * 도메인 어휘 카탈로그 — 커널(레퍼런스) SSOT 를 그대로 노출. { wms:{label,locationTypes[]}, yms:…, mes:… }.
21
21
  * 프로비저닝 UI 는 이걸 소싱해 노드 타입/시스템을 채운다(앱 내 하드코딩 금지).
22
22
  */
23
23
  twinDomainCatalog(): any;
@@ -23,8 +23,9 @@ let TwinLifecycleMutation = class TwinLifecycleMutation {
23
23
  /** 기동 — board 를 주면 provision+start, 없으면 레지스트리에 저장된 구조로 start. */
24
24
  async startTwinInstance(instanceId, context, kind, board) {
25
25
  const domainId = context.state.domain.id;
26
+ // 시각 기준은 **공간**이 갖는다(테넌트 아님) — 교대의 HH:MM 을 어느 기준으로 읽나.
26
27
  if (board)
27
- index_js_1.TwinEngine.start(instanceId, domainId, kind ?? 'wms', board);
28
+ index_js_1.TwinEngine.start(instanceId, domainId, kind ?? 'wms', await index_js_1.TwinEngine.withSpaceTimeBase(board, domainId));
28
29
  else
29
30
  await index_js_1.TwinEngine.startFromRegistry(domainId, instanceId);
30
31
  return true;
@@ -58,7 +59,7 @@ let TwinLifecycleMutation = class TwinLifecycleMutation {
58
59
  return list.filter((x) => x.running).map((x) => x.instanceId);
59
60
  }
60
61
  /**
61
- * 도메인 어휘 카탈로그 — 커널(레퍼런스) SSOT 를 그대로 노출. { wms:{label,nodeTypes[]}, yms:…, mes:… }.
62
+ * 도메인 어휘 카탈로그 — 커널(레퍼런스) SSOT 를 그대로 노출. { wms:{label,locationTypes[]}, yms:…, mes:… }.
62
63
  * 프로비저닝 UI 는 이걸 소싱해 노드 타입/시스템을 채운다(앱 내 하드코딩 금지).
63
64
  */
64
65
  twinDomainCatalog() {
@@ -129,7 +130,7 @@ tslib_1.__decorate([
129
130
  tslib_1.__metadata("design:returntype", Promise)
130
131
  ], TwinLifecycleMutation.prototype, "twinInstances", null);
131
132
  tslib_1.__decorate([
132
- (0, type_graphql_1.Query)(returns => shell_1.ScalarObject, { description: 'Domain vocabulary catalog (per-system location node types) sourced from the twin kernel — SSOT for provisioning UI.' }),
133
+ (0, type_graphql_1.Query)(returns => shell_1.ScalarObject, { description: 'Domain vocabulary catalog (per-system location location types) sourced from the twin kernel — SSOT for provisioning UI.' }),
133
134
  tslib_1.__metadata("design:type", Function),
134
135
  tslib_1.__metadata("design:paramtypes", []),
135
136
  tslib_1.__metadata("design:returntype", Object)
@@ -1 +1 @@
1
- {"version":3,"file":"twin-lifecycle-mutation.js","sourceRoot":"","sources":["../../../server/service/twin-lifecycle/twin-lifecycle-mutation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Mutation, Query, Resolver } from 'type-graphql'\n\nimport { ScalarObject } from '@things-factory/shell'\n\nimport { TwinEngine } from '../../engine/index.js'\n\nimport type { BoardDef } from '@operato/twin-kernel'\n\n/* 도메인 어휘 카탈로그(시스템별 노드 타입) — 커널이 SSOT. 앱/UI 는 이걸 소싱하고 재선언하지 않는다(방언 금지).\n * 조합은 domain-catalog.ts 가 소유 — 리졸버와 서버측 소비자(board-ai 도구 등)가 같은 형태를 본다. */\nimport { domainCatalog } from './domain-catalog.js'\n\n/*\n * 트윈 인스턴스 생명주기 채널 — 프로비저닝(상향, ADR-0015).\n * State/Command/Scenario 는 이미 running 인 인스턴스를 다루지만, 이 채널은 인스턴스 자체를 만들고/설정하고/없앤다.\n * 생명주기: provision(save, 미기동) → start → stop → delete. 모두 domain 스코프(context.state.domain.id).\n * board(BoardDef+layout)=구조, scenario=자극 은 분리 — 여기선 구조만 세운다. 자극은 controlTwinScenario 로.\n */\n@Resolver()\nexport class TwinLifecycleMutation {\n /** 프로비저닝(저장, 미기동) — 구조를 레지스트리에 등록만. 재프로비전 시 구조 변경이면 저널 purge(엔진). */\n @Mutation(returns => Boolean, {\n description: 'Provision (save) a twin instance structure (kind: wms|yms|mes) from a BoardDef without starting it.'\n })\n async saveTwinInstance(\n @Arg('instanceId') instanceId: string,\n @Arg('kind') kind: string,\n @Arg('board', type => ScalarObject) board: any,\n @Ctx() context: ResolverContext\n ): Promise<boolean> {\n await TwinEngine.provision(context.state.domain.id, instanceId, kind, board as BoardDef)\n return true\n }\n\n /** 기동 — board 를 주면 provision+start, 없으면 레지스트리에 저장된 구조로 start. */\n @Mutation(returns => Boolean, {\n description: 'Start a twin runtime instance and begin the worker tick. If board is given it provisions first; otherwise it starts from the saved registry board.'\n })\n async startTwinInstance(\n @Arg('instanceId') instanceId: string,\n @Ctx() context: ResolverContext,\n @Arg('kind', { nullable: true }) kind?: string,\n @Arg('board', type => ScalarObject, { nullable: true }) board?: any\n ): Promise<boolean> {\n const domainId = context.state.domain.id\n if (board) TwinEngine.start(instanceId, domainId, kind ?? 'wms', board as BoardDef)\n else await TwinEngine.startFromRegistry(domainId, instanceId)\n return true\n }\n\n @Mutation(returns => Boolean, { description: 'Stop a running twin runtime instance by its id (only if it belongs to the current domain).' })\n async stopTwinInstance(@Arg('instanceId') instanceId: string, @Ctx() context: ResolverContext): Promise<boolean> {\n if (!TwinEngine.owns(context.state.domain.id, instanceId)) return false // 테넌트 격리 — 남의 도메인 트윈 정지 차단\n await TwinEngine.stop(instanceId)\n return true\n }\n\n /** 삭제 — 정지 + 레지스트리 삭제 + 저널 purge(domain 스코프). */\n @Mutation(returns => Boolean, { description: 'Delete a twin instance: stop it, remove the registry entry and purge its event journal.' })\n async deleteTwinInstance(@Arg('instanceId') instanceId: string, @Ctx() context: ResolverContext): Promise<boolean> {\n await TwinEngine.remove(context.state.domain.id, instanceId)\n return true\n }\n\n /** 관리 목록 — 등록된 인스턴스 전체(kind/status/running/revision/노드·무버 수). 데이터 없으면 빈 배열. */\n @Query(returns => [ScalarObject], { description: 'List provisioned twin instances in the current domain with management metadata.' })\n async twinInstanceList(@Ctx() context: ResolverContext): Promise<any[]> {\n return TwinEngine.list(context.state.domain.id)\n }\n\n /** 단건 상세 — 프로비저닝 에디터용 board(구조+layout) 포함. */\n @Query(returns => ScalarObject, { nullable: true, description: 'Get one provisioned twin instance with its BoardDef (structure + layout) for editing.' })\n async twinInstanceDetail(@Arg('instanceId') instanceId: string, @Ctx() context: ResolverContext): Promise<any | null> {\n return TwinEngine.detail(context.state.domain.id, instanceId)\n }\n\n /**\n * 라이브(메모리) 인스턴스 id 목록 — 콘솔 셀렉터·포트폴리오용. **현재 도메인만**(테넌트 격리).\n * 과거엔 Object.keys(TwinEngine.instances) 로 전역 반환 → 다른 도메인 트윈이 종합현황/셀렉터에 새던 버그.\n * 도메인 스코프 목록(twinInstanceList 와 동일 소스)에서 running 만 추린다.\n */\n @Query(returns => [String], { description: 'List ids of running (in-memory) twin runtime instances in the current domain (tenant-isolated).' })\n async twinInstances(@Ctx() context: ResolverContext): Promise<string[]> {\n const list = await TwinEngine.list(context.state.domain.id)\n return list.filter((x: any) => x.running).map((x: any) => x.instanceId)\n }\n\n /**\n * 도메인 어휘 카탈로그 — 커널(레퍼런스) SSOT 를 그대로 노출. { wms:{label,nodeTypes[]}, yms:…, mes:… }.\n * 프로비저닝 UI 는 이걸 소싱해 노드 타입/시스템을 채운다(앱 내 하드코딩 금지).\n */\n @Query(returns => ScalarObject, { description: 'Domain vocabulary catalog (per-system location node types) sourced from the twin kernel — SSOT for provisioning UI.' })\n twinDomainCatalog(): any {\n return domainCatalog()\n }\n}\n"]}
1
+ {"version":3,"file":"twin-lifecycle-mutation.js","sourceRoot":"","sources":["../../../server/service/twin-lifecycle/twin-lifecycle-mutation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Mutation, Query, Resolver } from 'type-graphql'\n\nimport { ScalarObject } from '@things-factory/shell'\n\nimport { TwinEngine } from '../../engine/index.js'\n\nimport type { BoardDef } from '@operato/twin-kernel'\n\n/* 도메인 어휘 카탈로그(시스템별 노드 타입) — 커널이 SSOT. 앱/UI 는 이걸 소싱하고 재선언하지 않는다(방언 금지).\n * 조합은 domain-catalog.ts 가 소유 — 리졸버와 서버측 소비자(board-ai 도구 등)가 같은 형태를 본다. */\nimport { domainCatalog } from './domain-catalog.js'\n\n/*\n * 트윈 인스턴스 생명주기 채널 — 프로비저닝(상향, ADR-0015).\n * State/Command/Scenario 는 이미 running 인 인스턴스를 다루지만, 이 채널은 인스턴스 자체를 만들고/설정하고/없앤다.\n * 생명주기: provision(save, 미기동) → start → stop → delete. 모두 domain 스코프(context.state.domain.id).\n * board(BoardDef+layout)=구조, scenario=자극 은 분리 — 여기선 구조만 세운다. 자극은 controlTwinScenario 로.\n */\n@Resolver()\nexport class TwinLifecycleMutation {\n /** 프로비저닝(저장, 미기동) — 구조를 레지스트리에 등록만. 재프로비전 시 구조 변경이면 저널 purge(엔진). */\n @Mutation(returns => Boolean, {\n description: 'Provision (save) a twin instance structure (kind: wms|yms|mes) from a BoardDef without starting it.'\n })\n async saveTwinInstance(\n @Arg('instanceId') instanceId: string,\n @Arg('kind') kind: string,\n @Arg('board', type => ScalarObject) board: any,\n @Ctx() context: ResolverContext\n ): Promise<boolean> {\n await TwinEngine.provision(context.state.domain.id, instanceId, kind, board as BoardDef)\n return true\n }\n\n /** 기동 — board 를 주면 provision+start, 없으면 레지스트리에 저장된 구조로 start. */\n @Mutation(returns => Boolean, {\n description: 'Start a twin runtime instance and begin the worker tick. If board is given it provisions first; otherwise it starts from the saved registry board.'\n })\n async startTwinInstance(\n @Arg('instanceId') instanceId: string,\n @Ctx() context: ResolverContext,\n @Arg('kind', { nullable: true }) kind?: string,\n @Arg('board', type => ScalarObject, { nullable: true }) board?: any\n ): Promise<boolean> {\n const domainId = context.state.domain.id\n // 시각 기준은 **공간**이 갖는다(테넌트 아님) — 교대의 HH:MM 을 어느 기준으로 읽나.\n if (board) TwinEngine.start(instanceId, domainId, kind ?? 'wms', await TwinEngine.withSpaceTimeBase(board as BoardDef, domainId))\n else await TwinEngine.startFromRegistry(domainId, instanceId)\n return true\n }\n\n @Mutation(returns => Boolean, { description: 'Stop a running twin runtime instance by its id (only if it belongs to the current domain).' })\n async stopTwinInstance(@Arg('instanceId') instanceId: string, @Ctx() context: ResolverContext): Promise<boolean> {\n if (!TwinEngine.owns(context.state.domain.id, instanceId)) return false // 테넌트 격리 — 남의 도메인 트윈 정지 차단\n await TwinEngine.stop(instanceId)\n return true\n }\n\n /** 삭제 — 정지 + 레지스트리 삭제 + 저널 purge(domain 스코프). */\n @Mutation(returns => Boolean, { description: 'Delete a twin instance: stop it, remove the registry entry and purge its event journal.' })\n async deleteTwinInstance(@Arg('instanceId') instanceId: string, @Ctx() context: ResolverContext): Promise<boolean> {\n await TwinEngine.remove(context.state.domain.id, instanceId)\n return true\n }\n\n /** 관리 목록 — 등록된 인스턴스 전체(kind/status/running/revision/노드·무버 수). 데이터 없으면 빈 배열. */\n @Query(returns => [ScalarObject], { description: 'List provisioned twin instances in the current domain with management metadata.' })\n async twinInstanceList(@Ctx() context: ResolverContext): Promise<any[]> {\n return TwinEngine.list(context.state.domain.id)\n }\n\n /** 단건 상세 — 프로비저닝 에디터용 board(구조+layout) 포함. */\n @Query(returns => ScalarObject, { nullable: true, description: 'Get one provisioned twin instance with its BoardDef (structure + layout) for editing.' })\n async twinInstanceDetail(@Arg('instanceId') instanceId: string, @Ctx() context: ResolverContext): Promise<any | null> {\n return TwinEngine.detail(context.state.domain.id, instanceId)\n }\n\n /**\n * 라이브(메모리) 인스턴스 id 목록 — 콘솔 셀렉터·포트폴리오용. **현재 도메인만**(테넌트 격리).\n * 과거엔 Object.keys(TwinEngine.instances) 로 전역 반환 → 다른 도메인 트윈이 종합현황/셀렉터에 새던 버그.\n * 도메인 스코프 목록(twinInstanceList 와 동일 소스)에서 running 만 추린다.\n */\n @Query(returns => [String], { description: 'List ids of running (in-memory) twin runtime instances in the current domain (tenant-isolated).' })\n async twinInstances(@Ctx() context: ResolverContext): Promise<string[]> {\n const list = await TwinEngine.list(context.state.domain.id)\n return list.filter((x: any) => x.running).map((x: any) => x.instanceId)\n }\n\n /**\n * 도메인 어휘 카탈로그 — 커널(레퍼런스) SSOT 를 그대로 노출. { wms:{label,locationTypes[]}, yms:…, mes:… }.\n * 프로비저닝 UI 는 이걸 소싱해 노드 타입/시스템을 채운다(앱 내 하드코딩 금지).\n */\n @Query(returns => ScalarObject, { description: 'Domain vocabulary catalog (per-system location location types) sourced from the twin kernel — SSOT for provisioning UI.' })\n twinDomainCatalog(): any {\n return domainCatalog()\n }\n}\n"]}