@things-factory/headless-twin 10.1.28 → 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.
- package/dist-server/service/reference/actuation-verification.d.ts +22 -0
- package/dist-server/service/reference/actuation-verification.js +37 -0
- package/dist-server/service/reference/actuation-verification.js.map +1 -1
- package/dist-server/service/reference/reference-resolver.js +32 -2
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/twin-attention/twin-attention-query.d.ts +1 -3
- package/dist-server/service/twin-attention/twin-attention-query.js +15 -6
- package/dist-server/service/twin-attention/twin-attention-query.js.map +1 -1
- package/dist-server/service/twin-journal/twin-journal-query.d.ts +15 -0
- package/dist-server/service/twin-journal/twin-journal-query.js +18 -3
- package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-lineage-query.js +14 -0
- package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-item-query.js +11 -6
- package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
- package/package.json +8 -8
- package/server/service/reference/actuation-verification.ts +42 -0
- package/server/service/reference/reference-resolver.ts +33 -2
- package/server/service/twin-attention/twin-attention-query.ts +13 -6
- package/server/service/twin-journal/twin-journal-query.ts +19 -2
- package/server/service/twin-model/twin-lineage-query.ts +13 -0
- package/server/service/twin-model/twin-model-item-query.ts +33 -6
- package/test/actuation-verification-state.test.ts +46 -1
- package/test/attention-digest.test.ts +26 -0
- package/test/axis-declaration-passthrough-db.test.ts +43 -1
- package/test/event-type-split.test.ts +23 -0
- package/test/lineage-survives-restart.test.ts +15 -0
- package/test/twin-model-item-db.test.ts +18 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -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: [
|
|
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":"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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@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"]}
|
|
@@ -195,6 +195,20 @@ let TwinLineageQuery = class TwinLineageQuery {
|
|
|
195
195
|
else if (!recipe) {
|
|
196
196
|
conformance = { comparable: false, reason: 'no-recipe-for-material' };
|
|
197
197
|
}
|
|
198
|
+
else if (!(madeBy.get(epc) ?? []).length) {
|
|
199
|
+
/*
|
|
200
|
+
* ── **안 본 것을 「다르다」고 하지 않는다** (2026-09-18 · :3000 화면에서 확인) ─────────
|
|
201
|
+
*
|
|
202
|
+
* 이 개체를 만든 변환이 저널에 하나도 없으면 투입을 하나도 관측하지 못한 것이다. 그런데
|
|
203
|
+
* 아래 셈은 그대로 돌아 관측을 전부 0 으로 적었고, 레시피가 요구한 자재 23 줄이 전부
|
|
204
|
+
* 「안 맞는다」가 됐다 — 화면은 「레시피와 다릅니다」를 확신에 찬 얼굴로 그렸다. 실제로는
|
|
205
|
+
* 원본이 변환을 아직 안 보냈을 뿐이다(`mes-miratek` 의 카메라 로트가 그 경우다).
|
|
206
|
+
*
|
|
207
|
+
* 이 파일의 규율이 이미 그것을 말한다 — 짐작으로 이으면 「설계와 다르다」는 거짓 진단이
|
|
208
|
+
* 되고, 그건 아무 말도 안 하는 것보다 나쁘다. 관측이 **없는** 것도 같은 부류다.
|
|
209
|
+
*/
|
|
210
|
+
conformance = { comparable: false, reason: 'made-not-observed', recipe: recipe.key };
|
|
211
|
+
}
|
|
198
212
|
else {
|
|
199
213
|
/*
|
|
200
214
|
* 관측: **바인딩된 자재까지 내려가 센다** — 중간 산물(WIP)은 통과한다.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"twin-lineage-query.js","sourceRoot":"","sources":["../../../server/service/twin-model/twin-lineage-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Int, Query, Resolver } from 'type-graphql'\n\nimport { ScalarObject, getRepository } from '@things-factory/shell'\n\nimport { itemRefOf, resolveByDeclaration } from './item-ref.js'\nimport { chooseRecipe } from './choose-recipe.js'\n\nimport { TwinInstance } from '../twin-instance/twin-instance.js'\nimport { TwinEvent } from '../twin-event/twin-event.js'\n\n/*\n * 계보 — **이 개체는 무엇에서 나왔나**, 그리고 **설계대로 만들어졌나.**\n *\n * ── 구성과 계보는 주어가 다르다 ────────────────────────────────────────────────\n * 구성(BOM)의 주어는 품목 **정의**다 — \"트레일러는 프레임 1개로 만든다\"(선언).\n * 계보의 주어는 품목 **개체**다 — \"이 트레일러는 저 프레임 로트에서 나왔다\"(관측).\n * 둘은 같은 모양이라 겹쳐 볼 수 있고, **그 차이가 곧 진단**이다: \"설계는 1개인데 이건 2개를 먹었다\".\n * 트윈이 아니면 못 하는 답이고, 이 질의가 그 답의 재료다.\n *\n * ── 어디서 읽나 ────────────────────────────────────────────────────────────────\n * 계보는 상태가 아니라 **저널**에 있다(EPCIS `TransformationEvent` 의 입력·산출 목록). 상태는\n * 지금 무엇이 있는지만 알고, 없어진 원자재가 무엇이었는지는 이벤트만 안다.\n *\n * ── 규모 ───────────────────────────────────────────────────────────────────────\n * 입력 목록은 **컬럼이 아니라 봉투 안**에 있어서 인덱스로 좁힐 수 없다. 그래서 변환 이벤트만\n * 창으로 끊어 읽고, **끊었으면 끊었다고 말한다.** 그 사실을 감추면 \"이 개체는 원자재가 없다\" 로\n * 읽히고, 계보에서 그 오해는 리콜 범위를 좁게 잡는 결과로 이어진다.\n */\n\n\n@Resolver()\nexport class TwinLineageQuery {\n @Query(returns => ScalarObject, {\n nullable: true,\n description:\n 'Where one item came from (observed) laid next to what its type declares (BOM). Walks TransformationEvent input/output lists in the journal; reports the window it read and whether it was truncated. The declared|observed difference is the diagnosis: \"the recipe says one frame, this one consumed two\".'\n })\n async twinItemLineage(\n @Arg('instanceId') instanceId: string,\n @Arg('epc') epc: string,\n @Ctx() context: ResolverContext,\n @Arg('maxDepth', type => Int, { nullable: true }) maxDepth?: number,\n @Arg('window', type => Int, { nullable: true }) window?: number\n ): Promise<any | null> {\n const domainId = context.state.domain.id\n const inst = await getRepository(TwinInstance).findOne({ where: { domain: { id: domainId }, instanceId } })\n if (!inst) return null\n\n const depthCap = Math.min(Math.max(maxDepth ?? 6, 1), 12)\n const windowCap = Math.min(Math.max(window ?? 5000, 1), 50000)\n\n /*\n * 변환 이벤트만 읽는다. **최신부터** 읽는다 — 계보를 묻는 개체는 대개 최근에 만들어진 것이고,\n * 창을 넘겨 잘릴 때 잃는 쪽이 오래된 것이어야 덜 아프다.\n */\n /*\n * 관계를 **컬럼으로** 묻는다 (2026-08-28). `find({ where: { domain: { id } } })` 은 관계를 가진\n * 엔티티에서 두 단계 질의가 되고 **안쪽에 상한이 없다** — `take` 가 있어도 조건에 맞는 행을 전부\n * 만든 뒤 밖에서 자른다. 변환 사건이 적은 트윈에서는 표가 안 나지만, 변환을 많이 내는 트윈에서는\n * 이 한 번이 그 종류 전체를 만든다. 질의 빌더로 물으면 상한이 안쪽에 걸린다.\n *\n * 종류가 정확일치이므로 `(domain, instance, event_type, event_time)` 색인이 시각 순서까지 준다.\n */\n const rows: any[] = await getRepository(TwinEvent)\n .createQueryBuilder('e')\n .where('e.domain = :domainId', { domainId })\n .andWhere('e.instanceId = :instanceId', { instanceId })\n .andWhere('e.eventType = :eventType', { eventType: 'epcis.TransformationEvent' })\n .orderBy('e.eventTime', 'DESC')\n .limit(windowCap)\n .getMany()\n const truncated = rows.length >= windowCap\n\n /* 산출 → 그 변환. 한 개체는 한 번만 만들어지지만, 데이터가 그렇지 않을 수 있으므로 목록으로 든다. */\n const madeBy = new Map<string, { inputs: string[]; at?: string; tx?: string }[]>()\n for (const r of rows) {\n const d = r.payload?.data ?? {}\n const outs: string[] = d.outputEPCList ?? []\n const ins: string[] = d.inputEPCList ?? []\n const step = { inputs: ins, at: r.eventTime, tx: d.bizTransactionList?.[0]?.bizTransaction }\n for (const o of outs) {\n const list = madeBy.get(o) ?? []\n list.push(step)\n madeBy.set(o, list)\n }\n }\n\n /* 거슬러 올라간다 — 너비 우선, 지나온 것은 다시 파지 않는다(사이클·공유 부품). */\n const items: any[] = [{ epc, itemRef: itemRefOf(epc), depth: 0 }]\n const edges: any[] = []\n const seen = new Set<string>([epc])\n let depthTruncated = false\n let frontier = [{ epc, depth: 0 }]\n while (frontier.length) {\n const next: typeof frontier = []\n for (const cur of frontier) {\n if (cur.depth >= depthCap) { depthTruncated = true; continue }\n for (const step of madeBy.get(cur.epc) ?? []) {\n for (const input of step.inputs) {\n edges.push({ from: cur.epc, to: input, at: step.at, tx: step.tx, depth: cur.depth + 1 })\n if (seen.has(input)) continue\n seen.add(input)\n items.push({ epc: input, itemRef: itemRefOf(input), depth: cur.depth + 1 })\n next.push({ epc: input, depth: cur.depth + 1 })\n }\n }\n }\n frontier = next\n }\n\n /*\n * **겹쳐 본다.** 이 개체의 품목이 어느 레시피의 산출이면, 그 레시피가 선언한 투입과 실제로\n * 소비된 것을 자재별로 나란히 센다.\n *\n * 개체(EPC)와 자재 키를 잇는 것은 `productionSpec.binding`(자재 키 → GS1 품목 참조)이다.\n * 바인딩이 없으면 **비교하지 않는다** — 짐작으로 이으면 \"설계와 다르다\" 는 거짓 진단이 나오고,\n * 그건 아무 말도 안 하는 것보다 나쁘다.\n */\n const spec: any = (inst.model as any)?.productionSpec\n const binding: Record<string, string> = spec?.binding ?? {}\n const refToMaterial = new Map(Object.entries(binding).map(([mat, ref]) => [String(ref), mat]))\n /*\n * ── **화면은 끝마디로 묻는다** (2026-08-23) ─────────────────────────────────\n *\n * 서랍의 포커스 식별자는 클래스의 끝마디다(`net/class/product-M0000438`). 대응 표의 값은 전체\n * 식별자(`https://…/class/product-M0000438`)이므로 곧바로는 맞지 않고, 그래서 「선언된 자재가\n * 아닙니다」가 떴다 — 그 품목은 대응 표에 있었다.\n *\n * 끝마디로도 찾되 **하나뿐일 때만** 인정한다. 둘 이상이 같은 끝마디를 가지면 어느 것인지 모르므로\n * 아무 말도 하지 않는다 — 짐작으로 이으면 남의 레시피와 견주게 되고, 그것이 「설계와 다르다」는\n * 거짓 진단이 된다.\n */\n const materialOf = (e: string) => resolveByDeclaration(itemRefOf(e), refToMaterial) ?? null\n\n const myMaterial = materialOf(epc)\n const recipes: any[] = spec?.definition?.recipes ?? []\n\n /*\n * ── 「무엇으로 만들었나」는 **적혀 있다** (2026-09-05) ───────────────────────\n *\n * 예전에는 이 자리가 자재로 되짚어 `recipes.find(...)` 로 **먼저 걸리는 하나**를 골랐다. 배열\n * 순서가 답을 정한다는 뜻이고, 한 자재에 레시피가 둘이면 틀린 답이 확신에 찬 얼굴로 나온다.\n * 실제로 그런 자재가 있다 — `BOLT-M8` 에 `RCP-BOLT-STD` · `RCP-BOLT-FAST` 둘이다.\n *\n * 그런데 답은 이미 기록되어 있다. 만들 때 정해지는 사실이고 커널이 오더에 싣는다\n * (`FlowOrder.recipeKey`). 변환 사건은 그 오더를 거래 식별자로 가리키고(`bizTransactionList`),\n * 이 함수는 그 값을 이미 `tx` 로 꺼내 놓고도 쓰지 않았다.\n *\n * 세 자리가 다 있었고 마지막 연결만 없었다. 「가릴 수 없다」가 아니라 **안 읽고 있었다.**\n *\n * ── 규칙 셋 — 짐작하지 않는다 ───────────────────────────────────────────────\n * 오더가 말한다 그것을 쓴다. 판이 둘이든 열이든 상관없다\n * 오더가 말 안 하고\n * 후보가 하나 그것을 쓴다 — 고를 것이 없으면 짐작이 아니다\n * 후보가 둘 이상 **답하지 않는다.** 어느 것인지 모르는 채로 견주면 거짓 진단이 된다\n *\n * 마지막 줄이 이 함수가 식별자 되찾기에 이미 적용하던 규율이다(「후보가 둘 이상이면 답하지\n * 않는다」). 두 줄 위에서만 안 지키고 있었다.\n */\n const candidates = myMaterial\n ? recipes.filter((r: any) => (r.outputs ?? []).some((o: any) => o.material === myMaterial))\n : []\n\n /* 이 개체를 만든 변환이 가리키는 거래 — 그 오더가 레시피를 말한다. */\n const madeTx = (madeBy.get(epc) ?? []).map(s => s.tx).find(Boolean)\n const saidRecipeKey = madeTx ? await recipeKeyOfTransaction(domainId, instanceId, String(madeTx)) : undefined\n\n const chosen = chooseRecipe(recipes, candidates, saidRecipeKey)\n const recipe = chosen.recipe\n\n /*\n * ── 「모르는 개체」와 「대응이 없는 개체」는 다른 사실이다 (2026-08-23) ──────\n *\n * 화면이 「이 개체는 선언된 자재가 아닙니다(라인 내부의 중간 산물일 수 있습니다)」라고 말했는데,\n * 그 품목은 원본의 품목 선언에 **이름까지 있었다**(「냉동그린빈스」). 바인딩 표가 그 품목을 담지\n * 못한 것이고, 그것은 「중간 산물」과 전혀 다른 이야기다 — 사용자가 할 일도 다르다(원본을 볼 일이\n * 아니라 대응을 고칠 일이다).\n *\n * 그래서 개체가 가리키는 품목이 **모델의 품목 선언에 있는지** 함께 본다. 이름은 그 자리에 있으니\n * 함께 낸다(화면이 사람에게 무엇을 말하는지 알 수 있게).\n */\n const definitions: any[] = (inst.model as any)?.materialDefinitions ?? []\n const defByRef = new Map(definitions.filter(d => d?.id).map(d => [String(d.id), d]))\n /* 정의도 같은 문으로 되찾는다 — 규칙 하나, 후보가 둘 이상이면 답하지 않는다. */\n const myRef = itemRefOf(epc)\n const myDef = resolveByDeclaration(myRef, defByRef)\n\n let conformance: any = null\n if (!Object.keys(binding).length) {\n conformance = { comparable: false, reason: 'no-binding' }\n } else if (!myMaterial && myDef) {\n /* 원본은 이 품목을 안다 — 개체를 자재에 잇는 대응이 없어서 비교하지 못한다. */\n conformance = {\n comparable: false,\n reason: 'item-known-not-bound',\n ...(typeof myDef.name === 'string' && myDef.name ? { itemName: myDef.name } : {})\n }\n } else if (!myMaterial) {\n conformance = { comparable: false, reason: 'item-not-bound' }\n } else if (chosen.basis === 'said-but-undeclared') {\n /* 오더는 말했는데 그 레시피가 모델의 선언에 없다 — 원본과 선언이 어긋난 것이지 모르는 것이 아니다. */\n conformance = { comparable: false, reason: 'recipe-not-declared', recipe: saidRecipeKey }\n } else if (chosen.basis === 'ambiguous') {\n /* 짐작하지 않는다 — 어느 것으로 만들었는지 말해 주는 사실이 없다. 후보를 밝혀 사람이 판단하게 한다. */\n conformance = {\n comparable: false,\n reason: 'many-recipes-for-material',\n recipes: candidates.map((r: any) => r.key).sort()\n }\n } else if (!recipe) {\n conformance = { comparable: false, reason: 'no-recipe-for-material' }\n } else {\n /*\n * 관측: **바인딩된 자재까지 내려가 센다** — 중간 산물(WIP)은 통과한다.\n *\n * 다단 라인에서 완제품의 직접 투입은 원자재가 아니라 **WIP** 다: 원자재는 첫 공정에서\n * 소비되고 완제품은 마지막 공정에서 나온다. 그래서 직접 투입만 세면 레시피가 선언한 자재와\n * **영원히 안 맞는다**(검사가 이것을 잡았다).\n *\n * WIP 은 라우트의 내부 산물이고 선언된 자재가 아니다 — 레시피는 자재로 말하므로 비교도\n * 자재로 해야 한다. 바인딩에 있으면 거기서 멈추고, 없으면 그 위로 계속 따라간다.\n *\n * 다만 **다른 완제품까지 넘어가지 않는다**: 바인딩된 것을 만나면 멈추므로, 하위조립도\n * 자재로 선언돼 있으면 그 자리에서 세어진다(그 아래는 그 자재 자신의 레시피가 볼 일이다).\n */\n const observed = new Map<string, number>()\n let unbound = 0\n const parentsOf = (e: string) => edges.filter(x => x.from === e).map(x => x.to)\n const walked = new Set<string>([epc])\n let layer = parentsOf(epc)\n while (layer.length) {\n const deeper: string[] = []\n for (const e of layer) {\n if (walked.has(e)) continue\n walked.add(e)\n const mat = materialOf(e)\n if (mat) { observed.set(mat, (observed.get(mat) ?? 0) + 1); continue }\n const up = parentsOf(e)\n /* 바인딩에도 없고 더 올라갈 곳도 없다 — 정말 모르는 투입이다(수만 낸다). */\n if (!up.length) { unbound++; continue }\n deeper.push(...up)\n }\n layer = deeper\n }\n const declared = new Map<string, number>((recipe.inputs ?? []).map((i: any) => [i.material, i.qty ?? null]))\n const keys = [...new Set([...declared.keys(), ...observed.keys()])]\n conformance = {\n comparable: true,\n material: myMaterial,\n recipe: recipe.key,\n /* 그 레시피를 **어떻게 알았나** — 화면이 「기록으로 안다」와 「하나뿐이라 안다」를 구별할 수 있게. */\n recipeBasis: chosen.basis,\n /* 자재마다 선언·관측을 나란히. 어느 한쪽에만 있는 것도 사실이다(빠뜨리지 않는다). */\n lines: keys.map(m => ({\n material: m,\n declared: declared.has(m) ? declared.get(m) : null,\n observed: observed.get(m) ?? 0,\n matches: declared.get(m) === (observed.get(m) ?? 0)\n })),\n /* 바인딩에 없는 투입 — 설계에 없는 것이 들어갔을 수도, 바인딩이 덜 선언됐을 수도 있다.\n 어느 쪽인지 우리는 모르므로 **수만 낸다.** */\n unboundInputs: unbound\n }\n }\n\n return {\n instanceId,\n epc,\n itemRef: itemRefOf(epc),\n material: myMaterial,\n items,\n edges,\n conformance,\n /* 무엇을 근거로 답했나 — 창을 넘겨 끊겼으면 계보가 실제보다 얕게 보인다. */\n basis: { source: 'journal', transformationsRead: rows.length, truncated, depthTruncated, depthCap, windowCap }\n }\n }\n}\n\n/**\n * 그 거래를 가리키는 오더가 **어느 레시피로** 만들었다고 말하는가.\n *\n * ── 왜 거래 식별자로 묻나 ───────────────────────────────────────────────────\n * EPCIS 변환 사건에는 레시피가 없다 — 표준에 그 자리가 없고, 대신 생산 오더를 거래로 가리킨다\n * (`bizTransactionList`). 오더 사건은 `recipeKey` 와 그 거래 식별자를 **둘 다** 든다. 그래서 이\n * 두 칸이 EPCIS 에서 오더로 건너가는 다리다(§`twin-event-keys.ts`).\n *\n * ── 색인을 탄다 ─────────────────────────────────────────────────────────────\n * `ix_twin_event_9 (domain, instanceId, bizTransaction)` 로 좁힌다. 이 칸이 없던 때는 payload 를\n * 문자열로 훑어야 했고 32만 행에 3분이 걸렸다 — 화면이 기다릴 비용이 아니다.\n *\n * ── 못 찾으면 **답하지 않는다** ──────────────────────────────────────────────\n * 옛 행에는 이 칸이 비어 있다(과거 행은 채우지 않는다는 규율). 그때는 `undefined` 를 내고, 부르는\n * 쪽이 후보 수로 판단한다 — 여기서 아무거나 고르면 예전과 같은 결함이 된다.\n */\nasync function recipeKeyOfTransaction(\n domainId: string,\n instanceId: string,\n bizTransaction: string\n): Promise<string | undefined> {\n const row = await getRepository(TwinEvent)\n .createQueryBuilder('e')\n .where('e.domain = :domainId', { domainId })\n .andWhere('e.instanceId = :instanceId', { instanceId })\n .andWhere('e.bizTransaction = :tx', { tx: bizTransaction })\n .andWhere('e.eventType = :eventType', { eventType: 'order.status' })\n /* 어느 줄이든 같은 오더를 말하므로 하나면 된다 — 가장 이른 것이 그 오더가 선 순간이다. */\n .orderBy('e.eventTime', 'ASC')\n .limit(1)\n .getOne()\n\n const said = (row?.payload as any)?.data?.recipeKey\n return typeof said === 'string' && said ? said : undefined\n}\n"]}
|
|
1
|
+
{"version":3,"file":"twin-lineage-query.js","sourceRoot":"","sources":["../../../server/service/twin-model/twin-lineage-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Int, Query, Resolver } from 'type-graphql'\n\nimport { ScalarObject, getRepository } from '@things-factory/shell'\n\nimport { itemRefOf, resolveByDeclaration } from './item-ref.js'\nimport { chooseRecipe } from './choose-recipe.js'\n\nimport { TwinInstance } from '../twin-instance/twin-instance.js'\nimport { TwinEvent } from '../twin-event/twin-event.js'\n\n/*\n * 계보 — **이 개체는 무엇에서 나왔나**, 그리고 **설계대로 만들어졌나.**\n *\n * ── 구성과 계보는 주어가 다르다 ────────────────────────────────────────────────\n * 구성(BOM)의 주어는 품목 **정의**다 — \"트레일러는 프레임 1개로 만든다\"(선언).\n * 계보의 주어는 품목 **개체**다 — \"이 트레일러는 저 프레임 로트에서 나왔다\"(관측).\n * 둘은 같은 모양이라 겹쳐 볼 수 있고, **그 차이가 곧 진단**이다: \"설계는 1개인데 이건 2개를 먹었다\".\n * 트윈이 아니면 못 하는 답이고, 이 질의가 그 답의 재료다.\n *\n * ── 어디서 읽나 ────────────────────────────────────────────────────────────────\n * 계보는 상태가 아니라 **저널**에 있다(EPCIS `TransformationEvent` 의 입력·산출 목록). 상태는\n * 지금 무엇이 있는지만 알고, 없어진 원자재가 무엇이었는지는 이벤트만 안다.\n *\n * ── 규모 ───────────────────────────────────────────────────────────────────────\n * 입력 목록은 **컬럼이 아니라 봉투 안**에 있어서 인덱스로 좁힐 수 없다. 그래서 변환 이벤트만\n * 창으로 끊어 읽고, **끊었으면 끊었다고 말한다.** 그 사실을 감추면 \"이 개체는 원자재가 없다\" 로\n * 읽히고, 계보에서 그 오해는 리콜 범위를 좁게 잡는 결과로 이어진다.\n */\n\n\n@Resolver()\nexport class TwinLineageQuery {\n @Query(returns => ScalarObject, {\n nullable: true,\n description:\n 'Where one item came from (observed) laid next to what its type declares (BOM). Walks TransformationEvent input/output lists in the journal; reports the window it read and whether it was truncated. The declared|observed difference is the diagnosis: \"the recipe says one frame, this one consumed two\".'\n })\n async twinItemLineage(\n @Arg('instanceId') instanceId: string,\n @Arg('epc') epc: string,\n @Ctx() context: ResolverContext,\n @Arg('maxDepth', type => Int, { nullable: true }) maxDepth?: number,\n @Arg('window', type => Int, { nullable: true }) window?: number\n ): Promise<any | null> {\n const domainId = context.state.domain.id\n const inst = await getRepository(TwinInstance).findOne({ where: { domain: { id: domainId }, instanceId } })\n if (!inst) return null\n\n const depthCap = Math.min(Math.max(maxDepth ?? 6, 1), 12)\n const windowCap = Math.min(Math.max(window ?? 5000, 1), 50000)\n\n /*\n * 변환 이벤트만 읽는다. **최신부터** 읽는다 — 계보를 묻는 개체는 대개 최근에 만들어진 것이고,\n * 창을 넘겨 잘릴 때 잃는 쪽이 오래된 것이어야 덜 아프다.\n */\n /*\n * 관계를 **컬럼으로** 묻는다 (2026-08-28). `find({ where: { domain: { id } } })` 은 관계를 가진\n * 엔티티에서 두 단계 질의가 되고 **안쪽에 상한이 없다** — `take` 가 있어도 조건에 맞는 행을 전부\n * 만든 뒤 밖에서 자른다. 변환 사건이 적은 트윈에서는 표가 안 나지만, 변환을 많이 내는 트윈에서는\n * 이 한 번이 그 종류 전체를 만든다. 질의 빌더로 물으면 상한이 안쪽에 걸린다.\n *\n * 종류가 정확일치이므로 `(domain, instance, event_type, event_time)` 색인이 시각 순서까지 준다.\n */\n const rows: any[] = await getRepository(TwinEvent)\n .createQueryBuilder('e')\n .where('e.domain = :domainId', { domainId })\n .andWhere('e.instanceId = :instanceId', { instanceId })\n .andWhere('e.eventType = :eventType', { eventType: 'epcis.TransformationEvent' })\n .orderBy('e.eventTime', 'DESC')\n .limit(windowCap)\n .getMany()\n const truncated = rows.length >= windowCap\n\n /* 산출 → 그 변환. 한 개체는 한 번만 만들어지지만, 데이터가 그렇지 않을 수 있으므로 목록으로 든다. */\n const madeBy = new Map<string, { inputs: string[]; at?: string; tx?: string }[]>()\n for (const r of rows) {\n const d = r.payload?.data ?? {}\n const outs: string[] = d.outputEPCList ?? []\n const ins: string[] = d.inputEPCList ?? []\n const step = { inputs: ins, at: r.eventTime, tx: d.bizTransactionList?.[0]?.bizTransaction }\n for (const o of outs) {\n const list = madeBy.get(o) ?? []\n list.push(step)\n madeBy.set(o, list)\n }\n }\n\n /* 거슬러 올라간다 — 너비 우선, 지나온 것은 다시 파지 않는다(사이클·공유 부품). */\n const items: any[] = [{ epc, itemRef: itemRefOf(epc), depth: 0 }]\n const edges: any[] = []\n const seen = new Set<string>([epc])\n let depthTruncated = false\n let frontier = [{ epc, depth: 0 }]\n while (frontier.length) {\n const next: typeof frontier = []\n for (const cur of frontier) {\n if (cur.depth >= depthCap) { depthTruncated = true; continue }\n for (const step of madeBy.get(cur.epc) ?? []) {\n for (const input of step.inputs) {\n edges.push({ from: cur.epc, to: input, at: step.at, tx: step.tx, depth: cur.depth + 1 })\n if (seen.has(input)) continue\n seen.add(input)\n items.push({ epc: input, itemRef: itemRefOf(input), depth: cur.depth + 1 })\n next.push({ epc: input, depth: cur.depth + 1 })\n }\n }\n }\n frontier = next\n }\n\n /*\n * **겹쳐 본다.** 이 개체의 품목이 어느 레시피의 산출이면, 그 레시피가 선언한 투입과 실제로\n * 소비된 것을 자재별로 나란히 센다.\n *\n * 개체(EPC)와 자재 키를 잇는 것은 `productionSpec.binding`(자재 키 → GS1 품목 참조)이다.\n * 바인딩이 없으면 **비교하지 않는다** — 짐작으로 이으면 \"설계와 다르다\" 는 거짓 진단이 나오고,\n * 그건 아무 말도 안 하는 것보다 나쁘다.\n */\n const spec: any = (inst.model as any)?.productionSpec\n const binding: Record<string, string> = spec?.binding ?? {}\n const refToMaterial = new Map(Object.entries(binding).map(([mat, ref]) => [String(ref), mat]))\n /*\n * ── **화면은 끝마디로 묻는다** (2026-08-23) ─────────────────────────────────\n *\n * 서랍의 포커스 식별자는 클래스의 끝마디다(`net/class/product-M0000438`). 대응 표의 값은 전체\n * 식별자(`https://…/class/product-M0000438`)이므로 곧바로는 맞지 않고, 그래서 「선언된 자재가\n * 아닙니다」가 떴다 — 그 품목은 대응 표에 있었다.\n *\n * 끝마디로도 찾되 **하나뿐일 때만** 인정한다. 둘 이상이 같은 끝마디를 가지면 어느 것인지 모르므로\n * 아무 말도 하지 않는다 — 짐작으로 이으면 남의 레시피와 견주게 되고, 그것이 「설계와 다르다」는\n * 거짓 진단이 된다.\n */\n const materialOf = (e: string) => resolveByDeclaration(itemRefOf(e), refToMaterial) ?? null\n\n const myMaterial = materialOf(epc)\n const recipes: any[] = spec?.definition?.recipes ?? []\n\n /*\n * ── 「무엇으로 만들었나」는 **적혀 있다** (2026-09-05) ───────────────────────\n *\n * 예전에는 이 자리가 자재로 되짚어 `recipes.find(...)` 로 **먼저 걸리는 하나**를 골랐다. 배열\n * 순서가 답을 정한다는 뜻이고, 한 자재에 레시피가 둘이면 틀린 답이 확신에 찬 얼굴로 나온다.\n * 실제로 그런 자재가 있다 — `BOLT-M8` 에 `RCP-BOLT-STD` · `RCP-BOLT-FAST` 둘이다.\n *\n * 그런데 답은 이미 기록되어 있다. 만들 때 정해지는 사실이고 커널이 오더에 싣는다\n * (`FlowOrder.recipeKey`). 변환 사건은 그 오더를 거래 식별자로 가리키고(`bizTransactionList`),\n * 이 함수는 그 값을 이미 `tx` 로 꺼내 놓고도 쓰지 않았다.\n *\n * 세 자리가 다 있었고 마지막 연결만 없었다. 「가릴 수 없다」가 아니라 **안 읽고 있었다.**\n *\n * ── 규칙 셋 — 짐작하지 않는다 ───────────────────────────────────────────────\n * 오더가 말한다 그것을 쓴다. 판이 둘이든 열이든 상관없다\n * 오더가 말 안 하고\n * 후보가 하나 그것을 쓴다 — 고를 것이 없으면 짐작이 아니다\n * 후보가 둘 이상 **답하지 않는다.** 어느 것인지 모르는 채로 견주면 거짓 진단이 된다\n *\n * 마지막 줄이 이 함수가 식별자 되찾기에 이미 적용하던 규율이다(「후보가 둘 이상이면 답하지\n * 않는다」). 두 줄 위에서만 안 지키고 있었다.\n */\n const candidates = myMaterial\n ? recipes.filter((r: any) => (r.outputs ?? []).some((o: any) => o.material === myMaterial))\n : []\n\n /* 이 개체를 만든 변환이 가리키는 거래 — 그 오더가 레시피를 말한다. */\n const madeTx = (madeBy.get(epc) ?? []).map(s => s.tx).find(Boolean)\n const saidRecipeKey = madeTx ? await recipeKeyOfTransaction(domainId, instanceId, String(madeTx)) : undefined\n\n const chosen = chooseRecipe(recipes, candidates, saidRecipeKey)\n const recipe = chosen.recipe\n\n /*\n * ── 「모르는 개체」와 「대응이 없는 개체」는 다른 사실이다 (2026-08-23) ──────\n *\n * 화면이 「이 개체는 선언된 자재가 아닙니다(라인 내부의 중간 산물일 수 있습니다)」라고 말했는데,\n * 그 품목은 원본의 품목 선언에 **이름까지 있었다**(「냉동그린빈스」). 바인딩 표가 그 품목을 담지\n * 못한 것이고, 그것은 「중간 산물」과 전혀 다른 이야기다 — 사용자가 할 일도 다르다(원본을 볼 일이\n * 아니라 대응을 고칠 일이다).\n *\n * 그래서 개체가 가리키는 품목이 **모델의 품목 선언에 있는지** 함께 본다. 이름은 그 자리에 있으니\n * 함께 낸다(화면이 사람에게 무엇을 말하는지 알 수 있게).\n */\n const definitions: any[] = (inst.model as any)?.materialDefinitions ?? []\n const defByRef = new Map(definitions.filter(d => d?.id).map(d => [String(d.id), d]))\n /* 정의도 같은 문으로 되찾는다 — 규칙 하나, 후보가 둘 이상이면 답하지 않는다. */\n const myRef = itemRefOf(epc)\n const myDef = resolveByDeclaration(myRef, defByRef)\n\n let conformance: any = null\n if (!Object.keys(binding).length) {\n conformance = { comparable: false, reason: 'no-binding' }\n } else if (!myMaterial && myDef) {\n /* 원본은 이 품목을 안다 — 개체를 자재에 잇는 대응이 없어서 비교하지 못한다. */\n conformance = {\n comparable: false,\n reason: 'item-known-not-bound',\n ...(typeof myDef.name === 'string' && myDef.name ? { itemName: myDef.name } : {})\n }\n } else if (!myMaterial) {\n conformance = { comparable: false, reason: 'item-not-bound' }\n } else if (chosen.basis === 'said-but-undeclared') {\n /* 오더는 말했는데 그 레시피가 모델의 선언에 없다 — 원본과 선언이 어긋난 것이지 모르는 것이 아니다. */\n conformance = { comparable: false, reason: 'recipe-not-declared', recipe: saidRecipeKey }\n } else if (chosen.basis === 'ambiguous') {\n /* 짐작하지 않는다 — 어느 것으로 만들었는지 말해 주는 사실이 없다. 후보를 밝혀 사람이 판단하게 한다. */\n conformance = {\n comparable: false,\n reason: 'many-recipes-for-material',\n recipes: candidates.map((r: any) => r.key).sort()\n }\n } else if (!recipe) {\n conformance = { comparable: false, reason: 'no-recipe-for-material' }\n } else if (!(madeBy.get(epc) ?? []).length) {\n /*\n * ── **안 본 것을 「다르다」고 하지 않는다** (2026-09-18 · :3000 화면에서 확인) ─────────\n *\n * 이 개체를 만든 변환이 저널에 하나도 없으면 투입을 하나도 관측하지 못한 것이다. 그런데\n * 아래 셈은 그대로 돌아 관측을 전부 0 으로 적었고, 레시피가 요구한 자재 23 줄이 전부\n * 「안 맞는다」가 됐다 — 화면은 「레시피와 다릅니다」를 확신에 찬 얼굴로 그렸다. 실제로는\n * 원본이 변환을 아직 안 보냈을 뿐이다(`mes-miratek` 의 카메라 로트가 그 경우다).\n *\n * 이 파일의 규율이 이미 그것을 말한다 — 짐작으로 이으면 「설계와 다르다」는 거짓 진단이\n * 되고, 그건 아무 말도 안 하는 것보다 나쁘다. 관측이 **없는** 것도 같은 부류다.\n */\n conformance = { comparable: false, reason: 'made-not-observed', recipe: recipe.key }\n } else {\n /*\n * 관측: **바인딩된 자재까지 내려가 센다** — 중간 산물(WIP)은 통과한다.\n *\n * 다단 라인에서 완제품의 직접 투입은 원자재가 아니라 **WIP** 다: 원자재는 첫 공정에서\n * 소비되고 완제품은 마지막 공정에서 나온다. 그래서 직접 투입만 세면 레시피가 선언한 자재와\n * **영원히 안 맞는다**(검사가 이것을 잡았다).\n *\n * WIP 은 라우트의 내부 산물이고 선언된 자재가 아니다 — 레시피는 자재로 말하므로 비교도\n * 자재로 해야 한다. 바인딩에 있으면 거기서 멈추고, 없으면 그 위로 계속 따라간다.\n *\n * 다만 **다른 완제품까지 넘어가지 않는다**: 바인딩된 것을 만나면 멈추므로, 하위조립도\n * 자재로 선언돼 있으면 그 자리에서 세어진다(그 아래는 그 자재 자신의 레시피가 볼 일이다).\n */\n const observed = new Map<string, number>()\n let unbound = 0\n const parentsOf = (e: string) => edges.filter(x => x.from === e).map(x => x.to)\n const walked = new Set<string>([epc])\n let layer = parentsOf(epc)\n while (layer.length) {\n const deeper: string[] = []\n for (const e of layer) {\n if (walked.has(e)) continue\n walked.add(e)\n const mat = materialOf(e)\n if (mat) { observed.set(mat, (observed.get(mat) ?? 0) + 1); continue }\n const up = parentsOf(e)\n /* 바인딩에도 없고 더 올라갈 곳도 없다 — 정말 모르는 투입이다(수만 낸다). */\n if (!up.length) { unbound++; continue }\n deeper.push(...up)\n }\n layer = deeper\n }\n const declared = new Map<string, number>((recipe.inputs ?? []).map((i: any) => [i.material, i.qty ?? null]))\n const keys = [...new Set([...declared.keys(), ...observed.keys()])]\n conformance = {\n comparable: true,\n material: myMaterial,\n recipe: recipe.key,\n /* 그 레시피를 **어떻게 알았나** — 화면이 「기록으로 안다」와 「하나뿐이라 안다」를 구별할 수 있게. */\n recipeBasis: chosen.basis,\n /* 자재마다 선언·관측을 나란히. 어느 한쪽에만 있는 것도 사실이다(빠뜨리지 않는다). */\n lines: keys.map(m => ({\n material: m,\n declared: declared.has(m) ? declared.get(m) : null,\n observed: observed.get(m) ?? 0,\n matches: declared.get(m) === (observed.get(m) ?? 0)\n })),\n /* 바인딩에 없는 투입 — 설계에 없는 것이 들어갔을 수도, 바인딩이 덜 선언됐을 수도 있다.\n 어느 쪽인지 우리는 모르므로 **수만 낸다.** */\n unboundInputs: unbound\n }\n }\n\n return {\n instanceId,\n epc,\n itemRef: itemRefOf(epc),\n material: myMaterial,\n items,\n edges,\n conformance,\n /* 무엇을 근거로 답했나 — 창을 넘겨 끊겼으면 계보가 실제보다 얕게 보인다. */\n basis: { source: 'journal', transformationsRead: rows.length, truncated, depthTruncated, depthCap, windowCap }\n }\n }\n}\n\n/**\n * 그 거래를 가리키는 오더가 **어느 레시피로** 만들었다고 말하는가.\n *\n * ── 왜 거래 식별자로 묻나 ───────────────────────────────────────────────────\n * EPCIS 변환 사건에는 레시피가 없다 — 표준에 그 자리가 없고, 대신 생산 오더를 거래로 가리킨다\n * (`bizTransactionList`). 오더 사건은 `recipeKey` 와 그 거래 식별자를 **둘 다** 든다. 그래서 이\n * 두 칸이 EPCIS 에서 오더로 건너가는 다리다(§`twin-event-keys.ts`).\n *\n * ── 색인을 탄다 ─────────────────────────────────────────────────────────────\n * `ix_twin_event_9 (domain, instanceId, bizTransaction)` 로 좁힌다. 이 칸이 없던 때는 payload 를\n * 문자열로 훑어야 했고 32만 행에 3분이 걸렸다 — 화면이 기다릴 비용이 아니다.\n *\n * ── 못 찾으면 **답하지 않는다** ──────────────────────────────────────────────\n * 옛 행에는 이 칸이 비어 있다(과거 행은 채우지 않는다는 규율). 그때는 `undefined` 를 내고, 부르는\n * 쪽이 후보 수로 판단한다 — 여기서 아무거나 고르면 예전과 같은 결함이 된다.\n */\nasync function recipeKeyOfTransaction(\n domainId: string,\n instanceId: string,\n bizTransaction: string\n): Promise<string | undefined> {\n const row = await getRepository(TwinEvent)\n .createQueryBuilder('e')\n .where('e.domain = :domainId', { domainId })\n .andWhere('e.instanceId = :instanceId', { instanceId })\n .andWhere('e.bizTransaction = :tx', { tx: bizTransaction })\n .andWhere('e.eventType = :eventType', { eventType: 'order.status' })\n /* 어느 줄이든 같은 오더를 말하므로 하나면 된다 — 가장 이른 것이 그 오더가 선 순간이다. */\n .orderBy('e.eventTime', 'ASC')\n .limit(1)\n .getOne()\n\n const said = (row?.payload as any)?.data?.recipeKey\n return typeof said === 'string' && said ? said : undefined\n}\n"]}
|
|
@@ -109,7 +109,7 @@ let TwinModelItemQuery = class TwinModelItemQuery {
|
|
|
109
109
|
const repo = (0, shell_1.getRepository)(row.entity);
|
|
110
110
|
const where = { domain: { id: domainId }, instance: { id: inst.id } };
|
|
111
111
|
const [rows, total] = await repo.findAndCount({ where, order: row.order, skip, take });
|
|
112
|
-
|
|
112
|
+
const answer = {
|
|
113
113
|
axis,
|
|
114
114
|
label: info.label,
|
|
115
115
|
basis: 'rows',
|
|
@@ -119,22 +119,26 @@ let TwinModelItemQuery = class TwinModelItemQuery {
|
|
|
119
119
|
resourceClassLabels: this.classLabels(inst.model ?? {}),
|
|
120
120
|
items: rows.map((r) => this.rowItem(axis, r))
|
|
121
121
|
};
|
|
122
|
+
return answer;
|
|
122
123
|
}
|
|
123
124
|
if (row && q) {
|
|
124
125
|
/*
|
|
125
|
-
* 검색은 아직 DB 로 내리지 않았다. **그 사실을 밝힌다**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
126
|
+
* 검색은 아직 DB 로 내리지 않았다. **그 사실을 밝힌다** — 흉내로 덮으면 규모가 오는 날까지
|
|
127
|
+
* 아무도 이 한계를 모른다. 훑는 건수를 상한으로 막고, 상한에 닿았으면 잘렸다고 말한다
|
|
128
|
+
* (알리지 않고 자르지 않는다).
|
|
129
|
+
*
|
|
130
|
+
* 말하는 자리는 `scanned` · `truncated` 다. 근거(`basis`)는 그대로 `rows` 이고 — 값이 어디서
|
|
131
|
+
* 왔는지는 훑었든 DB 가 골랐든 같은 행이다 — 계약이 닫아 둔 다섯 밖으로 나가지 않는다.
|
|
128
132
|
*/
|
|
129
133
|
const SCAN_MAX = 5000;
|
|
130
134
|
const repo = (0, shell_1.getRepository)(row.entity);
|
|
131
135
|
const where = { domain: { id: domainId }, instance: { id: inst.id } };
|
|
132
136
|
const rows = await repo.find({ where, order: row.order, take: SCAN_MAX });
|
|
133
137
|
const hit = rows.filter((r) => matches(this.rowItem(axis, r), q));
|
|
134
|
-
|
|
138
|
+
const answer = {
|
|
135
139
|
axis,
|
|
136
140
|
label: info.label,
|
|
137
|
-
basis: 'rows
|
|
141
|
+
basis: 'rows',
|
|
138
142
|
total: hit.length,
|
|
139
143
|
offset: skip,
|
|
140
144
|
resourceClassLabels: this.classLabels(inst.model ?? {}),
|
|
@@ -145,6 +149,7 @@ let TwinModelItemQuery = class TwinModelItemQuery {
|
|
|
145
149
|
return { items, names: (0, name_index_js_1.namesFor)(items, (0, name_index_js_1.nameIndex)(inst.model ?? {})) };
|
|
146
150
|
})()
|
|
147
151
|
};
|
|
152
|
+
return answer;
|
|
148
153
|
}
|
|
149
154
|
/*
|
|
150
155
|
* **선언이 아닌 축** — 일정·실적(ISA-95 Part 4). 이 축의 집은 트윈 모델이 아니라 **커널 상태**다
|