@things-factory/headless-twin 10.1.29 → 10.1.31

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.
@@ -1,7 +1,5 @@
1
1
  export declare class TwinAttentionQuery {
2
- twinAttentions(context: ResolverContext, instanceId?: string, spaceId?: string,
3
- /** 상위 몇 건만 — 말하지 않으면 자르지 않는다. 전체 개수는 `twinAttentionDigest` 가 함께 답한다. */
4
- limit?: number): Promise<any[]>;
2
+ twinAttentions(context: ResolverContext, instanceId?: string, spaceId?: string): Promise<any[]>;
5
3
  /**
6
4
  * 잘랐다면 **몇 건을 잘랐는지**와 자리 색 요약까지 — 목록만 주면 화면이 "N건 더" 를 말할 수 없다.
7
5
  * 자리별 최고 심각도는 **전량을 근거로** 계산한다(상위 N 으로 줄이면 20위 밖 신호를 가진 자리가 색을 잃는다).
@@ -14,16 +14,26 @@ const attention_digest_js_1 = require("../../engine/attention-digest.js");
14
14
  * (실시간 스트림은 twinState subscription 이 attentions 를 포함. 이건 one-shot '질의' 표면.)
15
15
  */
16
16
  let TwinAttentionQuery = class TwinAttentionQuery {
17
- async twinAttentions(context, instanceId, spaceId, limit) {
17
+ async twinAttentions(context, instanceId, spaceId
18
+ /*
19
+ * ── 자르는 인자는 여기 없다 (2026-09-19, 수석 아키텍트 판정) ──────────────
20
+ *
21
+ * `limit` 이 있었다. 자르면서 **몇 건을 잘랐는지는 답에 싣지 못했다** — 답이 맨 목록이라 실을
22
+ * 자리가 없었기 때문이다. 자를 수 있는 문이 잘린 수를 못 말하면, 스무 건만 받은 화면은 그것이
23
+ * 전부인지 이백 중 스무 건인지 구별할 수 없다.
24
+ *
25
+ * 자르려는 소비자는 `twinAttentionDigest` 를 쓴다 — 그 문은 `attentionTotal` 을 함께 답하고,
26
+ * 자리별 색을 **전량을 근거로** 계산한다. 이 문은 자르지 않는다.
27
+ */
28
+ ) {
18
29
  if (spaceId)
19
- return (await index_js_1.TwinEngine.attentionsOfSpace(context.state.domain.id, spaceId, limit ?? undefined)).attentions;
30
+ return (await index_js_1.TwinEngine.attentionsOfSpace(context.state.domain.id, spaceId)).attentions;
20
31
  if (!instanceId || !index_js_1.TwinEngine.owns(context.state.domain.id, instanceId))
21
32
  return []; // 테넌트 격리
22
33
  // 라이브 커널 스냅샷(fresh) 우선 — snapshot()=resync() 는 캐시라 커맨드 델타 지연(recover 와 동일 원천 사용).
23
34
  const kernel = index_js_1.TwinEngine.kernel(context.state.domain.id, instanceId);
24
35
  const snap = kernel?.getSnapshot?.() ?? index_js_1.TwinEngine.snapshot(context.state.domain.id, instanceId);
25
- const list = snap?.attentions ?? [];
26
- return limit && limit > 0 ? (0, attention_digest_js_1.digestAttentions)(list, limit).attentions : list;
36
+ return snap?.attentions ?? [];
27
37
  }
28
38
  /**
29
39
  * 잘랐다면 **몇 건을 잘랐는지**와 자리 색 요약까지 — 목록만 주면 화면이 "N건 더" 를 말할 수 없다.
@@ -47,9 +57,8 @@ tslib_1.__decorate([
47
57
  tslib_1.__param(0, (0, type_graphql_1.Ctx)()),
48
58
  tslib_1.__param(1, (0, type_graphql_1.Arg)('instanceId', { nullable: true })),
49
59
  tslib_1.__param(2, (0, type_graphql_1.Arg)('spaceId', { nullable: true })),
50
- tslib_1.__param(3, (0, type_graphql_1.Arg)('limit', type => type_graphql_1.Int, { nullable: true })),
51
60
  tslib_1.__metadata("design:type", Function),
52
- tslib_1.__metadata("design:paramtypes", [Object, String, String, Number]),
61
+ tslib_1.__metadata("design:paramtypes", [Object, String, String]),
53
62
  tslib_1.__metadata("design:returntype", Promise)
54
63
  ], TwinAttentionQuery.prototype, "twinAttentions", null);
55
64
  tslib_1.__decorate([
@@ -1 +1 @@
1
- {"version":3,"file":"twin-attention-query.js","sourceRoot":"","sources":["../../../server/service/twin-attention/twin-attention-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Int, Query, Resolver } from 'type-graphql'\n\nimport { ScalarObject } from '@things-factory/shell'\n\nimport { TwinEngine } from '../../engine/index.js'\nimport { digestAttentions } from '../../engine/attention-digest.js'\n\n/*\n * 주목 신호(Attention) 질의 — 현재 스냅샷의 요약된 \"봐야 할 것\".\n * 커널 computeAttentions() 파생: severity(ISA-18.2/IEC 62682) · state(OPC UA A&C) ·\n * anchor(locationId/moverId/orderId = 공간 위치) · suggestedAction(조치 제안). kind 는 도메인 소유(무방언).\n * full StateSnapshot 대신 이 경량 목록을 AI read 도구·insight UI 가 소싱(대용량 덤프 회피).\n * (실시간 스트림은 twinState subscription 이 attentions 를 포함. 이건 one-shot '질의' 표면.)\n */\n@Resolver()\nexport class TwinAttentionQuery {\n @Query(returns => [ScalarObject], {\n description: 'Active attention signals (things to look at) derived from the current twin snapshot — severity (ISA-18.2), lifecycle state (OPC UA A&C), space anchor, and suggested action.'\n })\n async twinAttentions(\n @Ctx() context: ResolverContext,\n @Arg('instanceId', { nullable: true }) instanceId?: string,\n /*\n * **공간으로 물으면 그 현장의 모든 렌즈에서 모아 답한다** — 한 현실을 WMS·MES·YMS 가 함께 비추므로\n * \"이 현장에서 봐야 할 것\" 은 렌즈 하나로 답할 수 없다. 예전에는 화면이 인스턴스마다 물어 손으로\n * 합쳤고, 그러면 정렬 규칙이 화면마다 갈린다.\n *\n * 경고는 **합집합이 그대로 뜻이 있는 유일한 렌즈**다. 예측은 합칠 수 없다(P50 두 개를 더할 수 없다) —\n * 그래서 예측은 여전히 인스턴스별로 답하고 화면이 나란히 놓는다.\n */\n @Arg('spaceId', { nullable: true }) spaceId?: string,\n /** 상위 몇 건만 — 말하지 않으면 자르지 않는다. 전체 개수는 `twinAttentionDigest` 가 함께 답한다. */\n @Arg('limit', type => Int, { nullable: true }) limit?: number\n ): Promise<any[]> {\n if (spaceId) return (await TwinEngine.attentionsOfSpace(context.state.domain.id, spaceId, limit ?? undefined)).attentions\n if (!instanceId || !TwinEngine.owns(context.state.domain.id, instanceId)) return [] // 테넌트 격리\n // 라이브 커널 스냅샷(fresh) 우선 — snapshot()=resync() 는 캐시라 커맨드 델타 지연(recover 와 동일 원천 사용).\n const kernel = TwinEngine.kernel(context.state.domain.id, instanceId)\n const snap = kernel?.getSnapshot?.() ?? TwinEngine.snapshot(context.state.domain.id, instanceId)\n const list = snap?.attentions ?? []\n return limit && limit > 0 ? digestAttentions(list, limit).attentions : list\n }\n\n /**\n * 잘랐다면 **몇 건을 잘랐는지**와 자리 색 요약까지 — 목록만 주면 화면이 \"N건 더\" 를 말할 수 없다.\n * 자리별 최고 심각도는 **전량을 근거로** 계산한다(상위 N 으로 줄이면 20위 밖 신호를 가진 자리가 색을 잃는다).\n */\n @Query(returns => ScalarObject, {\n description: 'Attention signals for a space (all its lenses) or one twin, with the total count and the per-location highest severity folded from all of them.'\n })\n async twinAttentionDigest(\n @Ctx() context: ResolverContext,\n @Arg('spaceId', { nullable: true }) spaceId?: string,\n @Arg('instanceId', { nullable: true }) instanceId?: string,\n @Arg('limit', type => Int, { nullable: true }) limit?: number\n ): Promise<any> {\n if (spaceId) return TwinEngine.attentionsOfSpace(context.state.domain.id, spaceId, limit ?? undefined)\n if (!instanceId || !TwinEngine.owns(context.state.domain.id, instanceId)) return { attentions: [], attentionTotal: 0, severityByLocation: {} }\n const kernel = TwinEngine.kernel(context.state.domain.id, instanceId)\n const snap = kernel?.getSnapshot?.() ?? TwinEngine.snapshot(context.state.domain.id, instanceId)\n return digestAttentions((snap?.attentions ?? []).map((a: any) => ({ ...a, instanceId })), limit ?? undefined)\n }\n}\n"]}
1
+ {"version":3,"file":"twin-attention-query.js","sourceRoot":"","sources":["../../../server/service/twin-attention/twin-attention-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Int, Query, Resolver } from 'type-graphql'\n\nimport { ScalarObject } from '@things-factory/shell'\n\nimport { TwinEngine } from '../../engine/index.js'\nimport { digestAttentions } from '../../engine/attention-digest.js'\n\n/*\n * 주목 신호(Attention) 질의 — 현재 스냅샷의 요약된 \"봐야 할 것\".\n * 커널 computeAttentions() 파생: severity(ISA-18.2/IEC 62682) · state(OPC UA A&C) ·\n * anchor(locationId/moverId/orderId = 공간 위치) · suggestedAction(조치 제안). kind 는 도메인 소유(무방언).\n * full StateSnapshot 대신 이 경량 목록을 AI read 도구·insight UI 가 소싱(대용량 덤프 회피).\n * (실시간 스트림은 twinState subscription 이 attentions 를 포함. 이건 one-shot '질의' 표면.)\n */\n@Resolver()\nexport class TwinAttentionQuery {\n @Query(returns => [ScalarObject], {\n description: 'Active attention signals (things to look at) derived from the current twin snapshot — severity (ISA-18.2), lifecycle state (OPC UA A&C), space anchor, and suggested action.'\n })\n async twinAttentions(\n @Ctx() context: ResolverContext,\n @Arg('instanceId', { nullable: true }) instanceId?: string,\n /*\n * **공간으로 물으면 그 현장의 모든 렌즈에서 모아 답한다** — 한 현실을 WMS·MES·YMS 가 함께 비추므로\n * \"이 현장에서 봐야 할 것\" 은 렌즈 하나로 답할 수 없다. 예전에는 화면이 인스턴스마다 물어 손으로\n * 합쳤고, 그러면 정렬 규칙이 화면마다 갈린다.\n *\n * 경고는 **합집합이 그대로 뜻이 있는 유일한 렌즈**다. 예측은 합칠 수 없다(P50 두 개를 더할 수 없다) —\n * 그래서 예측은 여전히 인스턴스별로 답하고 화면이 나란히 놓는다.\n */\n @Arg('spaceId', { nullable: true }) spaceId?: string\n /*\n * ── 자르는 인자는 여기 없다 (2026-09-19, 수석 아키텍트 판정) ──────────────\n *\n * `limit` 이 있었다. 자르면서 **몇 건을 잘랐는지는 답에 싣지 못했다** — 답이 맨 목록이라 실을\n * 자리가 없었기 때문이다. 자를 수 있는 문이 잘린 수를 못 말하면, 스무 건만 받은 화면은 그것이\n * 전부인지 이백 중 스무 건인지 구별할 수 없다.\n *\n * 자르려는 소비자는 `twinAttentionDigest` 를 쓴다 — 그 문은 `attentionTotal` 을 함께 답하고,\n * 자리별 색을 **전량을 근거로** 계산한다. 이 문은 자르지 않는다.\n */\n ): Promise<any[]> {\n if (spaceId) return (await TwinEngine.attentionsOfSpace(context.state.domain.id, spaceId)).attentions\n if (!instanceId || !TwinEngine.owns(context.state.domain.id, instanceId)) return [] // 테넌트 격리\n // 라이브 커널 스냅샷(fresh) 우선 — snapshot()=resync() 는 캐시라 커맨드 델타 지연(recover 와 동일 원천 사용).\n const kernel = TwinEngine.kernel(context.state.domain.id, instanceId)\n const snap = kernel?.getSnapshot?.() ?? TwinEngine.snapshot(context.state.domain.id, instanceId)\n return snap?.attentions ?? []\n }\n\n /**\n * 잘랐다면 **몇 건을 잘랐는지**와 자리 색 요약까지 — 목록만 주면 화면이 \"N건 더\" 를 말할 수 없다.\n * 자리별 최고 심각도는 **전량을 근거로** 계산한다(상위 N 으로 줄이면 20위 밖 신호를 가진 자리가 색을 잃는다).\n */\n @Query(returns => ScalarObject, {\n description: 'Attention signals for a space (all its lenses) or one twin, with the total count and the per-location highest severity folded from all of them.'\n })\n async twinAttentionDigest(\n @Ctx() context: ResolverContext,\n @Arg('spaceId', { nullable: true }) spaceId?: string,\n @Arg('instanceId', { nullable: true }) instanceId?: string,\n @Arg('limit', type => Int, { nullable: true }) limit?: number\n ): Promise<any> {\n if (spaceId) return TwinEngine.attentionsOfSpace(context.state.domain.id, spaceId, limit ?? undefined)\n if (!instanceId || !TwinEngine.owns(context.state.domain.id, instanceId)) return { attentions: [], attentionTotal: 0, severityByLocation: {} }\n const kernel = TwinEngine.kernel(context.state.domain.id, instanceId)\n const snap = kernel?.getSnapshot?.() ?? TwinEngine.snapshot(context.state.domain.id, instanceId)\n return digestAttentions((snap?.attentions ?? []).map((a: any) => ({ ...a, instanceId })), limit ?? undefined)\n }\n}\n"]}
@@ -1,6 +1,21 @@
1
1
  import { ListParam } from '@things-factory/shell';
2
2
  import { TwinEvent } from '../twin-event/twin-event.js';
3
3
  import { TwinEventList } from '../twin-event/twin-event-type.js';
4
+ /**
5
+ * 자유 검색어가 펼쳐지는 축 — **목록과 건수가 같은 것을 본다**(2026-09-19).
6
+ *
7
+ * 전부 인덱스 가능한 승격 컬럼이다(payload JSON 안이 아니라). 프레임워크 질의 빌더는 이 목록에 없는
8
+ * 컬럼의 LIKE 를 경고 후 무시한다 — 인덱스 없는 전체 스캔을 막기 위해서다.
9
+ *
10
+ * ── 왜 상수인가 ────────────────────────────────────────────────────────────
11
+ * 목록과 건수가 **다른 빌더**로 만들어진다(빠른 갈래의 건수 probe). 두 자리에 배열을 따로 적으면
12
+ * 한쪽만 늘어나는 날 목록은 걸러지고 건수는 안 걸러진다 — 화면에 「한 건」과 「전체 수」가 나란히 선다.
13
+ *
14
+ * 지금은 그 일이 **일어날 수 없다**: 빠른 갈래는 검색어가 있으면 아예 안 탄다
15
+ * (`event-type-split-shape.ts` — 시험 「자유 검색어」가 그 축을 붙든다). 그래도 같은 상수를 쓴다 —
16
+ * 그 규칙이 언젠가 느슨해질 때 이 자리가 조용히 갈리지 않게.
17
+ */
18
+ export declare const JOURNAL_SEARCHABLES: readonly ["epc", "orderId", "locationId", "moverId", "bizStep", "eventType", "correlationId"];
4
19
  export declare class TwinJournalQuery {
5
20
  twinEvents(instanceId: string, fromRevision: number, toRevision: number, eventType: string, limit: number, context: ResolverContext, untilTime?: string, fromTime?: string): Promise<TwinEvent[]>;
6
21
  /**
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.TwinJournalQuery = void 0;
3
+ exports.TwinJournalQuery = exports.JOURNAL_SEARCHABLES = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const log_js_1 = require("../../engine/log.js");
6
6
  const typeorm_1 = require("typeorm");
@@ -42,6 +42,21 @@ function toDateValue(v) {
42
42
  };
43
43
  return Array.isArray(v) ? v.map(one) : one(v);
44
44
  }
45
+ /**
46
+ * 자유 검색어가 펼쳐지는 축 — **목록과 건수가 같은 것을 본다**(2026-09-19).
47
+ *
48
+ * 전부 인덱스 가능한 승격 컬럼이다(payload JSON 안이 아니라). 프레임워크 질의 빌더는 이 목록에 없는
49
+ * 컬럼의 LIKE 를 경고 후 무시한다 — 인덱스 없는 전체 스캔을 막기 위해서다.
50
+ *
51
+ * ── 왜 상수인가 ────────────────────────────────────────────────────────────
52
+ * 목록과 건수가 **다른 빌더**로 만들어진다(빠른 갈래의 건수 probe). 두 자리에 배열을 따로 적으면
53
+ * 한쪽만 늘어나는 날 목록은 걸러지고 건수는 안 걸러진다 — 화면에 「한 건」과 「전체 수」가 나란히 선다.
54
+ *
55
+ * 지금은 그 일이 **일어날 수 없다**: 빠른 갈래는 검색어가 있으면 아예 안 탄다
56
+ * (`event-type-split-shape.ts` — 시험 「자유 검색어」가 그 축을 붙든다). 그래도 같은 상수를 쓴다 —
57
+ * 그 규칙이 언젠가 느슨해질 때 이 자리가 조용히 갈리지 않게.
58
+ */
59
+ exports.JOURNAL_SEARCHABLES = ['epc', 'orderId', 'locationId', 'moverId', 'bizStep', 'eventType', 'correlationId'];
45
60
  let TwinJournalQuery = class TwinJournalQuery {
46
61
  async twinEvents(instanceId, fromRevision, toRevision, eventType, limit, context, untilTime, fromTime) {
47
62
  const where = { domain: { id: context.state.domain.id }, instanceId };
@@ -269,7 +284,7 @@ let TwinJournalQuery = class TwinJournalQuery {
269
284
  repository: (0, shell_1.getRepository)(twin_event_js_1.TwinEvent),
270
285
  params: { ...effective, pagination: { page: 1, limit: COUNT_CAP + 1 } },
271
286
  domain,
272
- searchables: [],
287
+ searchables: [...exports.JOURNAL_SEARCHABLES],
273
288
  sortables: ['eventTime', 'revision', 'eventType', 'bizStep'],
274
289
  defaultLimit: COUNT_CAP + 1,
275
290
  maxLimit: COUNT_CAP + 1
@@ -296,7 +311,7 @@ let TwinJournalQuery = class TwinJournalQuery {
296
311
  params: effective,
297
312
  domain,
298
313
  /* 전부 인덱스 가능한 승격 컬럼 — 자유 검색어는 서버가 이 축들로 펼친다. */
299
- searchables: ['epc', 'orderId', 'locationId', 'moverId', 'bizStep', 'eventType', 'correlationId'],
314
+ searchables: [...exports.JOURNAL_SEARCHABLES],
300
315
  /* 정렬은 인덱스가 받쳐 주는 축으로만 — 큰 저널에서 임의 컬럼 정렬은 전체 정렬 스캔이다. */
301
316
  sortables: ['eventTime', 'revision', 'eventType', 'bizStep'],
302
317
  defaultLimit: 100,
@@ -1 +1 @@
1
- {"version":3,"file":"twin-journal-query.js","sourceRoot":"","sources":["../../../server/service/twin-journal/twin-journal-query.ts"],"names":[],"mappings":";;;;AAAA,gDAA8C;AAC9C,qCAAmE;AACnE,+CAA0E;AAE1E,iDAA8I;AAE9I,+DAAuD;AACvD,qEAAkE;AAClE,yEAAgE;AAChE,oDAAkD;AAClD,8DAAqD;AACrD,0EAAmE;AACnE,4DAA8E;AAC9E,yDAAiD;AACjD,yFAAyE;AACzE,+DAA0D;AAC1D,2EAAwD;AAExD;;;;;GAKG;AACH;;;GAGG;AACH,MAAM,SAAS,GAAG,oCAAiB,CAAA;AAEnC;;;;GAIG;AACH,SAAS,WAAW,CAAC,CAAM;IACzB,MAAM,GAAG,GAAG,CAAC,CAAM,EAAE,EAAE;QACrB,IAAI,CAAC,YAAY,IAAI;YAAE,OAAO,CAAC,CAAA;QAC/B,MAAM,CAAC,GAAG,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAA;QACrD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAA;IAC1C,CAAC,CAAA;IACD,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;AAC/C,CAAC;AAGM,IAAM,gBAAgB,GAAtB,MAAM,gBAAgB;IAMrB,AAAN,KAAK,CAAC,UAAU,CACK,UAAkB,EACiB,YAAoB,EACtB,UAAkB,EAChC,SAAiB,EACR,KAAa,EACrD,OAAwB,EACO,SAAkB,EACnB,QAAiB;QAEtD,MAAM,KAAK,GAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,UAAU,EAAE,CAAA;QAC1E,IAAI,SAAS;YAAE,KAAK,CAAC,SAAS,GAAG,SAAS,CAAA;QAC1C;;;;;;;;;;;;;;;;;;WAkBG;QACH,IAAI,SAAS,IAAI,IAAI,IAAI,QAAQ,IAAI,IAAI;YAAE,KAAK,CAAC,SAAS,GAAG,IAAA,iBAAO,EAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,EAAE,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,CAAA;aACxG,IAAI,SAAS,IAAI,IAAI;YAAE,KAAK,CAAC,SAAS,GAAG,IAAA,yBAAe,EAAC,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,CAAA;aAC7E,IAAI,QAAQ,IAAI,IAAI;YAAE,KAAK,CAAC,SAAS,GAAG,IAAA,yBAAe,EAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAA;aAC3E,IAAI,YAAY,IAAI,IAAI,IAAI,UAAU,IAAI,IAAI;YAAE,KAAK,CAAC,QAAQ,GAAG,IAAA,iBAAO,EAAC,YAAY,EAAE,UAAU,CAAC,CAAA;aAClG,IAAI,YAAY,IAAI,IAAI;YAAE,KAAK,CAAC,QAAQ,GAAG,IAAA,yBAAe,EAAC,YAAY,CAAC,CAAA;aACxE,IAAI,UAAU,IAAI,IAAI;YAAE,KAAK,CAAC,QAAQ,GAAG,IAAA,yBAAe,EAAC,UAAU,CAAC,CAAA;QAEzE;;;;;;;;;;;;;;;;;;;;;;WAsBG;QACH;;;;;;;;;;;;;;;;;;;;WAoBG;QACH,MAAM,KAAK,GAAQ,IAAA,+BAAY,EAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,EAAE,UAAU,EAAE,CAAC,CAAA;QAElF;;;;;;;;;;;;;;;;;;WAkBG;QACH,MAAM,EAAE,GAAG,IAAA,qBAAa,EAAC,yBAAS,CAAC;aAChC,kBAAkB,CAAC,GAAG,CAAC;aACvB,KAAK,CAAC,sBAAsB,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;aACpE,QAAQ,CAAC,4BAA4B,EAAE,EAAE,UAAU,EAAE,CAAC,CAAA;QACzD,IAAI,SAAS;YAAE,EAAE,CAAC,QAAQ,CAAC,0BAA0B,EAAE,EAAE,SAAS,EAAE,CAAC,CAAA;QACrE,IAAI,KAAK,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;YAClC,0CAA0C;YAC1C,IAAI,SAAS,IAAI,IAAI,IAAI,QAAQ,IAAI,IAAI,EAAE,CAAC;gBAC1C,EAAE,CAAC,QAAQ,CAAC,8CAA8C,EAAE,EAAE,QAAQ,EAAE,IAAI,IAAI,CAAC,QAAQ,CAAC,EAAE,SAAS,EAAE,IAAI,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,CAAA;YAC/H,CAAC;iBAAM,IAAI,SAAS,IAAI,IAAI,EAAE,CAAC;gBAC7B,EAAE,CAAC,QAAQ,CAAC,2BAA2B,EAAE,EAAE,SAAS,EAAE,IAAI,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,CAAA;YAC9E,CAAC;iBAAM,CAAC;gBACN,EAAE,CAAC,QAAQ,CAAC,0BAA0B,EAAE,EAAE,QAAQ,EAAE,IAAI,IAAI,CAAC,QAAkB,CAAC,EAAE,CAAC,CAAA;YACrF,CAAC;QACH,CAAC;aAAM,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YACxC,IAAI,YAAY,IAAI,IAAI,IAAI,UAAU,IAAI,IAAI;gBAAE,EAAE,CAAC,QAAQ,CAAC,kCAAkC,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE,EAAE,UAAU,EAAE,CAAC,CAAA;iBAClI,IAAI,YAAY,IAAI,IAAI;gBAAE,EAAE,CAAC,QAAQ,CAAC,qBAAqB,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAA;;gBACpF,EAAE,CAAC,QAAQ,CAAC,mBAAmB,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE,CAAC,CAAA;QAC3D,CAAC;QACD,KAAK,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;YAAE,EAAE,CAAC,UAAU,CAAC,KAAK,MAAM,EAAE,EAAE,SAAmB,CAAC,CAAA;QAC1G,OAAO,EAAE,CAAC,KAAK,CAAC,KAAK,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,CAAA;IACzC,CAAC;IAED;;;;;;;;;;;;;;;OAeG;IASG,AAAN,KAAK,CAAC,aAAa,CACQ,MAAiB,EACnC,OAAwB,EACwD,UAAmB,EACwC,OAAgB;QAElK,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAChC,IAAI,CAAC,UAAU,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAA;QACxF;kEAC0D;QAC1D,IAAI,UAAU,IAAI,CAAC,CAAC,MAAM,qBAAU,CAAC,cAAc,CAAC,MAAM,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC,EAAE,CAAC;YAC5E,MAAM,IAAI,KAAK,CAAC,2CAA2C,UAAU,EAAE,CAAC,CAAA;QAC1E,CAAC;QAED,6DAA6D;QAC7D,MAAM,WAAW,GAAG,MAAM,IAAA,iCAAkB,EAAC,MAAM,CAAC,EAAE,EAAE,UAAU,EAAE,OAAO,CAAC,CAAA;QAC5E,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAA;QAE5D,0DAA0D;QAC1D,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,UAAU,EAAE,KAAK,IAAI,GAAG,EAAE,GAAG,CAAC,CAAA;QAC5D,MAAM,SAAS,GAAc;YAC3B,GAAG,MAAM;YACT;;;;;;;;eAQG;YACH,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,UAAU,EAAE,IAAI,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,UAAU,EAAE,KAAK,EAAE;YAC1F;oEACwD;YACxD,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;YACzF;;;;;;;eAOG;YACH,OAAO,EAAE,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;SAC/G,CAAA;QAED;;;;;;;;;;;;WAYG;QACH,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC7B,MAAM,IAAI,GAAG,MAAM,IAAA,yCAAa,EAAC,MAAM,CAAC,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,CAAA;YAC7E,IAAI,IAAI;gBAAE,OAAO,IAAI,CAAA;QACvB,CAAC;QAED;;;;;;;;;WASG;QACH,MAAM,QAAQ,GAAsB,WAAW,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAA;QAChF,MAAM,KAAK,GAAG,IAAA,sCAAU,EAAC,SAAS,EAAE,SAAS,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAA;QAC7D,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,IAAI,GAAG,MAAM,IAAA,wCAAkB,EAAC,MAAM,CAAC,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,CAAC,UAAU,EAAE,KAAK,CAAC,CAAA;YAClH,IAAI,IAAI,EAAE,CAAC;gBACT;;;mBAGG;gBACH,MAAM,KAAK,GAAG,IAAA,qCAA6B,EAAC;oBAC1C,UAAU,EAAE,IAAA,qBAAa,EAAC,yBAAS,CAAC;oBACpC,MAAM,EAAE,EAAE,GAAG,SAAS,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,GAAG,CAAC,EAAE,EAAE;oBACvE,MAAM;oBACN,WAAW,EAAE,EAAE;oBACf,SAAS,EAAE,CAAC,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,CAAC;oBAC5D,YAAY,EAAE,SAAS,GAAG,CAAC;oBAC3B,QAAQ,EAAE,SAAS,GAAG,CAAC;iBACxB,CAAC,CAAA;gBACF,KAAK,CAAC,QAAQ,CAAC,GAAG,KAAK,CAAC,KAAK,kCAAkC,EAAE,EAAE,WAAW,EAAE,CAAC,CAAA;gBACjF,MAAM,OAAO,GAAG,MAAM,KAAK;qBACxB,OAAO,EAAE;qBACT,MAAM,CAAC,GAAG,KAAK,CAAC,KAAK,KAAK,CAAC;qBAC3B,UAAU,EAAE;qBACZ,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAA;gBACpB,MAAM,MAAM,GAAG,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAA;gBACrD,OAAO;oBACL,KAAK,EAAE,IAAI,CAAC,KAAK;oBACjB,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAQ,CAAC,MAAM;oBAC3C,MAAM;oBACN,QAAQ,EAAE,SAAS;oBACnB,UAAU,EAAE,IAAI,CAAC,UAAU;oBAC3B,cAAc,EAAE,SAAS;iBAC1B,CAAA;YACH,CAAC;QACH,CAAC;QAED,MAAM,EAAE,GAAG,IAAA,qCAA6B,EAAC;YACvC,UAAU,EAAE,IAAA,qBAAa,EAAC,yBAAS,CAAC;YACpC,MAAM,EAAE,SAAS;YACjB,MAAM;YACN,+CAA+C;YAC/C,WAAW,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,eAAe,CAAC;YACjG,wDAAwD;YACxD,SAAS,EAAE,CAAC,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,CAAC;YAC5D,YAAY,EAAE,GAAG;YACjB,QAAQ,EAAE,GAAG;YACb;;;;;;;;;;;;;eAaG;YACH,cAAc,EAAE,QAAQ;SACzB,CAAC,CAAA;QACF,yDAAyD;QACzD,EAAE,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,KAAK,kCAAkC,EAAE,EAAE,WAAW,EAAE,CAAC,CAAA;QAE3E;;;;;;;;;;;;WAYG;QACH,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,OAAO,EAAE,CAAA;QAChC,MAAM,KAAK,GAAG,EAAE;aACb,KAAK,EAAE;aACP,OAAO,EAAE,CAAC,gDAAgD;aAC1D,IAAI,CAAC,CAAC,CAAC;aACP,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC;aACnB,MAAM,CAAC,GAAG,EAAE,CAAC,KAAK,KAAK,CAAC,CAAA;QAC3B,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,UAAU,EAAE,CAAA;QACxC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAA;QACzC,wDAAwD;QACxD,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAA;QAEjD,0DAA0D;QAC1D,MAAM,IAAI,GAAG,IAAA,sBAAc,EAAC,SAAS,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;QACzD,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,IAAA,uBAAe,EAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QAEtG;;;;;;;;;;;;WAYG;QACH,IAAI,cAAkC,CAAA;QACtC,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC7B,MAAM,KAAK,GAAG,MAAM,IAAA,qBAAa,EAAC,yBAAS,CAAC;iBACzC,kBAAkB,CAAC,GAAG,CAAC;iBACvB,MAAM,CAAC,YAAY,EAAE,UAAU,CAAC;iBAChC,KAAK,CAAC,sBAAsB,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC;iBACtD,QAAQ,CAAC,4BAA4B,EAAE,EAAE,UAAU,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC;iBACtE,OAAO,CAAC,YAAY,EAAE,KAAK,CAAC;iBAC5B,KAAK,CAAC,CAAC,CAAC;iBACR,SAAS,EAAyB;iBAClC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAA;YACpB,MAAM,EAAE,GAAG,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAA;YAClC,IAAI,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,CAAC;gBAAE,cAAc,GAAG,EAAE,CAAA;QACxD,CAAC;QAED,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,cAAc,EAAE,CAAA;IAClF,CAAC;IAED;;;;;;;;OAQG;IAKG,AAAN,KAAK,CAAC,OAAO,CACJ,OAAwB,EAEK,OAAgB,EACb,UAAmB,EAErB,QAAiB,EACnB,MAAe,EAEK,aAAsB,EAE5B,OAAgB,EAER,eAAwB,EAE7C,OAAgB,EAEA,UAAmB,EAEjC,SAAkB,EAEC,eAAwB,EAEtC,cAAuB;QAElE;8EACsE;QACtE,IAAI,UAAU,IAAI,CAAC,CAAC,MAAM,qBAAU,CAAC,cAAc,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC,EAAE,CAAC;YAC1F,MAAM,IAAI,KAAK,CAAC,2CAA2C,UAAU,EAAE,CAAC,CAAA;QAC1E,CAAC;QACD,IAAI,CAAC,UAAU,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAA;QACxF;6CACqC;QACrC,OAAO,IAAA,6BAAc,EAAC;YACpB,QAAQ,EAAE,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE;YACjC,UAAU;YACV,OAAO;YACP,QAAQ;YACR,MAAM;YACN,aAAa;YACb,OAAO;YACP,eAAe;YACf,OAAO,EAAE,OAAc;YACvB,UAAU;YACV,SAAS,EAAE,SAAgB;YAC3B,GAAG,CAAC,eAAe,IAAI,eAAe,GAAG,CAAC;gBACxC,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,EAAG,cAAsB,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE;gBACzG,CAAC,CAAC,EAAE,CAAC;SACR,CAAC,CAAA;IACJ,CAAC;IAMK,AAAN,KAAK,CAAC,UAAU,CACK,UAAkB,EACkB,aAAqB,EACtC,SAAiB,EAChD,OAAwB,EAOoB,IAAe,EAOV,cAAuB;QAE/E,MAAM,KAAK,GAAG,MAAM,qBAAU,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,UAAU,EAAE,aAAa,IAAI,SAAS,EAAE,SAAS,IAAI,SAAS,CAAC,CAAA;QAC/H,MAAM,MAAM,GAAG,IAAA,wBAAQ,EAAC,KAAK,EAAE,IAAI,CAAC,CAAA;QACpC,gEAAgE;QAChE,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM;YAAE,IAAA,iBAAQ,EAAC,gDAAgD,UAAU,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAChI,IAAI,CAAC,cAAc,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,UAAU;YAAE,OAAO,MAAM,CAAC,KAAK,CAAA;QACrE,MAAM,CAAC,GAAG,IAAA,sCAAgB,EAAC,MAAM,CAAC,KAAK,CAAC,UAAU,EAAE,cAAc,CAAC,CAAA;QACnE,oDAAoD;QACpD,OAAO,EAAE,GAAG,MAAM,CAAC,KAAK,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE,cAAc,EAAE,CAAC,CAAC,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC,kBAAkB,EAAE,CAAA;IAClI,CAAC;IAOK,AAAN,KAAK,CAAC,YAAY,CACuB,UAAkB,EACrB,OAAe,EAEhB,WAAmB,EAKV,eAAuB,EAC5D,OAAwB;QAE/B,IAAI,UAAU,IAAI,CAAC,CAAC,MAAM,qBAAU,CAAC,cAAc,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC,EAAE,CAAC;YAC1F,MAAM,IAAI,KAAK,CAAC,2CAA2C,UAAU,EAAE,CAAC,CAAA;QAC1E,CAAC;QACD,IAAI,CAAC,UAAU,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAA;QACxF,IAAI,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAA;QAEhF;;;;WAIG;QACH,MAAM,IAAI,GAAG,eAAe,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAA;QACvE,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,4CAA4C,eAAe,EAAE,CAAC,CAAA;QACtG,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,CAAA;QACxB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;QAExG,OAAO,qBAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,WAAW,EAAE,MAAM,CAAC,CAAA;IACnG,CAAC;IAOK,AAAN,KAAK,CAAC,wBAAwB,CACW,UAAkB,EACrB,OAAe,EAChB,WAAmB,EAErC,QAAgB,EAClB,MAAc,EACe,eAAuB,EAC5D,OAAwB;QAE/B,IAAI,UAAU,IAAI,CAAC,CAAC,MAAM,qBAAU,CAAC,cAAc,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC,EAAE,CAAC;YAC1F,MAAM,IAAI,KAAK,CAAC,2CAA2C,UAAU,EAAE,CAAC,CAAA;QAC1E,CAAC;QACD,IAAI,CAAC,UAAU,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAA;QACxF,IAAI,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAA;QAChF,iEAAiE;QACjE,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;QACjC,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;QAC7B,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,mCAAmC,QAAQ,MAAM,MAAM,EAAE,CAAC,CAAA;QACtH,IAAI,EAAE,IAAI,IAAI;YAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,QAAQ,MAAM,MAAM,EAAE,CAAC,CAAA;QAE9E,4DAA4D;QAC5D,MAAM,IAAI,GAAG,eAAe,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;QACjE,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,4CAA4C,eAAe,EAAE,CAAC,CAAA;QACtG,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,CAAA;QACxB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;QAExG,OAAO,qBAAU,CAAC,oBAAoB,CACpC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EACvB,EAAE,UAAU,EAAE,OAAO,EAAE,EACvB,WAAW,EACX,MAAM,EACN,EAAE,QAAQ,EAAE,MAAM,EAAE,CACrB,CAAA;IACH,CAAC;IAMK,AAAN,KAAK,CAAC,cAAc,CAAoB,UAAkB,EAAS,OAAwB;QACzF,yDAAyD;QACzD,IAAI,CAAC,CAAC,MAAM,qBAAU,CAAC,cAAc,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC,EAAE,CAAC;YAC5E,MAAM,IAAI,KAAK,CAAC,2CAA2C,UAAU,EAAE,CAAC,CAAA;QAC1E,CAAC;QACD,OAAO,qBAAU,CAAC,gBAAgB,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,UAAU,CAAC,CAAA;IACzE,CAAC;IAKK,AAAN,KAAK,CAAC,aAAa,CACD,OAAe,EACxB,OAAwB;QAE/B,OAAO,qBAAU,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,OAAO,CAAC,CAAA;IAC/D,CAAC;CACF,CAAA;AAviBY,4CAAgB;AAMrB;IALL,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,CAAC,yBAAS,CAAC,EAAE;QAC7B,WAAW,EACT,kGAAkG;YAClG,0DAA0D;KAC7D,CAAC;IAEC,mBAAA,IAAA,kBAAG,EAAC,YAAY,CAAC,CAAA;IACjB,mBAAA,IAAA,kBAAG,EAAC,cAAc,EAAE,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IACpD,mBAAA,IAAA,kBAAG,EAAC,YAAY,EAAE,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IAClD,mBAAA,IAAA,kBAAG,EAAC,WAAW,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IACpC,mBAAA,IAAA,kBAAG,EAAC,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IAC7C,mBAAA,IAAA,kBAAG,GAAE,CAAA;IACL,mBAAA,IAAA,kBAAG,EAAC,WAAW,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IACpC,mBAAA,IAAA,kBAAG,EAAC,UAAU,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;;;;kDAoHrC;AA0BK;IARL,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,kCAAa,EAAE;QAC/B,WAAW,EACT,uHAAuH;YACvH,+LAA+L;YAC/L,2IAA2I;YAC3I,qHAAqH;YACrH,uGAAuG;KAC1G,CAAC;IAEC,mBAAA,IAAA,mBAAI,EAAC,IAAI,CAAC,EAAE,CAAC,iBAAS,CAAC,CAAA;IACvB,mBAAA,IAAA,kBAAG,GAAE,CAAA;IACL,mBAAA,IAAA,kBAAG,EAAC,YAAY,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,iCAAiC,EAAE,CAAC,CAAA;IACrF,mBAAA,IAAA,kBAAG,EAAC,SAAS,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,+FAA+F,EAAE,CAAC,CAAA;;6CAHhH,iBAAS;;qDAoM3C;AAeK;IAJL,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,oBAAY,EAAE;QAC9B,WAAW,EACT,ulIAAulI;KAC1lI,CAAC;IAEC,mBAAA,IAAA,kBAAG,GAAE,CAAA;IAEL,mBAAA,IAAA,kBAAG,EAAC,SAAS,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IAClC,mBAAA,IAAA,kBAAG,EAAC,YAAY,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IAErC,mBAAA,IAAA,kBAAG,EAAC,UAAU,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IACnC,mBAAA,IAAA,kBAAG,EAAC,QAAQ,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IAEjC,mBAAA,IAAA,kBAAG,EAAC,eAAe,EAAE,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IAErD,mBAAA,IAAA,kBAAG,EAAC,SAAS,EAAE,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IAE/C,mBAAA,IAAA,kBAAG,EAAC,iBAAiB,EAAE,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IAEvD,mBAAA,IAAA,kBAAG,EAAC,SAAS,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IAElC,mBAAA,IAAA,kBAAG,EAAC,YAAY,EAAE,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IAElD,oBAAA,IAAA,kBAAG,EAAC,WAAW,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IAEpC,oBAAA,IAAA,kBAAG,EAAC,iBAAiB,EAAE,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IAEvD,oBAAA,IAAA,kBAAG,EAAC,gBAAgB,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;;;;+CA0B3C;AAMK;IAJL,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,oBAAY,EAAE;QAC9B,QAAQ,EAAE,IAAI;QACd,WAAW,EAAE,oMAAoM;KAClN,CAAC;IAEC,mBAAA,IAAA,kBAAG,EAAC,YAAY,CAAC,CAAA;IACjB,mBAAA,IAAA,kBAAG,EAAC,eAAe,EAAE,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IACrD,mBAAA,IAAA,kBAAG,EAAC,WAAW,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IACpC,mBAAA,IAAA,kBAAG,GAAE,CAAA;IAOL,mBAAA,IAAA,kBAAG,EAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IAOjD,mBAAA,IAAA,kBAAG,EAAC,gBAAgB,EAAE,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;;;;kDAUxD;AAOK;IALL,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,oBAAY,EAAE;QAC9B,QAAQ,EAAE,IAAI;QACd,WAAW,EACT,uTAAuT;KAC1T,CAAC;IAEC,mBAAA,IAAA,kBAAG,EAAC,YAAY,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IACrC,mBAAA,IAAA,kBAAG,EAAC,SAAS,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IAElC,mBAAA,IAAA,kBAAG,EAAC,aAAa,EAAE,IAAI,CAAC,EAAE,CAAC,oBAAK,CAAC,CAAA;IAKjC,mBAAA,IAAA,kBAAG,EAAC,iBAAiB,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IAC1C,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;oDAmBP;AAOK;IALL,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,oBAAY,EAAE;QAC9B,QAAQ,EAAE,IAAI;QACd,WAAW,EACT,8ZAA8Z;KACja,CAAC;IAEC,mBAAA,IAAA,kBAAG,EAAC,YAAY,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IACrC,mBAAA,IAAA,kBAAG,EAAC,SAAS,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IAClC,mBAAA,IAAA,kBAAG,EAAC,aAAa,EAAE,IAAI,CAAC,EAAE,CAAC,oBAAK,CAAC,CAAA;IAEjC,mBAAA,IAAA,kBAAG,EAAC,UAAU,CAAC,CAAA;IACf,mBAAA,IAAA,kBAAG,EAAC,QAAQ,CAAC,CAAA;IACb,mBAAA,IAAA,kBAAG,EAAC,iBAAiB,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IAC1C,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;gEA0BP;AAMK;IAJL,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,oBAAY,EAAE;QAC9B,WAAW,EACT,mUAAmU;KACtU,CAAC;IACoB,mBAAA,IAAA,kBAAG,EAAC,YAAY,CAAC,CAAA;IAAsB,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;sDAMjE;AAKK;IAHL,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,oBAAY,EAAE;QAC9B,WAAW,EAAE,yGAAyG;KACvH,CAAC;IAEC,mBAAA,IAAA,kBAAG,EAAC,SAAS,CAAC,CAAA;IACd,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;qDAGP;2BAtiBU,gBAAgB;IAD5B,IAAA,uBAAQ,GAAE;GACE,gBAAgB,CAuiB5B","sourcesContent":["import { twinWarn } from '../../engine/log.js'\nimport { 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 { JOURNAL_COUNT_CAP } from '../twin-event/journal-count.js'\nimport { TwinEventList } from '../twin-event/twin-event-type.js'\nimport { TwinEngine } from '../../engine/index.js'\nimport { pickAxes } from '../../engine/state-axes.js'\nimport { digestAttentions } from '../../engine/attention-digest.js'\nimport { computeTwinKpi, resolveTwinTargets } from '../../engine/kpi-query.js'\nimport { journalOrder } from './journal-order.js'\nimport { readModelPage } from '../twin-subject/twin-subject-fast-path.js'\nimport { eventTypeSplitPage } from './event-type-split.js'\nimport { splitShape } from './event-type-split-shape.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/*\n * 건수 상한은 **저널 세기의 문**에서 온다(`journal-count.ts`) — 값과 규율이 두 곳에 흩어지면 한쪽만\n * 고쳐진다. 근거(2026-08-22 실측): 상한 1만이면 0.04초, 상한 없이 세면 5.32초(저널 358만 행).\n */\nconst COUNT_CAP = JOURNAL_COUNT_CAP\n\n/**\n * 시각 필터 값을 날짜로 — 배열(between)이면 원소마다.\n *\n * 못 읽는 값은 **그대로 둔다.** 여기서 지어내면 사용자가 요청하지 않은 구간을 조회하게 된다.\n */\nfunction toDateValue(v: any): any {\n const one = (x: any) => {\n if (x instanceof Date) return x\n const t = typeof x === 'string' ? Date.parse(x) : NaN\n return Number.isNaN(t) ? x : new Date(t)\n }\n return Array.isArray(v) ? v.map(one) : one(v)\n}\n\n@Resolver()\nexport class TwinJournalQuery {\n @Query(returns => [TwinEvent], {\n description:\n 'Read the append-only twin event journal (Ledger / History / audit). Newest first by event time; ' +\n 'ordered by revision only when a revision range is given.'\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 @Arg('fromTime', { nullable: true }) fromTime?: string\n ): Promise<TwinEvent[]> {\n const where: any = { domain: { id: context.state.domain.id }, instanceId }\n if (eventType) where.eventType = eventType\n /*\n * ── 구간으로 물을 수 있게 한다 (2026-08-25 실측) ────────────────────────────\n *\n * `untilTime` 만 있으면 **끝만 정해진 조회**다. 그러면 위쪽 경계에서 뒤로 걸어오며 종류를 하나씩\n * 확인해야 하고, 찾는 종류가 며칠 전에 멈춘 트윈에서는 그 사이 행을 전부 열어 본다. 실측:\n *\n * 공정 타임라인이 부르는 방식(종류만·200건) 22.0초\n * 같은 조회에 `untilTime` 만 더한 것 9.7초\n *\n * 이유는 계획이 말한다. 종류별 시각순 인덱스가 없어서 `(domain, instanceId, eventTime)` 로 걷고\n * 행마다 테이블을 열어 종류를 본다. 승화푸드에서 작업 사실은 8월 22일에 멈춰 있고 그 뒤로 리비전이\n * 110만 늘었으므로, 200건에 닿기까지 그만큼을 읽고 버린다.\n *\n * **구간을 주면 그 범위만 걷는다**(`event_time>? AND event_time<?`). 화면은 자기가 그리는 구간을\n * 알고 있으므로 그것을 그대로 물으면 된다 — 「전체」가 실제로는 상한에 걸린 6.7시간이었던 문제도\n * 같은 자리에서 없어진다.\n *\n * 컬럼이 날짜라 **날짜로 넘긴다**(문자열로 비교하면 sqlite 에서 오류 없이 빈 결과가 나온다).\n */\n if (untilTime != null && fromTime != null) where.eventTime = Between(new Date(fromTime), new Date(untilTime))\n else if (untilTime != null) where.eventTime = LessThanOrEqual(new Date(untilTime))\n else if (fromTime != null) where.eventTime = MoreThanOrEqual(new Date(fromTime))\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 /*\n * 최신순(DESC) 기본 — Ledger/recent 뷰. 범위 필터로 시간여행 구간 조회도 가능.\n *\n * ── 정렬 축을 **거르는 축과 맞춘다** (2026-08-22 실측) ─────────────────────\n * `untilTime` 경로는 `eventTime <= T` 로 거르면서 `revision` 으로 정렬했다. 그러면 sqlite 는 조건에\n * 맞는 행을 **전부 찾아 전부 정렬한 뒤** 상한을 적용한다 — 상한이 정렬 뒤에 걸리므로 200건을 얻는\n * 데도 전량 정렬을 치른다. 실측(인스턴스 하나가 375만 행):\n *\n * ORDER BY revision DESC → 33.4초\n * ORDER BY eventTime DESC, revision DESC → 0.007초 (4,700배)\n *\n * 계획이 그것을 그대로 말한다: 앞은 `USE TEMP B-TREE FOR ORDER BY`(전체 정렬), 뒤는\n * `... FOR LAST TERM OF ORDER BY` — 시각은 인덱스(`ix_twin_event_1`)에서 순서대로 나오고\n * **동시각만** 임시 정렬한다.\n *\n * **`revision` 경로는 그대로 둔다.** 그때는 거르는 축이 `revision` 이고 정렬도 `revision` 이라 이미\n * 맞아 있다(`ix_twin_event_0`). 축이 어긋난 경로만 고친다 — 맞는 자리를 건드리면 그쪽이 어긋난다.\n *\n * 순서의 뜻이 **완전히 같지는 않다**(측정해서 고친 문장): `revision` 순서와 `eventTime` 순서가\n * 어긋나는 자리가 실재한다(426만 행 중 7건 — 미러는 원본의 시각을 싣고 도착 순서로 리비전을 받는다).\n * 이 목록은 사람이 이력을 훑는 화면이고 **시각순으로 보는 것이 그 화면의 뜻**이므로, 그 어긋남은\n * 결함이 아니라 오히려 맞는 순서다. 동시각의 순서를 위해 `revision` 을 둘째 축으로 남긴다.\n */\n /*\n * ── 아무 필터도 없을 때도 시각순으로 낸다 (2026-08-25 실측) ────────────────\n * 이 갈래가 `revision DESC` 였다. 공정 타임라인은 필터 없이 상한만 걸고 부르므로\n * (`loadTwinEvents(id, { eventType: 'task.status', limit })`) 그 갈래로 떨어졌다.\n *\n * `revision` 은 **적힌 순서**다. 연결된 시스템을 되읽는 트윈에서는 그 순서가 일어난 순서와 다르다 —\n * 커넥터가 원본의 갱신 순서를 걷기 때문에, 원본에서 나중에 손댄 행이 4월 사실을 담고 있으면 그\n * 행이 큰 revision 을 받는다. 승화푸드 실측:\n *\n * 작업 축에 있는 가장 늦은 사실 2026-08-23 03:20\n * revision 이 가장 큰 작업 사실 2026-04-15 10:49 ← 상한이 잡던 값\n *\n * 상한 안에 8월 두 달치가 들어오지 않았고, 화면은 4월 사실을 최근 것으로 그렸다.\n *\n * 아래 목록 계약(`twinEventList`)은 이미 시각순을 기본으로 골라 두었다(§`sortings`). 이 함수만\n * 남아 있었다. 성능도 이유가 되지 않는다 — 시각 정렬이 인덱스를 순서대로 타는 것은 아래 실측이\n * 말한다.\n *\n * `revision` 범위로 거를 때는 `revision DESC` 를 그대로 쓴다. 거르는 축과 정렬 축을 맞추는 규칙이고,\n * 그때는 부르는 쪽이 리비전으로 말하고 있으므로 리비전 순서가 그 물음의 답이다.\n */\n const order: any = journalOrder({ untilTime, fromTime, fromRevision, toRevision })\n\n /*\n * ── `find` 이 아니라 질의 빌더로 묻는다 (2026-08-25 실측) ───────────────────\n *\n * `find({ where: { domain: { id } }, order, take })` 는 관계를 가진 엔티티에서 **두 단계 질의**가\n * 된다. 실제로 나간 SQL 이 이렇다:\n *\n * SELECT DISTINCT \"distinctAlias\".\"TwinEvent_id\", …\n * FROM (SELECT … 모든 컬럼 … FROM twin_events WHERE …) \"distinctAlias\"\n * ORDER BY … LIMIT ?\n *\n * **안쪽에 상한이 없다.** 조건에 맞는 행을 전부 만들어 놓고 밖에서 자른다. 승화푸드의 작업 사실은\n * 438,913건이고 `payload` 가 함께 실리므로, 200건을 얻는 데 그 전부를 읽었다.\n *\n * 실측: 같은 조회를 손으로 쓰면 0.024초, 이 함수를 통하면 12초였다. 인덱스를 새로 얹은 뒤에도\n * 그대로였다 — 인덱스는 안쪽 질의를 도와줄 뿐이고, 문제는 안쪽이 전부를 만든다는 것이었다.\n *\n * 관계를 **컬럼으로** 물으면 감싸지 않는다. 이 파일의 다른 자리(`timeRange` · 최종 리비전)가 이미\n * 그 방식이다. `getMany` 는 조인이 없으면 `DISTINCT` 를 붙이지 않는다.\n */\n const qb = getRepository(TwinEvent)\n .createQueryBuilder('e')\n .where('e.domain = :domainId', { domainId: context.state.domain.id })\n .andWhere('e.instanceId = :instanceId', { instanceId })\n if (eventType) qb.andWhere('e.eventType = :eventType', { eventType })\n if (where.eventTime !== undefined) {\n /* 조건은 위에서 이미 정했다 — 같은 판단을 두 벌로 적지 않는다. */\n if (untilTime != null && fromTime != null) {\n qb.andWhere('e.eventTime BETWEEN :fromTime AND :untilTime', { fromTime: new Date(fromTime), untilTime: new Date(untilTime) })\n } else if (untilTime != null) {\n qb.andWhere('e.eventTime <= :untilTime', { untilTime: new Date(untilTime) })\n } else {\n qb.andWhere('e.eventTime >= :fromTime', { fromTime: new Date(fromTime as string) })\n }\n } else if (where.revision !== undefined) {\n if (fromRevision != null && toRevision != null) qb.andWhere('e.revision BETWEEN :from AND :to', { from: fromRevision, to: toRevision })\n else if (fromRevision != null) qb.andWhere('e.revision >= :from', { from: fromRevision })\n else qb.andWhere('e.revision <= :to', { to: toRevision })\n }\n for (const [column, direction] of Object.entries(order)) qb.addOrderBy(`e.${column}`, direction as 'DESC')\n return qb.limit(limit ?? 200).getMany()\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 /*\n * ── 이어 읽기 지점을 **잃지 않는다** (2026-08-28 실측) ─────────────────────\n *\n * 여기서 페이지 값을 다시 만들면서 `after` 를 빼고 있었다. 그래서 응답은 다음 지점(`nextCursor`)을\n * 내주는데 다음 요청의 그 값은 아무 데도 쓰이지 않았고, 「더 보기」가 **1페이지를 다시 냈다.**\n * 실측: 같은 품목의 30건을 세 번 이어 읽었더니 90건 중 60건이 되풀었다(같은 id).\n *\n * 오류가 나지 않는 결함이라 화면에서는 「이력이 30건뿐」이나 「같은 사건이 계속 나온다」로 보인다.\n */\n pagination: { page: params.pagination?.page ?? 1, limit, after: params.pagination?.after },\n /* 기본 정렬은 시각 내림차순 — 여러 트윈을 합칠 때 공통 축은 revision(트윈별 카운터)이 아니라\n * **시각**이다. revision 으로 섞으면 서로 다른 트윈의 무관한 카운터가 뒤엉킨다. */\n sortings: params.sortings?.length ? params.sortings : [{ name: 'eventTime', desc: true }],\n /*\n * 시각 필터는 **날짜로 바꿔서** 질의 빌더에 넘긴다.\n *\n * 화면은 ISO 문자열(`2026-08-07T11:00:00.000Z`)을 보내는데 eventTime 은 날짜 컬럼이고,\n * sqlite 는 그것을 공백 구분 형식(`2026-08-07 11:00:00.000`)으로 저장한다. 문자열 그대로\n * 비교하면 사전식으로 어긋나 **오류 없이 빈 결과**가 나온다(실측으로 확인했다).\n * 화면이 날짜를 어떻게 적어 보내든 서버가 받아 주는 편이 맞다 — 계약을 좁히는 대신 경계에서 옮긴다.\n */\n filters: (params.filters ?? []).map(f => (f.name === 'eventTime' ? { ...f, value: toDateValue(f.value) } : f))\n }\n\n /*\n * ── 대상 하나의 이력은 **파생 테이블**이 답한다 (2026-08-28 실측) ───────────\n *\n * 360 화면의 이력이 이 문으로 온다. 물음은 「이 품목(오더·자리·설비)의 사건을 최신순으로 30건」\n * 인데, 저널에는 그 모양의 목차가 없다: 축으로 걸러도 시각으로 정렬하는 순간 목차를 못 쓰고,\n * 품목과 로트가 한 문자열에 붙어 있어 품목으로는 아예 찾지 못한다. 실측 **63.998초**였다.\n *\n * 그 물음의 모양대로 만든 테이블이 이미 있다(`twin_subject_events`). 같은 질의가 **0.078초**다.\n * 화면을 고치지 않고 여기서 답을 가져오는 곳만 바꾼다 — 계약도 커서도 그대로다.\n *\n * 맞지 않는 물음(여러 트윈·자유 검색·종류 필터·다른 정렬)이거나 파생 테이블이 그 구간을 말할 수\n * 없으면 `null` 이 오고, 그때는 아래 저널 경로로 간다. 느리지만 온전하다.\n */\n if (instanceIds.length === 1) {\n const fast = await readModelPage(domain.id, instanceIds[0], effective, limit)\n if (fast) return fast\n }\n\n /*\n * ── **여러 종류를 시각순으로 물으면 나눠 묻는다** (2026-08-28 실측) ──────────\n *\n * 에너지 원장이 「이 여덟 종류를 시각 내림차순으로」를 묻는다. 그 모양에서 sqlite 는 종류 색인을\n * 버리고 시각 색인으로 337만 행을 걸으며 행마다 종류를 확인한다 — **37.181초, 결과 0건**.\n * 종류를 정확일치로 하나씩 물으면 색인이 시각 순서까지 주므로 **각 0.003초**다.\n *\n * 새 색인도 새 테이블도 필요 없다. 있는 색인(`ix_twin_event_7`)을 쓰게 하는 것이 전부다.\n * 건수 세기는 이미 빠르다(정렬을 지우므로 같은 색인을 탄다) — 목록만 이 갈래를 탄다.\n */\n const tiebreak: 'revision' | 'id' = instanceIds.length === 1 ? 'revision' : 'id'\n const split = splitShape(effective, effective.sortings ?? [])\n if (split) {\n const page = await eventTypeSplitPage(domain.id, instanceIds, split, limit, tiebreak, effective.pagination?.after)\n if (page) {\n /*\n * 건수는 일반 경로와 같은 방식으로 센다 — 상한까지 키만 읽는다. 이 조건은 정렬이 없어 종류\n * 색인을 그대로 타므로 값이 싸다(실측 0.003초).\n */\n const probe = getQueryBuilderFromListParams({\n repository: getRepository(TwinEvent),\n params: { ...effective, pagination: { page: 1, limit: COUNT_CAP + 1 } },\n domain,\n searchables: [],\n sortables: ['eventTime', 'revision', 'eventType', 'bizStep'],\n defaultLimit: COUNT_CAP + 1,\n maxLimit: COUNT_CAP + 1\n })\n probe.andWhere(`${probe.alias}.instanceId IN (:...instanceIds)`, { instanceIds })\n const counted = await probe\n .orderBy()\n .select(`${probe.alias}.id`)\n .getRawMany()\n .catch(() => null)\n const capped = !counted || counted.length > COUNT_CAP\n return {\n items: page.items,\n total: capped ? undefined : counted!.length,\n capped,\n countCap: COUNT_CAP,\n nextCursor: page.nextCursor,\n oldestRevision: undefined\n }\n }\n }\n\n const qb = getQueryBuilderFromListParams({\n repository: getRepository(TwinEvent),\n params: effective,\n domain,\n /* 전부 인덱스 가능한 승격 컬럼 — 자유 검색어는 서버가 이 축들로 펼친다. */\n searchables: ['epc', 'orderId', 'locationId', 'moverId', 'bizStep', 'eventType', 'correlationId'],\n /* 정렬은 인덱스가 받쳐 주는 축으로만 — 큰 저널에서 임의 컬럼 정렬은 전체 정렬 스캔이다. */\n sortables: ['eventTime', 'revision', 'eventType', 'bizStep'],\n defaultLimit: 100,\n maxLimit: 500,\n /*\n * ── 동시각을 가리는 축을 **두 경로가 같게** 쓴다 (2026-08-28 실측) ─────────\n *\n * 커서는 「이 행 다음부터」이므로 시각이 같은 행들 사이에서 무엇이 앞인지 정해야 한다. 기본값은\n * 기본키(`id`)인데, 파생 테이블 경로는 `revision` 으로 가린다(그 테이블에 `id` 가 없다).\n *\n * 두 경로가 한 목록을 나눠 답하면 그 축이 달라지고, 시각이 같은 행이 많은 자리에서 **본 행이\n * 또 나온다.** 실측: 재고 사건 2,665건이 같은 시각을 가진 트윈에서 30건씩 네 번 이어 읽어\n * 120건 중 15건이 되풀었다.\n *\n * 그래서 트윈 하나를 볼 때는 `revision` 으로 가린다 — 그 트윈 안에서 유일하고 빈 값이 없다.\n * 여러 트윈을 합쳐 보는 목록에서는 리비전이 트윈마다 따로 세는 번호라 유일하지 않으므로\n * 기본키를 쓴다.\n */\n cursorTiebreak: tiebreak\n })\n /* 대상 범위는 호출자가 필터로 빼먹을 수 있는 값이 아니다 — 계약상 필수라 여기서 강제한다. */\n qb.andWhere(`${qb.alias}.instanceId IN (:...instanceIds)`, { instanceIds })\n\n /*\n * ── 건수는 **상한까지만** 센다 (2026-08-22 실측) ──────────────────────────\n * 예전에는 `getManyAndCount()` 였다. 그 두 번째 질의가 조건에 맞는 행을 **전부** 세는데, 실측한\n * 개발 저널에서 트윈 하나가 358만 행이라 **한 번에 5.32초**였다. 이력 화면을 열 때마다 그것을\n * 물었고, 그 5초 동안 이벤트 루프가 다른 요청을 처리하지 못했다.\n *\n * 상한을 두면 같은 답이 **0.04초**다(130배). 상한을 넘으면 정확한 수를 모르는 것이 사실이므로\n * 목록 규약의 「서버 상한에 부딪힘(total 모름)」 갈래로 낸다 — 화면은 「처음 N 건만」이라고 말하고\n * 좁히기를 권한다. **알리지 않고 자르지 않는다**: 상한을 넘었다는 사실을 `capped` 로 함께 낸다.\n *\n * 왜 하위 질의로 세지 않나: 5개 드라이버에서 같이 도는 형태여야 한다(이 저장소의 절대 규칙).\n * 그래서 **키 컬럼만 상한까지 읽어 세는** 방식을 쓴다 — 색인만 읽으므로 행 본문을 만들지 않는다.\n */\n const items = await qb.getMany()\n const probe = qb\n .clone()\n .orderBy() /* 세는 데 순서는 필요 없다 — 정렬을 남기면 임의 축에서 전체 정렬이 된다. */\n .skip(0)\n .take(COUNT_CAP + 1)\n .select(`${qb.alias}.id`)\n const counted = await probe.getRawMany()\n const capped = counted.length > COUNT_CAP\n /* 상한을 넘었으면 **전체 수를 모른다** — 상한값을 전체로 적으면 화면이 거짓을 말한다. */\n const total = capped ? undefined : counted.length\n\n /* 다음 커서는 **이번 페이지의 마지막 행**에서 만든다. 페이지가 상한보다 짧으면 뒤가 없다. */\n const axes = cursorSortings(effective.sortings, tiebreak)\n const nextCursor = items.length === limit ? buildNextCursor(items[items.length - 1], axes) : undefined\n\n /*\n * ── **앞부분이 지워졌는지 함께 낸다** (2026-08-27) ─────────────────────────\n *\n * 보관 기간 정리가 오래된 사건을 지운다(기본 7일). 그러면 이 목록을 아무리 내려가도 어느 지점에서\n * 기록이 끝나는데, 화면이 그것을 말하지 않으면 사람은 「이 트윈은 그때 시작했다」로 읽는다.\n * 없었던 것과 지워진 것은 다른 사실이다.\n *\n * 저장하지 않고 그때그때 구한다 — 저장한 값은 실제 데이터와 어긋날 수 있고, 어긋나면 어느 쪽이\n * 사실인지 알 수 없다. 색인 `(domain, instanceId, revision)` 의 첫 행 하나라서 값이 싸다.\n *\n * 트윈 하나를 물었을 때만 낸다. 여러 트윈을 합쳐 보는 목록에서는 리비전이 트윈마다 따로 세는\n * 번호라 하나의 답이 없다.\n */\n let oldestRevision: number | undefined\n if (instanceIds.length === 1) {\n const first = await getRepository(TwinEvent)\n .createQueryBuilder('e')\n .select('e.revision', 'revision')\n .where('e.domain = :domainId', { domainId: domain.id })\n .andWhere('e.instanceId = :instanceId', { instanceId: instanceIds[0] })\n .orderBy('e.revision', 'ASC')\n .limit(1)\n .getRawOne<{ revision: unknown }>()\n .catch(() => null)\n const at = Number(first?.revision)\n if (Number.isFinite(at) && at > 0) oldestRevision = at\n }\n\n return { items, total, capped, countCap: COUNT_CAP, nextCursor, oldestRevision }\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 model 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. Pass baselinePeriods (1-14, with baselineStride day | week) to fold the same time-of-day window on each of the last N days (or the same weekday on each of the last N weeks) and get `baseline.metrics` — per metric the distribution of those windows (min / median / max) plus where the current window sits: `rank` (1 = smallest, direction-free) and `standing` (best | better-than-typical | typical | worse-than-typical | worst, which does apply the metric direction). This answers what a single comparison cannot: comparing to yesterday alone is useless if yesterday was also bad. Every requested window is returned in `samples` with `measured` true or false — windows without records are kept and counted (measuredCount vs requested) rather than dropped, so \"the last 7 days\" never silently means 3. No position is reported when the current window has no value or fewer than two baseline windows were measured; `noPosition` says which, because a missing distribution drawn as neutral would read as \"typical\". The baseline is produced even when the current window itself has no records, since that is exactly when knowing the usual level matters (a stopped line).'\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 /** 여러 구간 동시 비교 — 최근 N 구간의 같은 시간대 분포(1~14). */\n @Arg('baselinePeriods', type => Int, { nullable: true }) baselinePeriods?: number,\n /** 분포의 간격 — day(같은 시간대) | week(같은 요일·시간대). 기본 day. */\n @Arg('baselineStride', { nullable: true }) baselineStride?: 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 ...(baselinePeriods && baselinePeriods > 0\n ? { baseline: { periods: baselinePeriods, stride: (baselineStride as any) === 'week' ? 'week' : 'day' } }\n : {})\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 /*\n * 필요한 축만 — 지도는 `locations`·`attentions` 만 쓰는데 상태를 통째로 받고 있었다(실 저널로\n * 재 보니 트윈 하나가 최대 710KB, 같은 공간에 셋이면 약 2MB). 화면 전용 질의를 만들지 않고\n * 호출부가 축을 말한다(무방언 — 어휘가 화면 이름이 아니라 상태의 축이다).\n * 말하지 않으면 자르지 않는다(기존 호출부 무영향).\n */\n @Arg('axes', type => [String], { nullable: true }) axes?: string[],\n /*\n * 신호는 몇 건만 필요한가 — 레일은 20건을 그리고 나머지는 \"N건 더\" 라고만 말한다. 그런데 신호가\n * 많은 트윈은 그 전량이 108KB 다(`hatio-yard` 397건). 상위 N 만 보내고 **전체 개수와 자리별\n * 최고 심각도는 전량을 근거로** 함께 보낸다 — 레일은 잘라도 되지만 지도 색은 잘라선 안 된다.\n * 말하지 않으면 자르지 않는다.\n */\n @Arg('attentionLimit', type => Int, { nullable: true }) attentionLimit?: number\n ): Promise<any> {\n const state = await TwinEngine.recover(context.state.domain.id, instanceId, untilRevision ?? undefined, untilTime ?? undefined)\n const picked = pickAxes(state, axes)\n /* 오타를 알리지 않고 넘기지 않는다 — 화면이 빈 채로 뜨는 이유를 아무도 모르게 되는 것이 가장 나쁘다. */\n if (picked.unknown.length) twinWarn(`[twin] twinReplay: unknown axes ignored for \"${instanceId}\": ${picked.unknown.join(', ')}`)\n if (!attentionLimit || !picked.state?.attentions) return picked.state\n const d = digestAttentions(picked.state.attentions, attentionLimit)\n /* 잘랐다는 사실을 값으로 남긴다 — 개수만 줄어들면 화면은 \"괜찮아졌다\" 로 읽는다. */\n return { ...picked.state, attentions: d.attentions, attentionTotal: d.attentionTotal, severityByLocation: d.severityByLocation }\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 nullable: true,\n description:\n 'Operations capability of a twin for a time window (ISA-95 Part 4 OperationsCapability): per operation the ceiling for that window split into Committed (already promised), Unattainable (lost to resources that cannot be used — down, held, off-shift, expired qualification) and Available (what is left). Answers \"can we take more work?\" — which the steady-state rate alone cannot. Null when the twin is not running.'\n })\n async twinOperationsCapability(\n @Arg('instanceId', { nullable: true }) instanceId: string,\n @Arg('spaceId', { nullable: true }) spaceId: string,\n @Arg('unitsPerDay', type => Float) unitsPerDay: number,\n /** 능력을 물을 구간 — **구간 없이 능력은 존재하지 않는다**(표준 StartTime/EndTime). */\n @Arg('fromTime') fromTime: string,\n @Arg('toTime') toTime: string,\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 const from = Date.parse(fromTime)\n const to = Date.parse(toTime)\n if (Number.isNaN(from) || Number.isNaN(to)) throw new Error(`window is not a valid ISO time: ${fromTime} ~ ${toTime}`)\n if (to <= from) throw new Error(`window is inverted: ${fromTime} ~ ${toTime}`)\n\n /* 기준 주 규율은 `twinCapacity` 와 같다(같은 계산을 쓰므로 같은 주를 골라야 한다). */\n const base = sampleWeekStart ? Date.parse(sampleWeekStart) : from\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.operationsCapability(\n context.state.domain.id,\n { instanceId, spaceId },\n unitsPerDay,\n monday,\n { fromTime, toTime }\n )\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 model 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"]}
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 { twinWarn } from '../../engine/log.js'\nimport { 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 { JOURNAL_COUNT_CAP } from '../twin-event/journal-count.js'\nimport { TwinEventList } from '../twin-event/twin-event-type.js'\nimport { TwinEngine } from '../../engine/index.js'\nimport { pickAxes } from '../../engine/state-axes.js'\nimport { digestAttentions } from '../../engine/attention-digest.js'\nimport { computeTwinKpi, resolveTwinTargets } from '../../engine/kpi-query.js'\nimport { journalOrder } from './journal-order.js'\nimport { readModelPage } from '../twin-subject/twin-subject-fast-path.js'\nimport { eventTypeSplitPage } from './event-type-split.js'\nimport { splitShape } from './event-type-split-shape.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/*\n * 건수 상한은 **저널 세기의 문**에서 온다(`journal-count.ts`) — 값과 규율이 두 곳에 흩어지면 한쪽만\n * 고쳐진다. 근거(2026-08-22 실측): 상한 1만이면 0.04초, 상한 없이 세면 5.32초(저널 358만 행).\n */\nconst COUNT_CAP = JOURNAL_COUNT_CAP\n\n/**\n * 시각 필터 값을 날짜로 — 배열(between)이면 원소마다.\n *\n * 못 읽는 값은 **그대로 둔다.** 여기서 지어내면 사용자가 요청하지 않은 구간을 조회하게 된다.\n */\nfunction toDateValue(v: any): any {\n const one = (x: any) => {\n if (x instanceof Date) return x\n const t = typeof x === 'string' ? Date.parse(x) : NaN\n return Number.isNaN(t) ? x : new Date(t)\n }\n return Array.isArray(v) ? v.map(one) : one(v)\n}\n\n/**\n * 자유 검색어가 펼쳐지는 축 — **목록과 건수가 같은 것을 본다**(2026-09-19).\n *\n * 전부 인덱스 가능한 승격 컬럼이다(payload JSON 안이 아니라). 프레임워크 질의 빌더는 이 목록에 없는\n * 컬럼의 LIKE 를 경고 후 무시한다 — 인덱스 없는 전체 스캔을 막기 위해서다.\n *\n * ── 왜 상수인가 ────────────────────────────────────────────────────────────\n * 목록과 건수가 **다른 빌더**로 만들어진다(빠른 갈래의 건수 probe). 두 자리에 배열을 따로 적으면\n * 한쪽만 늘어나는 날 목록은 걸러지고 건수는 안 걸러진다 — 화면에 「한 건」과 「전체 수」가 나란히 선다.\n *\n * 지금은 그 일이 **일어날 수 없다**: 빠른 갈래는 검색어가 있으면 아예 안 탄다\n * (`event-type-split-shape.ts` — 시험 「자유 검색어」가 그 축을 붙든다). 그래도 같은 상수를 쓴다 —\n * 그 규칙이 언젠가 느슨해질 때 이 자리가 조용히 갈리지 않게.\n */\nexport const JOURNAL_SEARCHABLES = ['epc', 'orderId', 'locationId', 'moverId', 'bizStep', 'eventType', 'correlationId'] as const\n\n@Resolver()\nexport class TwinJournalQuery {\n @Query(returns => [TwinEvent], {\n description:\n 'Read the append-only twin event journal (Ledger / History / audit). Newest first by event time; ' +\n 'ordered by revision only when a revision range is given.'\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 @Arg('fromTime', { nullable: true }) fromTime?: string\n ): Promise<TwinEvent[]> {\n const where: any = { domain: { id: context.state.domain.id }, instanceId }\n if (eventType) where.eventType = eventType\n /*\n * ── 구간으로 물을 수 있게 한다 (2026-08-25 실측) ────────────────────────────\n *\n * `untilTime` 만 있으면 **끝만 정해진 조회**다. 그러면 위쪽 경계에서 뒤로 걸어오며 종류를 하나씩\n * 확인해야 하고, 찾는 종류가 며칠 전에 멈춘 트윈에서는 그 사이 행을 전부 열어 본다. 실측:\n *\n * 공정 타임라인이 부르는 방식(종류만·200건) 22.0초\n * 같은 조회에 `untilTime` 만 더한 것 9.7초\n *\n * 이유는 계획이 말한다. 종류별 시각순 인덱스가 없어서 `(domain, instanceId, eventTime)` 로 걷고\n * 행마다 테이블을 열어 종류를 본다. 승화푸드에서 작업 사실은 8월 22일에 멈춰 있고 그 뒤로 리비전이\n * 110만 늘었으므로, 200건에 닿기까지 그만큼을 읽고 버린다.\n *\n * **구간을 주면 그 범위만 걷는다**(`event_time>? AND event_time<?`). 화면은 자기가 그리는 구간을\n * 알고 있으므로 그것을 그대로 물으면 된다 — 「전체」가 실제로는 상한에 걸린 6.7시간이었던 문제도\n * 같은 자리에서 없어진다.\n *\n * 컬럼이 날짜라 **날짜로 넘긴다**(문자열로 비교하면 sqlite 에서 오류 없이 빈 결과가 나온다).\n */\n if (untilTime != null && fromTime != null) where.eventTime = Between(new Date(fromTime), new Date(untilTime))\n else if (untilTime != null) where.eventTime = LessThanOrEqual(new Date(untilTime))\n else if (fromTime != null) where.eventTime = MoreThanOrEqual(new Date(fromTime))\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 /*\n * 최신순(DESC) 기본 — Ledger/recent 뷰. 범위 필터로 시간여행 구간 조회도 가능.\n *\n * ── 정렬 축을 **거르는 축과 맞춘다** (2026-08-22 실측) ─────────────────────\n * `untilTime` 경로는 `eventTime <= T` 로 거르면서 `revision` 으로 정렬했다. 그러면 sqlite 는 조건에\n * 맞는 행을 **전부 찾아 전부 정렬한 뒤** 상한을 적용한다 — 상한이 정렬 뒤에 걸리므로 200건을 얻는\n * 데도 전량 정렬을 치른다. 실측(인스턴스 하나가 375만 행):\n *\n * ORDER BY revision DESC → 33.4초\n * ORDER BY eventTime DESC, revision DESC → 0.007초 (4,700배)\n *\n * 계획이 그것을 그대로 말한다: 앞은 `USE TEMP B-TREE FOR ORDER BY`(전체 정렬), 뒤는\n * `... FOR LAST TERM OF ORDER BY` — 시각은 인덱스(`ix_twin_event_1`)에서 순서대로 나오고\n * **동시각만** 임시 정렬한다.\n *\n * **`revision` 경로는 그대로 둔다.** 그때는 거르는 축이 `revision` 이고 정렬도 `revision` 이라 이미\n * 맞아 있다(`ix_twin_event_0`). 축이 어긋난 경로만 고친다 — 맞는 자리를 건드리면 그쪽이 어긋난다.\n *\n * 순서의 뜻이 **완전히 같지는 않다**(측정해서 고친 문장): `revision` 순서와 `eventTime` 순서가\n * 어긋나는 자리가 실재한다(426만 행 중 7건 — 미러는 원본의 시각을 싣고 도착 순서로 리비전을 받는다).\n * 이 목록은 사람이 이력을 훑는 화면이고 **시각순으로 보는 것이 그 화면의 뜻**이므로, 그 어긋남은\n * 결함이 아니라 오히려 맞는 순서다. 동시각의 순서를 위해 `revision` 을 둘째 축으로 남긴다.\n */\n /*\n * ── 아무 필터도 없을 때도 시각순으로 낸다 (2026-08-25 실측) ────────────────\n * 이 갈래가 `revision DESC` 였다. 공정 타임라인은 필터 없이 상한만 걸고 부르므로\n * (`loadTwinEvents(id, { eventType: 'task.status', limit })`) 그 갈래로 떨어졌다.\n *\n * `revision` 은 **적힌 순서**다. 연결된 시스템을 되읽는 트윈에서는 그 순서가 일어난 순서와 다르다 —\n * 커넥터가 원본의 갱신 순서를 걷기 때문에, 원본에서 나중에 손댄 행이 4월 사실을 담고 있으면 그\n * 행이 큰 revision 을 받는다. 승화푸드 실측:\n *\n * 작업 축에 있는 가장 늦은 사실 2026-08-23 03:20\n * revision 이 가장 큰 작업 사실 2026-04-15 10:49 ← 상한이 잡던 값\n *\n * 상한 안에 8월 두 달치가 들어오지 않았고, 화면은 4월 사실을 최근 것으로 그렸다.\n *\n * 아래 목록 계약(`twinEventList`)은 이미 시각순을 기본으로 골라 두었다(§`sortings`). 이 함수만\n * 남아 있었다. 성능도 이유가 되지 않는다 — 시각 정렬이 인덱스를 순서대로 타는 것은 아래 실측이\n * 말한다.\n *\n * `revision` 범위로 거를 때는 `revision DESC` 를 그대로 쓴다. 거르는 축과 정렬 축을 맞추는 규칙이고,\n * 그때는 부르는 쪽이 리비전으로 말하고 있으므로 리비전 순서가 그 물음의 답이다.\n */\n const order: any = journalOrder({ untilTime, fromTime, fromRevision, toRevision })\n\n /*\n * ── `find` 이 아니라 질의 빌더로 묻는다 (2026-08-25 실측) ───────────────────\n *\n * `find({ where: { domain: { id } }, order, take })` 는 관계를 가진 엔티티에서 **두 단계 질의**가\n * 된다. 실제로 나간 SQL 이 이렇다:\n *\n * SELECT DISTINCT \"distinctAlias\".\"TwinEvent_id\", …\n * FROM (SELECT … 모든 컬럼 … FROM twin_events WHERE …) \"distinctAlias\"\n * ORDER BY … LIMIT ?\n *\n * **안쪽에 상한이 없다.** 조건에 맞는 행을 전부 만들어 놓고 밖에서 자른다. 승화푸드의 작업 사실은\n * 438,913건이고 `payload` 가 함께 실리므로, 200건을 얻는 데 그 전부를 읽었다.\n *\n * 실측: 같은 조회를 손으로 쓰면 0.024초, 이 함수를 통하면 12초였다. 인덱스를 새로 얹은 뒤에도\n * 그대로였다 — 인덱스는 안쪽 질의를 도와줄 뿐이고, 문제는 안쪽이 전부를 만든다는 것이었다.\n *\n * 관계를 **컬럼으로** 물으면 감싸지 않는다. 이 파일의 다른 자리(`timeRange` · 최종 리비전)가 이미\n * 그 방식이다. `getMany` 는 조인이 없으면 `DISTINCT` 를 붙이지 않는다.\n */\n const qb = getRepository(TwinEvent)\n .createQueryBuilder('e')\n .where('e.domain = :domainId', { domainId: context.state.domain.id })\n .andWhere('e.instanceId = :instanceId', { instanceId })\n if (eventType) qb.andWhere('e.eventType = :eventType', { eventType })\n if (where.eventTime !== undefined) {\n /* 조건은 위에서 이미 정했다 — 같은 판단을 두 벌로 적지 않는다. */\n if (untilTime != null && fromTime != null) {\n qb.andWhere('e.eventTime BETWEEN :fromTime AND :untilTime', { fromTime: new Date(fromTime), untilTime: new Date(untilTime) })\n } else if (untilTime != null) {\n qb.andWhere('e.eventTime <= :untilTime', { untilTime: new Date(untilTime) })\n } else {\n qb.andWhere('e.eventTime >= :fromTime', { fromTime: new Date(fromTime as string) })\n }\n } else if (where.revision !== undefined) {\n if (fromRevision != null && toRevision != null) qb.andWhere('e.revision BETWEEN :from AND :to', { from: fromRevision, to: toRevision })\n else if (fromRevision != null) qb.andWhere('e.revision >= :from', { from: fromRevision })\n else qb.andWhere('e.revision <= :to', { to: toRevision })\n }\n for (const [column, direction] of Object.entries(order)) qb.addOrderBy(`e.${column}`, direction as 'DESC')\n return qb.limit(limit ?? 200).getMany()\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\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 /*\n * ── 이어 읽기 지점을 **잃지 않는다** (2026-08-28 실측) ─────────────────────\n *\n * 여기서 페이지 값을 다시 만들면서 `after` 를 빼고 있었다. 그래서 응답은 다음 지점(`nextCursor`)을\n * 내주는데 다음 요청의 그 값은 아무 데도 쓰이지 않았고, 「더 보기」가 **1페이지를 다시 냈다.**\n * 실측: 같은 품목의 30건을 세 번 이어 읽었더니 90건 중 60건이 되풀었다(같은 id).\n *\n * 오류가 나지 않는 결함이라 화면에서는 「이력이 30건뿐」이나 「같은 사건이 계속 나온다」로 보인다.\n */\n pagination: { page: params.pagination?.page ?? 1, limit, after: params.pagination?.after },\n /* 기본 정렬은 시각 내림차순 — 여러 트윈을 합칠 때 공통 축은 revision(트윈별 카운터)이 아니라\n * **시각**이다. revision 으로 섞으면 서로 다른 트윈의 무관한 카운터가 뒤엉킨다. */\n sortings: params.sortings?.length ? params.sortings : [{ name: 'eventTime', desc: true }],\n /*\n * 시각 필터는 **날짜로 바꿔서** 질의 빌더에 넘긴다.\n *\n * 화면은 ISO 문자열(`2026-08-07T11:00:00.000Z`)을 보내는데 eventTime 은 날짜 컬럼이고,\n * sqlite 는 그것을 공백 구분 형식(`2026-08-07 11:00:00.000`)으로 저장한다. 문자열 그대로\n * 비교하면 사전식으로 어긋나 **오류 없이 빈 결과**가 나온다(실측으로 확인했다).\n * 화면이 날짜를 어떻게 적어 보내든 서버가 받아 주는 편이 맞다 — 계약을 좁히는 대신 경계에서 옮긴다.\n */\n filters: (params.filters ?? []).map(f => (f.name === 'eventTime' ? { ...f, value: toDateValue(f.value) } : f))\n }\n\n /*\n * ── 대상 하나의 이력은 **파생 테이블**이 답한다 (2026-08-28 실측) ───────────\n *\n * 360 화면의 이력이 이 문으로 온다. 물음은 「이 품목(오더·자리·설비)의 사건을 최신순으로 30건」\n * 인데, 저널에는 그 모양의 목차가 없다: 축으로 걸러도 시각으로 정렬하는 순간 목차를 못 쓰고,\n * 품목과 로트가 한 문자열에 붙어 있어 품목으로는 아예 찾지 못한다. 실측 **63.998초**였다.\n *\n * 그 물음의 모양대로 만든 테이블이 이미 있다(`twin_subject_events`). 같은 질의가 **0.078초**다.\n * 화면을 고치지 않고 여기서 답을 가져오는 곳만 바꾼다 — 계약도 커서도 그대로다.\n *\n * 맞지 않는 물음(여러 트윈·자유 검색·종류 필터·다른 정렬)이거나 파생 테이블이 그 구간을 말할 수\n * 없으면 `null` 이 오고, 그때는 아래 저널 경로로 간다. 느리지만 온전하다.\n */\n if (instanceIds.length === 1) {\n const fast = await readModelPage(domain.id, instanceIds[0], effective, limit)\n if (fast) return fast\n }\n\n /*\n * ── **여러 종류를 시각순으로 물으면 나눠 묻는다** (2026-08-28 실측) ──────────\n *\n * 에너지 원장이 「이 여덟 종류를 시각 내림차순으로」를 묻는다. 그 모양에서 sqlite 는 종류 색인을\n * 버리고 시각 색인으로 337만 행을 걸으며 행마다 종류를 확인한다 — **37.181초, 결과 0건**.\n * 종류를 정확일치로 하나씩 물으면 색인이 시각 순서까지 주므로 **각 0.003초**다.\n *\n * 새 색인도 새 테이블도 필요 없다. 있는 색인(`ix_twin_event_7`)을 쓰게 하는 것이 전부다.\n * 건수 세기는 이미 빠르다(정렬을 지우므로 같은 색인을 탄다) — 목록만 이 갈래를 탄다.\n */\n const tiebreak: 'revision' | 'id' = instanceIds.length === 1 ? 'revision' : 'id'\n const split = splitShape(effective, effective.sortings ?? [])\n if (split) {\n const page = await eventTypeSplitPage(domain.id, instanceIds, split, limit, tiebreak, effective.pagination?.after)\n if (page) {\n /*\n * 건수는 일반 경로와 같은 방식으로 센다 — 상한까지 키만 읽는다. 이 조건은 정렬이 없어 종류\n * 색인을 그대로 타므로 값이 싸다(실측 0.003초).\n */\n const probe = getQueryBuilderFromListParams({\n repository: getRepository(TwinEvent),\n params: { ...effective, pagination: { page: 1, limit: COUNT_CAP + 1 } },\n domain,\n searchables: [...JOURNAL_SEARCHABLES],\n sortables: ['eventTime', 'revision', 'eventType', 'bizStep'],\n defaultLimit: COUNT_CAP + 1,\n maxLimit: COUNT_CAP + 1\n })\n probe.andWhere(`${probe.alias}.instanceId IN (:...instanceIds)`, { instanceIds })\n const counted = await probe\n .orderBy()\n .select(`${probe.alias}.id`)\n .getRawMany()\n .catch(() => null)\n const capped = !counted || counted.length > COUNT_CAP\n return {\n items: page.items,\n total: capped ? undefined : counted!.length,\n capped,\n countCap: COUNT_CAP,\n nextCursor: page.nextCursor,\n oldestRevision: undefined\n }\n }\n }\n\n const qb = getQueryBuilderFromListParams({\n repository: getRepository(TwinEvent),\n params: effective,\n domain,\n /* 전부 인덱스 가능한 승격 컬럼 — 자유 검색어는 서버가 이 축들로 펼친다. */\n searchables: [...JOURNAL_SEARCHABLES],\n /* 정렬은 인덱스가 받쳐 주는 축으로만 — 큰 저널에서 임의 컬럼 정렬은 전체 정렬 스캔이다. */\n sortables: ['eventTime', 'revision', 'eventType', 'bizStep'],\n defaultLimit: 100,\n maxLimit: 500,\n /*\n * ── 동시각을 가리는 축을 **두 경로가 같게** 쓴다 (2026-08-28 실측) ─────────\n *\n * 커서는 「이 행 다음부터」이므로 시각이 같은 행들 사이에서 무엇이 앞인지 정해야 한다. 기본값은\n * 기본키(`id`)인데, 파생 테이블 경로는 `revision` 으로 가린다(그 테이블에 `id` 가 없다).\n *\n * 두 경로가 한 목록을 나눠 답하면 그 축이 달라지고, 시각이 같은 행이 많은 자리에서 **본 행이\n * 또 나온다.** 실측: 재고 사건 2,665건이 같은 시각을 가진 트윈에서 30건씩 네 번 이어 읽어\n * 120건 중 15건이 되풀었다.\n *\n * 그래서 트윈 하나를 볼 때는 `revision` 으로 가린다 — 그 트윈 안에서 유일하고 빈 값이 없다.\n * 여러 트윈을 합쳐 보는 목록에서는 리비전이 트윈마다 따로 세는 번호라 유일하지 않으므로\n * 기본키를 쓴다.\n */\n cursorTiebreak: tiebreak\n })\n /* 대상 범위는 호출자가 필터로 빼먹을 수 있는 값이 아니다 — 계약상 필수라 여기서 강제한다. */\n qb.andWhere(`${qb.alias}.instanceId IN (:...instanceIds)`, { instanceIds })\n\n /*\n * ── 건수는 **상한까지만** 센다 (2026-08-22 실측) ──────────────────────────\n * 예전에는 `getManyAndCount()` 였다. 그 두 번째 질의가 조건에 맞는 행을 **전부** 세는데, 실측한\n * 개발 저널에서 트윈 하나가 358만 행이라 **한 번에 5.32초**였다. 이력 화면을 열 때마다 그것을\n * 물었고, 그 5초 동안 이벤트 루프가 다른 요청을 처리하지 못했다.\n *\n * 상한을 두면 같은 답이 **0.04초**다(130배). 상한을 넘으면 정확한 수를 모르는 것이 사실이므로\n * 목록 규약의 「서버 상한에 부딪힘(total 모름)」 갈래로 낸다 — 화면은 「처음 N 건만」이라고 말하고\n * 좁히기를 권한다. **알리지 않고 자르지 않는다**: 상한을 넘었다는 사실을 `capped` 로 함께 낸다.\n *\n * 왜 하위 질의로 세지 않나: 5개 드라이버에서 같이 도는 형태여야 한다(이 저장소의 절대 규칙).\n * 그래서 **키 컬럼만 상한까지 읽어 세는** 방식을 쓴다 — 색인만 읽으므로 행 본문을 만들지 않는다.\n */\n const items = await qb.getMany()\n const probe = qb\n .clone()\n .orderBy() /* 세는 데 순서는 필요 없다 — 정렬을 남기면 임의 축에서 전체 정렬이 된다. */\n .skip(0)\n .take(COUNT_CAP + 1)\n .select(`${qb.alias}.id`)\n const counted = await probe.getRawMany()\n const capped = counted.length > COUNT_CAP\n /* 상한을 넘었으면 **전체 수를 모른다** — 상한값을 전체로 적으면 화면이 거짓을 말한다. */\n const total = capped ? undefined : counted.length\n\n /* 다음 커서는 **이번 페이지의 마지막 행**에서 만든다. 페이지가 상한보다 짧으면 뒤가 없다. */\n const axes = cursorSortings(effective.sortings, tiebreak)\n const nextCursor = items.length === limit ? buildNextCursor(items[items.length - 1], axes) : undefined\n\n /*\n * ── **앞부분이 지워졌는지 함께 낸다** (2026-08-27) ─────────────────────────\n *\n * 보관 기간 정리가 오래된 사건을 지운다(기본 7일). 그러면 이 목록을 아무리 내려가도 어느 지점에서\n * 기록이 끝나는데, 화면이 그것을 말하지 않으면 사람은 「이 트윈은 그때 시작했다」로 읽는다.\n * 없었던 것과 지워진 것은 다른 사실이다.\n *\n * 저장하지 않고 그때그때 구한다 — 저장한 값은 실제 데이터와 어긋날 수 있고, 어긋나면 어느 쪽이\n * 사실인지 알 수 없다. 색인 `(domain, instanceId, revision)` 의 첫 행 하나라서 값이 싸다.\n *\n * 트윈 하나를 물었을 때만 낸다. 여러 트윈을 합쳐 보는 목록에서는 리비전이 트윈마다 따로 세는\n * 번호라 하나의 답이 없다.\n */\n let oldestRevision: number | undefined\n if (instanceIds.length === 1) {\n const first = await getRepository(TwinEvent)\n .createQueryBuilder('e')\n .select('e.revision', 'revision')\n .where('e.domain = :domainId', { domainId: domain.id })\n .andWhere('e.instanceId = :instanceId', { instanceId: instanceIds[0] })\n .orderBy('e.revision', 'ASC')\n .limit(1)\n .getRawOne<{ revision: unknown }>()\n .catch(() => null)\n const at = Number(first?.revision)\n if (Number.isFinite(at) && at > 0) oldestRevision = at\n }\n\n return { items, total, capped, countCap: COUNT_CAP, nextCursor, oldestRevision }\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 model 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. Pass baselinePeriods (1-14, with baselineStride day | week) to fold the same time-of-day window on each of the last N days (or the same weekday on each of the last N weeks) and get `baseline.metrics` — per metric the distribution of those windows (min / median / max) plus where the current window sits: `rank` (1 = smallest, direction-free) and `standing` (best | better-than-typical | typical | worse-than-typical | worst, which does apply the metric direction). This answers what a single comparison cannot: comparing to yesterday alone is useless if yesterday was also bad. Every requested window is returned in `samples` with `measured` true or false — windows without records are kept and counted (measuredCount vs requested) rather than dropped, so \"the last 7 days\" never silently means 3. No position is reported when the current window has no value or fewer than two baseline windows were measured; `noPosition` says which, because a missing distribution drawn as neutral would read as \"typical\". The baseline is produced even when the current window itself has no records, since that is exactly when knowing the usual level matters (a stopped line).'\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 /** 여러 구간 동시 비교 — 최근 N 구간의 같은 시간대 분포(1~14). */\n @Arg('baselinePeriods', type => Int, { nullable: true }) baselinePeriods?: number,\n /** 분포의 간격 — day(같은 시간대) | week(같은 요일·시간대). 기본 day. */\n @Arg('baselineStride', { nullable: true }) baselineStride?: 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 ...(baselinePeriods && baselinePeriods > 0\n ? { baseline: { periods: baselinePeriods, stride: (baselineStride as any) === 'week' ? 'week' : 'day' } }\n : {})\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 /*\n * 필요한 축만 — 지도는 `locations`·`attentions` 만 쓰는데 상태를 통째로 받고 있었다(실 저널로\n * 재 보니 트윈 하나가 최대 710KB, 같은 공간에 셋이면 약 2MB). 화면 전용 질의를 만들지 않고\n * 호출부가 축을 말한다(무방언 — 어휘가 화면 이름이 아니라 상태의 축이다).\n * 말하지 않으면 자르지 않는다(기존 호출부 무영향).\n */\n @Arg('axes', type => [String], { nullable: true }) axes?: string[],\n /*\n * 신호는 몇 건만 필요한가 — 레일은 20건을 그리고 나머지는 \"N건 더\" 라고만 말한다. 그런데 신호가\n * 많은 트윈은 그 전량이 108KB 다(`hatio-yard` 397건). 상위 N 만 보내고 **전체 개수와 자리별\n * 최고 심각도는 전량을 근거로** 함께 보낸다 — 레일은 잘라도 되지만 지도 색은 잘라선 안 된다.\n * 말하지 않으면 자르지 않는다.\n */\n @Arg('attentionLimit', type => Int, { nullable: true }) attentionLimit?: number\n ): Promise<any> {\n const state = await TwinEngine.recover(context.state.domain.id, instanceId, untilRevision ?? undefined, untilTime ?? undefined)\n const picked = pickAxes(state, axes)\n /* 오타를 알리지 않고 넘기지 않는다 — 화면이 빈 채로 뜨는 이유를 아무도 모르게 되는 것이 가장 나쁘다. */\n if (picked.unknown.length) twinWarn(`[twin] twinReplay: unknown axes ignored for \"${instanceId}\": ${picked.unknown.join(', ')}`)\n if (!attentionLimit || !picked.state?.attentions) return picked.state\n const d = digestAttentions(picked.state.attentions, attentionLimit)\n /* 잘랐다는 사실을 값으로 남긴다 — 개수만 줄어들면 화면은 \"괜찮아졌다\" 로 읽는다. */\n return { ...picked.state, attentions: d.attentions, attentionTotal: d.attentionTotal, severityByLocation: d.severityByLocation }\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 nullable: true,\n description:\n 'Operations capability of a twin for a time window (ISA-95 Part 4 OperationsCapability): per operation the ceiling for that window split into Committed (already promised), Unattainable (lost to resources that cannot be used — down, held, off-shift, expired qualification) and Available (what is left). Answers \"can we take more work?\" — which the steady-state rate alone cannot. Null when the twin is not running.'\n })\n async twinOperationsCapability(\n @Arg('instanceId', { nullable: true }) instanceId: string,\n @Arg('spaceId', { nullable: true }) spaceId: string,\n @Arg('unitsPerDay', type => Float) unitsPerDay: number,\n /** 능력을 물을 구간 — **구간 없이 능력은 존재하지 않는다**(표준 StartTime/EndTime). */\n @Arg('fromTime') fromTime: string,\n @Arg('toTime') toTime: string,\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 const from = Date.parse(fromTime)\n const to = Date.parse(toTime)\n if (Number.isNaN(from) || Number.isNaN(to)) throw new Error(`window is not a valid ISO time: ${fromTime} ~ ${toTime}`)\n if (to <= from) throw new Error(`window is inverted: ${fromTime} ~ ${toTime}`)\n\n /* 기준 주 규율은 `twinCapacity` 와 같다(같은 계산을 쓰므로 같은 주를 골라야 한다). */\n const base = sampleWeekStart ? Date.parse(sampleWeekStart) : from\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.operationsCapability(\n context.state.domain.id,\n { instanceId, spaceId },\n unitsPerDay,\n monday,\n { fromTime, toTime }\n )\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 model 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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/headless-twin",
3
- "version": "10.1.29",
3
+ "version": "10.1.31",
4
4
  "main": "dist-server/index.js",
5
5
  "things-factory": true,
6
6
  "author": "heartyoh <heartyoh@hatiolab.com>",
@@ -27,13 +27,13 @@
27
27
  "clean:shared": "rm -rf dist-shared tsconfig.shared.tsbuildinfo"
28
28
  },
29
29
  "dependencies": {
30
- "@operato/ops-contract": "^0.9.28",
31
- "@operato/twin-kernel": "^0.11.16",
32
- "@things-factory/auth-base": "^10.1.29",
33
- "@things-factory/cache-service": "^10.1.29",
30
+ "@operato/ops-contract": "^0.9.30",
31
+ "@operato/twin-kernel": "^0.11.20",
32
+ "@things-factory/auth-base": "^10.1.31",
33
+ "@things-factory/cache-service": "^10.1.31",
34
34
  "@things-factory/env": "^10.1.20",
35
- "@things-factory/ingest": "^10.1.29",
36
- "@things-factory/shell": "^10.1.29"
35
+ "@things-factory/ingest": "^10.1.31",
36
+ "@things-factory/shell": "^10.1.31"
37
37
  },
38
- "gitHead": "e6bf9dec003fe5e794746aaeb5852b3b50478232"
38
+ "gitHead": "5213e6f9913b40ea2618a2111d9884b792dbaa01"
39
39
  }
@@ -28,17 +28,24 @@ export class TwinAttentionQuery {
28
28
  * 경고는 **합집합이 그대로 뜻이 있는 유일한 렌즈**다. 예측은 합칠 수 없다(P50 두 개를 더할 수 없다) —
29
29
  * 그래서 예측은 여전히 인스턴스별로 답하고 화면이 나란히 놓는다.
30
30
  */
31
- @Arg('spaceId', { nullable: true }) spaceId?: string,
32
- /** 상위 몇 건만 — 말하지 않으면 자르지 않는다. 전체 개수는 `twinAttentionDigest` 가 함께 답한다. */
33
- @Arg('limit', type => Int, { nullable: true }) limit?: number
31
+ @Arg('spaceId', { nullable: true }) spaceId?: string
32
+ /*
33
+ * ── 자르는 인자는 여기 없다 (2026-09-19, 수석 아키텍트 판정) ──────────────
34
+ *
35
+ * `limit` 이 있었다. 자르면서 **몇 건을 잘랐는지는 답에 싣지 못했다** — 답이 맨 목록이라 실을
36
+ * 자리가 없었기 때문이다. 자를 수 있는 문이 잘린 수를 못 말하면, 스무 건만 받은 화면은 그것이
37
+ * 전부인지 이백 중 스무 건인지 구별할 수 없다.
38
+ *
39
+ * 자르려는 소비자는 `twinAttentionDigest` 를 쓴다 — 그 문은 `attentionTotal` 을 함께 답하고,
40
+ * 자리별 색을 **전량을 근거로** 계산한다. 이 문은 자르지 않는다.
41
+ */
34
42
  ): Promise<any[]> {
35
- if (spaceId) return (await TwinEngine.attentionsOfSpace(context.state.domain.id, spaceId, limit ?? undefined)).attentions
43
+ if (spaceId) return (await TwinEngine.attentionsOfSpace(context.state.domain.id, spaceId)).attentions
36
44
  if (!instanceId || !TwinEngine.owns(context.state.domain.id, instanceId)) return [] // 테넌트 격리
37
45
  // 라이브 커널 스냅샷(fresh) 우선 — snapshot()=resync() 는 캐시라 커맨드 델타 지연(recover 와 동일 원천 사용).
38
46
  const kernel = TwinEngine.kernel(context.state.domain.id, instanceId)
39
47
  const snap = kernel?.getSnapshot?.() ?? TwinEngine.snapshot(context.state.domain.id, instanceId)
40
- const list = snap?.attentions ?? []
41
- return limit && limit > 0 ? digestAttentions(list, limit).attentions : list
48
+ return snap?.attentions ?? []
42
49
  }
43
50
 
44
51
  /**
@@ -42,6 +42,22 @@ function toDateValue(v: any): any {
42
42
  return Array.isArray(v) ? v.map(one) : one(v)
43
43
  }
44
44
 
45
+ /**
46
+ * 자유 검색어가 펼쳐지는 축 — **목록과 건수가 같은 것을 본다**(2026-09-19).
47
+ *
48
+ * 전부 인덱스 가능한 승격 컬럼이다(payload JSON 안이 아니라). 프레임워크 질의 빌더는 이 목록에 없는
49
+ * 컬럼의 LIKE 를 경고 후 무시한다 — 인덱스 없는 전체 스캔을 막기 위해서다.
50
+ *
51
+ * ── 왜 상수인가 ────────────────────────────────────────────────────────────
52
+ * 목록과 건수가 **다른 빌더**로 만들어진다(빠른 갈래의 건수 probe). 두 자리에 배열을 따로 적으면
53
+ * 한쪽만 늘어나는 날 목록은 걸러지고 건수는 안 걸러진다 — 화면에 「한 건」과 「전체 수」가 나란히 선다.
54
+ *
55
+ * 지금은 그 일이 **일어날 수 없다**: 빠른 갈래는 검색어가 있으면 아예 안 탄다
56
+ * (`event-type-split-shape.ts` — 시험 「자유 검색어」가 그 축을 붙든다). 그래도 같은 상수를 쓴다 —
57
+ * 그 규칙이 언젠가 느슨해질 때 이 자리가 조용히 갈리지 않게.
58
+ */
59
+ export const JOURNAL_SEARCHABLES = ['epc', 'orderId', 'locationId', 'moverId', 'bizStep', 'eventType', 'correlationId'] as const
60
+
45
61
  @Resolver()
46
62
  export class TwinJournalQuery {
47
63
  @Query(returns => [TwinEvent], {
@@ -191,6 +207,7 @@ export class TwinJournalQuery {
191
207
  *
192
208
  * 정렬 기본값은 revision DESC(최신순) — 저널의 자연 순서이자 ix_twin_event_0 가 그대로 타는 축.
193
209
  */
210
+
194
211
  @Query(returns => TwinEventList, {
195
212
  description:
196
213
  'List twin journal events with the standard list contract (filters, pagination, sortings) plus the total match count. ' +
@@ -286,7 +303,7 @@ export class TwinJournalQuery {
286
303
  repository: getRepository(TwinEvent),
287
304
  params: { ...effective, pagination: { page: 1, limit: COUNT_CAP + 1 } },
288
305
  domain,
289
- searchables: [],
306
+ searchables: [...JOURNAL_SEARCHABLES],
290
307
  sortables: ['eventTime', 'revision', 'eventType', 'bizStep'],
291
308
  defaultLimit: COUNT_CAP + 1,
292
309
  maxLimit: COUNT_CAP + 1
@@ -314,7 +331,7 @@ export class TwinJournalQuery {
314
331
  params: effective,
315
332
  domain,
316
333
  /* 전부 인덱스 가능한 승격 컬럼 — 자유 검색어는 서버가 이 축들로 펼친다. */
317
- searchables: ['epc', 'orderId', 'locationId', 'moverId', 'bizStep', 'eventType', 'correlationId'],
334
+ searchables: [...JOURNAL_SEARCHABLES],
318
335
  /* 정렬은 인덱스가 받쳐 주는 축으로만 — 큰 저널에서 임의 컬럼 정렬은 전체 정렬 스캔이다. */
319
336
  sortables: ['eventTime', 'revision', 'eventType', 'bizStep'],
320
337
  defaultLimit: 100,
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import { test } from 'node:test'
15
15
  import assert from 'node:assert/strict'
16
+ import { readFile } from 'node:fs/promises'
16
17
 
17
18
  import { digestAttentions, compareAttentions, mergeLensAttentions } from '../server/engine/attention-digest.ts'
18
19
 
@@ -133,3 +134,28 @@ test('★ 렌즈가 하나도 없으면 빈 요약 — 공간에 배치된 트
133
134
  assert.equal(d.attentionTotal, 0)
134
135
  assert.deepEqual(d.severityByLocation, {})
135
136
  })
137
+
138
+ /*
139
+ * ── 자를 수 있는 문만 자른다 (2026-09-19, 수석 아키텍트 판정) ────────────────
140
+ *
141
+ * 목록 문(`twinAttentions`)에 `limit` 이 있었다. 자르면서 **몇 건을 잘랐는지는 답에 실을 자리가
142
+ * 없었다** — 답이 맨 목록이라서다. 그러면 스무 건을 받은 화면은 그것이 전부인지 이백 중 스물인지
143
+ * 구별할 수 없고, 그 구별이 없으면 「괜찮아졌다」로 읽힌다(위 ②와 같은 자리).
144
+ *
145
+ * 그래서 자르는 일은 **자른 수를 말할 수 있는 문**에만 둔다. 이 검사는 그 규칙을 소스에서 센다 —
146
+ * 인자를 되돌려 놓으면 빨강이 된다.
147
+ */
148
+ test('★ 목록 문은 자르지 않는다 — 자르는 인자는 전체 개수를 함께 내는 문에만 있다', async () => {
149
+ const source = await readFile(new URL('../server/service/twin-attention/twin-attention-query.ts', import.meta.url), 'utf-8')
150
+
151
+ const listAt = source.indexOf('async twinAttentions(')
152
+ const digestAt = source.indexOf('async twinAttentionDigest(')
153
+ assert.ok(listAt >= 0 && digestAt > listAt, '두 문을 못 찾았다 — 이 검사는 아무것도 지키지 않는다')
154
+
155
+ const list = source.slice(listAt, digestAt)
156
+ const digest = source.slice(digestAt)
157
+
158
+ assert.ok(!/@Arg\(\s*'limit'/.test(list), '목록 문에 자르는 인자가 있다 — 자른 수를 답에 실을 자리가 없다')
159
+ assert.ok(/@Arg\(\s*'limit'/.test(digest), '자르는 문이 없어졌다 — 화면이 상위 N 을 받을 길이 없다')
160
+ assert.ok(/digestAttentions\(/.test(digest), '자르는 문은 전체 개수를 함께 내는 함수를 지나야 한다')
161
+ })