@things-factory/headless-twin 10.1.8 → 10.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist-server/migrations/1786800000000-CreateTwinReferenceReconciliations.d.ts +5 -0
  2. package/dist-server/migrations/1786800000000-CreateTwinReferenceReconciliations.js +44 -0
  3. package/dist-server/migrations/1786800000000-CreateTwinReferenceReconciliations.js.map +1 -0
  4. package/dist-server/migrations/index.js +3 -1
  5. package/dist-server/migrations/index.js.map +1 -1
  6. package/dist-server/service/index.d.ts +4 -2
  7. package/dist-server/service/index.js +2 -0
  8. package/dist-server/service/index.js.map +1 -1
  9. package/dist-server/service/reference/index.d.ts +5 -2
  10. package/dist-server/service/reference/index.js +5 -2
  11. package/dist-server/service/reference/index.js.map +1 -1
  12. package/dist-server/service/reference/reference-fill-run.js +13 -0
  13. package/dist-server/service/reference/reference-fill-run.js.map +1 -1
  14. package/dist-server/service/reference/reference-reconciliation-query.d.ts +5 -0
  15. package/dist-server/service/reference/reference-reconciliation-query.js +42 -0
  16. package/dist-server/service/reference/reference-reconciliation-query.js.map +1 -0
  17. package/dist-server/service/reference/reference-reconciliation-writer.d.ts +10 -0
  18. package/dist-server/service/reference/reference-reconciliation-writer.js +33 -0
  19. package/dist-server/service/reference/reference-reconciliation-writer.js.map +1 -0
  20. package/dist-server/service/reference/reference-reconciliation.d.ts +19 -0
  21. package/dist-server/service/reference/reference-reconciliation.js +33 -0
  22. package/dist-server/service/reference/reference-reconciliation.js.map +1 -0
  23. package/dist-server/service/reference/twin-reference-reconciliation.d.ts +19 -0
  24. package/dist-server/service/reference/twin-reference-reconciliation.js +95 -0
  25. package/dist-server/service/reference/twin-reference-reconciliation.js.map +1 -0
  26. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +9 -0
  27. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +41 -0
  28. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  29. package/package.json +4 -4
  30. package/server/migrations/1786800000000-CreateTwinReferenceReconciliations.ts +39 -0
  31. package/server/migrations/index.ts +3 -1
  32. package/server/service/index.ts +2 -0
  33. package/server/service/reference/index.ts +5 -2
  34. package/server/service/reference/reference-fill-run.ts +15 -1
  35. package/server/service/reference/reference-reconciliation-query.ts +28 -0
  36. package/server/service/reference/reference-reconciliation-writer.ts +38 -0
  37. package/server/service/reference/reference-reconciliation.ts +55 -0
  38. package/server/service/reference/twin-reference-reconciliation.ts +27 -0
  39. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +38 -0
  40. package/test/reference-reconciliation.test.ts +39 -0
  41. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TwinReferenceReconciliation = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const typeorm_1 = require("typeorm");
6
+ const type_graphql_1 = require("type-graphql");
7
+ const shell_1 = require("@things-factory/shell");
8
+ /* 한 연결·Twin·단위의 최신 번호 대사 상태. 과거 추이는 ingest window가, 현재 조치 판단은 이 표가 답한다. */
9
+ let TwinReferenceReconciliation = class TwinReferenceReconciliation {
10
+ };
11
+ exports.TwinReferenceReconciliation = TwinReferenceReconciliation;
12
+ tslib_1.__decorate([
13
+ (0, typeorm_1.PrimaryGeneratedColumn)('uuid'),
14
+ (0, type_graphql_1.Field)(type => type_graphql_1.ID),
15
+ tslib_1.__metadata("design:type", String)
16
+ ], TwinReferenceReconciliation.prototype, "id", void 0);
17
+ tslib_1.__decorate([
18
+ (0, typeorm_1.ManyToOne)(type => shell_1.Domain),
19
+ (0, type_graphql_1.Field)(type => shell_1.Domain, { nullable: true }),
20
+ tslib_1.__metadata("design:type", shell_1.Domain)
21
+ ], TwinReferenceReconciliation.prototype, "domain", void 0);
22
+ tslib_1.__decorate([
23
+ (0, typeorm_1.RelationId)((e) => e.domain),
24
+ tslib_1.__metadata("design:type", String)
25
+ ], TwinReferenceReconciliation.prototype, "domainId", void 0);
26
+ tslib_1.__decorate([
27
+ (0, typeorm_1.Column)(),
28
+ (0, type_graphql_1.Field)(),
29
+ tslib_1.__metadata("design:type", String)
30
+ ], TwinReferenceReconciliation.prototype, "instanceId", void 0);
31
+ tslib_1.__decorate([
32
+ (0, typeorm_1.Column)(),
33
+ (0, type_graphql_1.Field)(),
34
+ tslib_1.__metadata("design:type", String)
35
+ ], TwinReferenceReconciliation.prototype, "scope", void 0);
36
+ tslib_1.__decorate([
37
+ (0, typeorm_1.Column)(),
38
+ (0, type_graphql_1.Field)(),
39
+ tslib_1.__metadata("design:type", String)
40
+ ], TwinReferenceReconciliation.prototype, "status", void 0);
41
+ tslib_1.__decorate([
42
+ (0, typeorm_1.Column)({ type: 'int', nullable: true }),
43
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { nullable: true }),
44
+ tslib_1.__metadata("design:type", Number)
45
+ ], TwinReferenceReconciliation.prototype, "sourceFirst", void 0);
46
+ tslib_1.__decorate([
47
+ (0, typeorm_1.Column)({ type: 'int', nullable: true }),
48
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { nullable: true }),
49
+ tslib_1.__metadata("design:type", Number)
50
+ ], TwinReferenceReconciliation.prototype, "sourceLast", void 0);
51
+ tslib_1.__decorate([
52
+ (0, typeorm_1.Column)({ type: 'int', nullable: true }),
53
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { nullable: true }),
54
+ tslib_1.__metadata("design:type", Number)
55
+ ], TwinReferenceReconciliation.prototype, "cursorSeq", void 0);
56
+ tslib_1.__decorate([
57
+ (0, typeorm_1.Column)({ type: 'int', nullable: true }),
58
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { nullable: true }),
59
+ tslib_1.__metadata("design:type", Number)
60
+ ], TwinReferenceReconciliation.prototype, "firstSeqSeen", void 0);
61
+ tslib_1.__decorate([
62
+ (0, typeorm_1.Column)({ type: 'int', nullable: true }),
63
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { nullable: true }),
64
+ tslib_1.__metadata("design:type", Number)
65
+ ], TwinReferenceReconciliation.prototype, "headMissing", void 0);
66
+ tslib_1.__decorate([
67
+ (0, typeorm_1.Column)({ type: 'int', nullable: true }),
68
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { nullable: true }),
69
+ tslib_1.__metadata("design:type", Number)
70
+ ], TwinReferenceReconciliation.prototype, "behind", void 0);
71
+ tslib_1.__decorate([
72
+ (0, typeorm_1.Column)({ nullable: true }),
73
+ (0, type_graphql_1.Field)({ nullable: true }),
74
+ tslib_1.__metadata("design:type", String)
75
+ ], TwinReferenceReconciliation.prototype, "reason", void 0);
76
+ tslib_1.__decorate([
77
+ (0, typeorm_1.Column)(),
78
+ (0, type_graphql_1.Field)(),
79
+ tslib_1.__metadata("design:type", Date)
80
+ ], TwinReferenceReconciliation.prototype, "checkedAt", void 0);
81
+ tslib_1.__decorate([
82
+ (0, typeorm_1.CreateDateColumn)(),
83
+ tslib_1.__metadata("design:type", Date)
84
+ ], TwinReferenceReconciliation.prototype, "createdAt", void 0);
85
+ tslib_1.__decorate([
86
+ (0, typeorm_1.UpdateDateColumn)(),
87
+ tslib_1.__metadata("design:type", Date)
88
+ ], TwinReferenceReconciliation.prototype, "updatedAt", void 0);
89
+ exports.TwinReferenceReconciliation = TwinReferenceReconciliation = tslib_1.__decorate([
90
+ (0, typeorm_1.Entity)(),
91
+ (0, typeorm_1.Index)('uq_twin_reference_reconciliation_0', (e) => [e.domain, e.instanceId, e.scope], { unique: true }),
92
+ (0, typeorm_1.Index)('ix_twin_reference_reconciliation_0', (e) => [e.domain, e.instanceId, e.checkedAt], { unique: false }),
93
+ (0, type_graphql_1.ObjectType)({ description: 'Latest durable sequence reconciliation for one connected source scope. It reports only what numbered source ranges can prove; it never substitutes replay-prone ingest-window counts for source facts.' })
94
+ ], TwinReferenceReconciliation);
95
+ //# sourceMappingURL=twin-reference-reconciliation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"twin-reference-reconciliation.js","sourceRoot":"","sources":["../../../server/service/reference/twin-reference-reconciliation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Column, CreateDateColumn, Entity, Index, ManyToOne, PrimaryGeneratedColumn, RelationId, UpdateDateColumn } from 'typeorm'\nimport { Field, ID, Int, ObjectType } from 'type-graphql'\nimport { Domain } from '@things-factory/shell'\n\n/* 한 연결·Twin·단위의 최신 번호 대사 상태. 과거 추이는 ingest window가, 현재 조치 판단은 이 표가 답한다. */\n@Entity()\n@Index('uq_twin_reference_reconciliation_0', (e: TwinReferenceReconciliation) => [e.domain, e.instanceId, e.scope], { unique: true })\n@Index('ix_twin_reference_reconciliation_0', (e: TwinReferenceReconciliation) => [e.domain, e.instanceId, e.checkedAt], { unique: false })\n@ObjectType({ description: 'Latest durable sequence reconciliation for one connected source scope. It reports only what numbered source ranges can prove; it never substitutes replay-prone ingest-window counts for source facts.' })\nexport class TwinReferenceReconciliation {\n @PrimaryGeneratedColumn('uuid') @Field(type => ID) readonly id: string\n @ManyToOne(type => Domain) @Field(type => Domain, { nullable: true }) domain?: Domain\n @RelationId((e: TwinReferenceReconciliation) => e.domain) domainId?: string\n @Column() @Field() instanceId: string\n @Column() @Field() scope: string\n @Column() @Field() status: 'aligned' | 'gap' | 'unknown'\n @Column({ type: 'int', nullable: true }) @Field(type => Int, { nullable: true }) sourceFirst?: number\n @Column({ type: 'int', nullable: true }) @Field(type => Int, { nullable: true }) sourceLast?: number\n @Column({ type: 'int', nullable: true }) @Field(type => Int, { nullable: true }) cursorSeq?: number\n @Column({ type: 'int', nullable: true }) @Field(type => Int, { nullable: true }) firstSeqSeen?: number\n @Column({ type: 'int', nullable: true }) @Field(type => Int, { nullable: true }) headMissing?: number\n @Column({ type: 'int', nullable: true }) @Field(type => Int, { nullable: true }) behind?: number\n @Column({ nullable: true }) @Field({ nullable: true }) reason?: string\n @Column() @Field() checkedAt: Date\n @CreateDateColumn() createdAt?: Date\n @UpdateDateColumn() updatedAt?: Date\n}\n"]}
@@ -45,6 +45,15 @@ export declare class TwinLifecycleMutation {
45
45
  * 그것을 「이름을 바꿨다」로 보고하면 거짓이다. 지우고 싶으면 그 뜻의 문이 따로 있어야 한다.
46
46
  */
47
47
  renameTwinInstance(instanceId: string, name: string, context: ResolverContext): Promise<any>;
48
+ /**
49
+ * 폐기 전에 사라질 것을 읽는다.
50
+ *
51
+ * 삭제는 레지스트리 행 하나만 없애는 일이 아니다. `TwinEngine.remove` 는 같은 트윈의 저널도
52
+ * purge 한다. 그래서 확인 창이 추측한 숫자를 쓰지 않고, **삭제와 같은 domain·instance 범위**를
53
+ * 직접 센다. 저널이 비어 있는 트윈도 `0` 과 null 기간을 돌려준다 — 없는 기록을 기간으로
54
+ * 꾸며내면 "아무것도 안 지워진다"와 "언제부터인지 모른다"가 섞인다.
55
+ */
56
+ twinDeletionPreview(instanceId: string, context: ResolverContext): Promise<any | null>;
48
57
  /** 삭제 — 정지 + 레지스트리 삭제 + 저널 purge(domain 스코프). */
49
58
  deleteTwinInstance(instanceId: string, context: ResolverContext): Promise<boolean>;
50
59
  /** 관리 목록 — 등록된 인스턴스 전체(kind/status/running/revision/노드·무버 수). 데이터 없으면 빈 배열. */
@@ -8,6 +8,7 @@ const index_js_1 = require("../../engine/index.js");
8
8
  /* 정책 읽기는 그 모듈에서 직접 가져온다 — 다른 소비처(`reference-resolver`)와 같은 길이다. */
9
9
  const restart_policy_js_1 = require("../../engine/restart-policy.js");
10
10
  const twin_instance_js_1 = require("../twin-instance/twin-instance.js");
11
+ const twin_event_js_1 = require("../twin-event/twin-event.js");
11
12
  /* 도메인 어휘 카탈로그(시스템별 노드 타입) — 커널이 SSOT. 앱/UI 는 이걸 소싱하고 재선언하지 않는다(방언 금지).
12
13
  * 조합은 domain-catalog.ts 가 소유 — 리졸버와 서버측 소비자(board-ai 도구 등)가 같은 형태를 본다. */
13
14
  const domain_catalog_js_1 = require("./domain-catalog.js");
@@ -120,6 +121,35 @@ let TwinLifecycleMutation = class TwinLifecycleMutation {
120
121
  /* 도는 런타임은 이름을 쓰지 않는다(식별자로 돈다) — 그래서 정지·기동이 필요하지 않다. */
121
122
  return { instanceId, name: next, previous: before || null, changed: true };
122
123
  }
124
+ /**
125
+ * 폐기 전에 사라질 것을 읽는다.
126
+ *
127
+ * 삭제는 레지스트리 행 하나만 없애는 일이 아니다. `TwinEngine.remove` 는 같은 트윈의 저널도
128
+ * purge 한다. 그래서 확인 창이 추측한 숫자를 쓰지 않고, **삭제와 같은 domain·instance 범위**를
129
+ * 직접 센다. 저널이 비어 있는 트윈도 `0` 과 null 기간을 돌려준다 — 없는 기록을 기간으로
130
+ * 꾸며내면 "아무것도 안 지워진다"와 "언제부터인지 모른다"가 섞인다.
131
+ */
132
+ async twinDeletionPreview(instanceId, context) {
133
+ const domainId = context.state.domain.id;
134
+ const instance = await (0, shell_1.getRepository)(twin_instance_js_1.TwinInstance).findOne({ where: { domain: { id: domainId }, instanceId } });
135
+ if (!instance)
136
+ return null;
137
+ const journal = await (0, shell_1.getRepository)(twin_event_js_1.TwinEvent)
138
+ .createQueryBuilder('event')
139
+ .select('COUNT(event.id)', 'count')
140
+ .addSelect('MIN(event.eventTime)', 'firstEventAt')
141
+ .addSelect('MAX(event.eventTime)', 'lastEventAt')
142
+ .where('event.domain = :domainId', { domainId })
143
+ .andWhere('event.instanceId = :instanceId', { instanceId })
144
+ .getRawOne();
145
+ return {
146
+ instanceId,
147
+ name: instance.name ?? null,
148
+ journalCount: Number(journal?.count ?? 0),
149
+ firstEventAt: journal?.firstEventAt ?? null,
150
+ lastEventAt: journal?.lastEventAt ?? null
151
+ };
152
+ }
123
153
  /** 삭제 — 정지 + 레지스트리 삭제 + 저널 purge(domain 스코프). */
124
154
  async deleteTwinInstance(instanceId, context) {
125
155
  await index_js_1.TwinEngine.remove(context.state.domain.id, instanceId);
@@ -235,6 +265,17 @@ tslib_1.__decorate([
235
265
  tslib_1.__metadata("design:paramtypes", [String, String, Object]),
236
266
  tslib_1.__metadata("design:returntype", Promise)
237
267
  ], TwinLifecycleMutation.prototype, "renameTwinInstance", null);
268
+ tslib_1.__decorate([
269
+ (0, type_graphql_1.Query)(returns => shell_1.ScalarObject, {
270
+ nullable: true,
271
+ description: 'Describe what deleting one twin would purge before it is deleted: its registry identity and the count plus first/last event time of its journal. Null means this twin is not in the current domain.'
272
+ }),
273
+ tslib_1.__param(0, (0, type_graphql_1.Arg)('instanceId')),
274
+ tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
275
+ tslib_1.__metadata("design:type", Function),
276
+ tslib_1.__metadata("design:paramtypes", [String, Object]),
277
+ tslib_1.__metadata("design:returntype", Promise)
278
+ ], TwinLifecycleMutation.prototype, "twinDeletionPreview", null);
238
279
  tslib_1.__decorate([
239
280
  (0, type_graphql_1.Directive)('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)'),
240
281
  (0, type_graphql_1.Mutation)(returns => Boolean, { description: 'Delete a twin instance: stop it, remove the registry entry and purge its event journal.' }),
@@ -1 +1 @@
1
- {"version":3,"file":"twin-lifecycle-mutation.js","sourceRoot":"","sources":["../../../server/service/twin-lifecycle/twin-lifecycle-mutation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Mutation, Query, Resolver, Directive} from 'type-graphql'\n\nimport { ScalarObject, getRepository } from '@things-factory/shell'\n\nimport { TwinEngine } from '../../engine/index.js'\n/* 정책 읽기는 그 모듈에서 직접 가져온다 — 다른 소비처(`reference-resolver`)와 같은 길이다. */\nimport { readRestartPolicy } from '../../engine/restart-policy.js'\nimport { TwinInstance } from '../twin-instance/twin-instance.js'\n\nimport type { TwinModelDef } from '@operato/ops-contract'\n\n/* 도메인 어휘 카탈로그(시스템별 노드 타입) — 커널이 SSOT. 앱/UI 는 이걸 소싱하고 재선언하지 않는다(방언 금지).\n * 조합은 domain-catalog.ts 가 소유 — 리졸버와 서버측 소비자(board-ai 도구 등)가 같은 형태를 본다. */\nimport { domainCatalog } from './domain-catalog.js'\n\n/*\n * 트윈 인스턴스 생명주기 채널 — 프로비저닝(상향, ADR-0015).\n * State/Command/Scenario 는 이미 running 인 인스턴스를 다루지만, 이 채널은 인스턴스 자체를 만들고/설정하고/없앤다.\n * 생명주기: provision(save, 미기동) → start → stop → delete. 모두 domain 스코프(context.state.domain.id).\n * model(TwinModelDef+layout)=구조, scenario=자극 은 분리 — 여기선 구조만 세운다. 자극은 **원본이 선언하고**\n * (`TwinReference.connectionConfig.scenario`) 기동이 싣거나, 원본의 구동 능력으로 태운다(ADR-0029 §4·§8).\n */\n@Resolver()\nexport class TwinLifecycleMutation {\n /*\n * `saveTwinInstance` 는 **없앴다**(2026-08-08, ADR-0028).\n *\n * 트윈의 구조를 임의의 model 로 덮어쓰는 문이었고, 유일한 소비자는 손저작 편집기였다.\n * 구조는 원본에서 온다 — 템플릿(`createFromTemplate`)이나 커넥터(`ingestMaster`)가 그 길이다.\n * 문을 열어 두면 화면만 지워도 다음 소비자가 다시 생긴다.\n *\n * 시뮬 트윈을 세우는 것은 여전히 가능하다. 다만 그 입구는 **원본을 만드는 쪽**이지\n * 트윈을 고치는 쪽이 아니다(ADR-0029 — 시뮬레이터는 원본의 한 종류).\n */\n\n /** 기동 — model 를 주면 provision+start, 없으면 레지스트리에 저장된 구조로 start. */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => Boolean, {\n description: 'Start a twin runtime instance and begin the worker tick. If model is given it provisions first; otherwise it starts from the saved registry model.'\n })\n async startTwinInstance(\n @Arg('instanceId') instanceId: string,\n @Ctx() context: ResolverContext,\n @Arg('kind', { nullable: true }) kind?: string,\n @Arg('model', type => ScalarObject, { nullable: true }) model?: any,\n /** 새 시뮬레이션은 재기동 때 저널을 이어야 한다. 엔진에는 이미 필수지만 이 API가 누락해\n * zero-to-twin 경로가 기동 단계에서 막혔다. */\n @Arg('restartPolicy', { nullable: true }) restartPolicy?: 'resync' | 'resume' | 'reset'\n ): Promise<boolean> {\n const domainId = context.state.domain.id\n // 시각 기준은 **공간**이 갖는다(테넌트 아님) — 교대의 HH:MM 을 어느 기준으로 읽나.\n if (model) {\n /* model 를 함께 주는 것은 프로비저닝이다 — 그러면 종류도 말해야 한다.\n 예전에는 `kind ?? 'wms'` 로 메웠는데, 종류를 빠뜨린 야드/생산 트윈이 **창고로 알리지 않고 기동**했다. */\n if (!kind) throw new Error('kind is required when model is given — it decides which domain kernel runs this twin')\n TwinEngine.start(instanceId, domainId, kind, await TwinEngine.withSpaceTimeBase(model as TwinModelDef, domainId), restartPolicy)\n }\n else await TwinEngine.startFromRegistry(domainId, instanceId)\n return true\n }\n\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => Boolean, { description: 'Stop a running twin runtime instance by its id (only if it belongs to the current domain).' })\n async stopTwinInstance(@Arg('instanceId') instanceId: string, @Ctx() context: ResolverContext): Promise<boolean> {\n if (!TwinEngine.owns(context.state.domain.id, instanceId)) return false // 테넌트 격리 — 남의 도메인 트윈 정지 차단\n await TwinEngine.stop(context.state.domain.id, instanceId)\n return true\n }\n\n /**\n * 이 트윈이 **자기 과거를 어떻게 대할 것인가**를 다시 선언한다 (2026-08-20).\n *\n * ── 왜 이 문이 필요한가 ────────────────────────────────────────────────────\n * `restartPolicy` 는 사람이 하는 선언인데, 만들 때 한 번 정하면 **고칠 길이 없었다.** 그래서 「씨앗\n * 재현용」으로 세운 트윈이 나중에 사람들이 이력을 보는 트윈이 되어도 선언은 `reset` 으로 남았다.\n * 선언과 쓰임이 어긋난 채로 두면, 선언대로 구현하는 순간(저널 초기화) 이력을 잃는다.\n *\n * ── 지금 도는 것에는 손대지 않는다 ─────────────────────────────────────────\n * 이 값은 **다음 기동**의 규칙이다. 돌고 있는 런타임의 구동 방식을 바꾸려면 정지·기동이 필요하고,\n * 그 판단은 사람의 것이다(도는 트윈을 알리지 않고 갈아 끼우면 그 사이의 사실이 어디로 갔는지 알 수 없다).\n * 그래서 결과에 「지금 도는 것과 다르다」는 사실을 함께 낸다.\n */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, {\n description:\n \"Re-declare how a twin treats its own past on the next boot: 'resync' (re-read from the source), 'resume' (carry its own journal forward) or 'reset' (reproduce from the seed, journal cleared). The value is read strictly — an unknown or legacy word is refused with the accepted words named, because a silent default would decide whether a twin's history survives. This changes the declaration only: a running runtime keeps the mode it was started with, and the result says so (appliesAtNextStart) rather than swapping it underneath.\"\n })\n async setTwinRestartPolicy(\n @Arg('instanceId') instanceId: string,\n @Arg('restartPolicy') restartPolicy: string,\n @Ctx() context: ResolverContext\n ): Promise<any> {\n const domainId = context.state.domain.id\n /* 값은 **엄격히** 읽는다 — 모르는 말을 기본값으로 메우면 그 트윈의 이력이 남을지가 알리지 않고 결정된다. */\n const policy = readRestartPolicy(restartPolicy, `twin \"${instanceId}\"`)\n const repo = getRepository(TwinInstance)\n const row = await repo.findOne({ where: { domain: { id: domainId }, instanceId } })\n if (!row) throw new Error(`unknown twin \"${instanceId}\" in this domain — nothing was changed`)\n\n const before = row.restartPolicy\n if (before === policy) return { instanceId, restartPolicy: policy, changed: false, appliesAtNextStart: false }\n\n await repo.update({ id: row.id }, { restartPolicy: policy })\n /* 도는 런타임의 구동 방식은 그대로다 — 그 사실을 숨기지 않는다. */\n const running = !!TwinEngine.metrics(domainId, instanceId)\n return { instanceId, restartPolicy: policy, previous: before, changed: true, appliesAtNextStart: running }\n }\n\n /**\n * 트윈의 **이름을 다시 붙인다** (2026-08-26).\n *\n * ── 왜 이 문이 필요한가 ────────────────────────────────────────────────────\n * 이름은 트윈을 처음 세울 때 한 번 정해지고(`provision`), 다시 읽기는 그것을 건드리지 않는다. 그\n * 자체는 안전하다 — 사람이 붙인 이름을 연결이 덮어쓰면 안 된다. 그런데 그래서 **첫날의 이름에\n * 갇힌다.**\n *\n * 실제로 그런 일이 났다: 태양광 발전소를 붙일 때 커넥터가 아는 이름이 연결된 시스템의 코드\n * (`ES202307202202`)뿐이었다. 뒤에 사람이 부르는 이름(`호현에너지`)이 어디 있는지 알아내 커넥터를\n * 고쳤는데, 트윈 이름은 그대로였다. **연결된 시스템을 따라가는 트윈에서는 이름이 나중에 좋아진다** —\n * 커넥터가 나아질 때마다 이 일이 난다.\n *\n * 그때 남은 유일한 길이 트윈을 지우고 다시 세우는 것이었다. 그러면 지난 기록이 사라진다 — **이름\n * 하나 때문에 사실을 버리는 선택**이 된다. 그래서 이름만 바꾸는 문을 낸다.\n *\n * ── 연결이 따라오게 하지 않는다 ────────────────────────────────────────────\n * 다시 읽기가 이름을 따라가게 하면 사람이 붙인 이름을 덮어쓴다. 「사람이 손대지 않았으면 따라간다」가\n * 옳아 보이지만, 그러려면 「손댔다」는 표시를 새로 만들어야 하고 그것은 이름 하나 때문에 지기에 무거운\n * 빚이다. 사람이 바꾸는 길만 낸다.\n *\n * ── 빈 이름은 받지 않는다 ──────────────────────────────────────────────────\n * 이름을 지우는 것과 바꾸는 것은 다른 일이다. 빈 값을 받으면 그 트윈은 목록에서 식별자만 남는데,\n * 그것을 「이름을 바꿨다」로 보고하면 거짓이다. 지우고 싶으면 그 뜻의 문이 따로 있어야 한다.\n */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, {\n description:\n 'Rename a twin instance. The name is set when the twin is first provisioned and a re-read never touches it, ' +\n 'so a twin stays with the name its connector knew on day one — often a code from the connected system. ' +\n 'This changes the name only; nothing about the twin data, its journal or its runtime is affected. ' +\n 'An empty name is refused: clearing a name and changing it are different acts.'\n })\n async renameTwinInstance(\n @Arg('instanceId') instanceId: string,\n @Arg('name') name: string,\n @Ctx() context: ResolverContext\n ): Promise<any> {\n const domainId = context.state.domain.id\n const next = String(name ?? '').trim()\n if (!next) throw new Error(`a twin name cannot be empty — \"${instanceId}\" was left as it was`)\n\n const repo = getRepository(TwinInstance)\n const row = await repo.findOne({ where: { domain: { id: domainId }, instanceId } })\n if (!row) throw new Error(`unknown twin \"${instanceId}\" in this domain — nothing was changed`)\n\n const before = row.name ?? ''\n if (before === next) return { instanceId, name: next, changed: false }\n\n await repo.update({ id: row.id }, { name: next, updater: context.state.user })\n /* 도는 런타임은 이름을 쓰지 않는다(식별자로 돈다) — 그래서 정지·기동이 필요하지 않다. */\n return { instanceId, name: next, previous: before || null, changed: true }\n }\n\n /** 삭제 — 정지 + 레지스트리 삭제 + 저널 purge(domain 스코프). */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => Boolean, { description: 'Delete a twin instance: stop it, remove the registry entry and purge its event journal.' })\n async deleteTwinInstance(@Arg('instanceId') instanceId: string, @Ctx() context: ResolverContext): Promise<boolean> {\n await TwinEngine.remove(context.state.domain.id, instanceId)\n return true\n }\n\n /** 관리 목록 — 등록된 인스턴스 전체(kind/status/running/revision/노드·무버 수). 데이터 없으면 빈 배열. */\n /**\n * **이 공간의 트윈 id 들** — 한 현실을 여러 렌즈가 비추므로 공간을 물으면 그 렌즈 전부를 답한다.\n *\n * 화면이 전체 목록을 받아 걸러 쓰던 규칙을 서버로 모은다. 예측 화면은 같은 확장을 한 파일에서\n * **두 번** 복제하고 있었고, 규칙이 흩어지면 한쪽만 고쳐진다. 떠 있지 않은 것도 포함한다 —\n * 공간에 무엇이 있는지는 기동 여부와 다른 사실이다.\n */\n @Query(returns => [String], { description: 'Ids of the twins that live in a space (all its lenses), running or not.' })\n async twinSpaceInstanceIds(@Arg('spaceId') spaceId: string, @Ctx() context: ResolverContext): Promise<string[]> {\n return TwinEngine.instanceIdsOfSpace(context.state.domain.id, spaceId)\n }\n\n @Query(returns => [ScalarObject], {\n description:\n 'List provisioned twin instances in the current domain with management metadata. Each row also carries `sync`: a one-line answer to \"is this twin keeping in step with the floor?\" — a verdict (syncing | mapping-blocked | no-traffic | not-connected | not-applicable | unknown), the accepted share of the last closed 10-minute window, and up to 36 closed windows of offered/rejected counts for a sparkline. Reasons and rejected-record samples are deliberately absent here; ask twinIngestHealth for one twin when you need them, because carrying raw records for every row would make this list heavy for no gain. acceptedRatio is null while the first window is still filling — a partially filled window under-counts, and drawing it as zero would read as \"nothing passed\".'\n })\n async twinInstanceList(@Ctx() context: ResolverContext): Promise<any[]> {\n const domainId = context.state.domain.id\n const list = await TwinEngine.list(domainId)\n /*\n * 동기화 요약을 **같은 응답에 얹는다.** 따로 질의하면 트윈 수만큼 왕복이 생기고(N+1), 그 사이 화면은\n * 판정 없는 목록을 먼저 그린다 — 사람이 그 순간을 「정상」으로 읽는다.\n */\n const brief = new Map(\n TwinEngine.ingestHealthBrief(\n domainId,\n list.map((x: any) => x.instanceId)\n ).map(b => [b.instanceId, b])\n )\n /*\n * **쓰기 사실도 같은 응답에 얹는다** (2026-08-20).\n *\n * 「이 트윈이 사실을 적고 있나」는 운영자가 트윈 화면에서 묻는 질문인데, 그 답은 벤치(슈퍼유저)의\n * 부하 계기판에만 있었다. 유입(`sync`)과 나란히 두는 이유는 둘이 한 사슬이기 때문이다 — 받은 것이\n * 있어도 적히지 않으면 저널이 비고, 그러면 성과·시간여행이 함께 빈다.\n *\n * 계기는 메모리라 왕복이 없다. **도는 트윈만** 답이 있고, 멈춘 트윈은 `null` 이다(0 이 아니다 —\n * 「쓰지 않았다」와 「돌지 않는다」는 다른 사실이다).\n */\n return list.map((x: any) => {\n const m = TwinEngine.metrics(domainId, x.instanceId)\n return {\n ...x,\n sync: brief.get(x.instanceId) ?? null,\n write: m ? { rate: m.journalRate, journaled: m.journaledTotal, backlog: m.backlog } : null\n }\n })\n }\n\n /** 단건 상세 — 프로비저닝 에디터용 model(구조+layout) 포함. */\n @Query(returns => ScalarObject, { nullable: true, description: 'Get one provisioned twin instance with its TwinModelDef (structure + layout) for editing.' })\n async twinInstanceDetail(@Arg('instanceId') instanceId: string, @Ctx() context: ResolverContext): Promise<any | null> {\n return TwinEngine.detail(context.state.domain.id, instanceId)\n }\n\n /**\n * 라이브(메모리) 인스턴스 id 목록 — 콘솔 셀렉터·포트폴리오용. **현재 도메인만**(테넌트 격리).\n * 과거엔 Object.keys(TwinEngine.instances) 로 전역 반환 → 다른 도메인 트윈이 종합현황/셀렉터에 새던 버그.\n * 도메인 스코프 목록(twinInstanceList 와 동일 소스)에서 running 만 추린다.\n */\n @Query(returns => [String], { description: 'List ids of running (in-memory) twin runtime instances in the current domain (tenant-isolated).' })\n async twinInstances(@Ctx() context: ResolverContext): Promise<string[]> {\n const list = await TwinEngine.list(context.state.domain.id)\n return list.filter((x: any) => x.running).map((x: any) => x.instanceId)\n }\n\n /**\n * 도메인 어휘 카탈로그 — 커널(레퍼런스) SSOT 를 그대로 노출. { wms:{label,locationTypes[]}, yms:…, mes:… }.\n * 프로비저닝 UI 는 이걸 소싱해 노드 타입/시스템을 채운다(앱 내 하드코딩 금지).\n */\n @Query(returns => ScalarObject, { description: 'Domain vocabulary catalog (per-system location location types) sourced from the twin kernel — SSOT for provisioning UI.' })\n twinDomainCatalog(): any {\n return domainCatalog()\n }\n}\n"]}
1
+ {"version":3,"file":"twin-lifecycle-mutation.js","sourceRoot":"","sources":["../../../server/service/twin-lifecycle/twin-lifecycle-mutation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Mutation, Query, Resolver, Directive} from 'type-graphql'\n\nimport { ScalarObject, getRepository } from '@things-factory/shell'\n\nimport { TwinEngine } from '../../engine/index.js'\n/* 정책 읽기는 그 모듈에서 직접 가져온다 — 다른 소비처(`reference-resolver`)와 같은 길이다. */\nimport { readRestartPolicy } from '../../engine/restart-policy.js'\nimport { TwinInstance } from '../twin-instance/twin-instance.js'\nimport { TwinEvent } from '../twin-event/twin-event.js'\n\nimport type { TwinModelDef } from '@operato/ops-contract'\n\n/* 도메인 어휘 카탈로그(시스템별 노드 타입) — 커널이 SSOT. 앱/UI 는 이걸 소싱하고 재선언하지 않는다(방언 금지).\n * 조합은 domain-catalog.ts 가 소유 — 리졸버와 서버측 소비자(board-ai 도구 등)가 같은 형태를 본다. */\nimport { domainCatalog } from './domain-catalog.js'\n\n/*\n * 트윈 인스턴스 생명주기 채널 — 프로비저닝(상향, ADR-0015).\n * State/Command/Scenario 는 이미 running 인 인스턴스를 다루지만, 이 채널은 인스턴스 자체를 만들고/설정하고/없앤다.\n * 생명주기: provision(save, 미기동) → start → stop → delete. 모두 domain 스코프(context.state.domain.id).\n * model(TwinModelDef+layout)=구조, scenario=자극 은 분리 — 여기선 구조만 세운다. 자극은 **원본이 선언하고**\n * (`TwinReference.connectionConfig.scenario`) 기동이 싣거나, 원본의 구동 능력으로 태운다(ADR-0029 §4·§8).\n */\n@Resolver()\nexport class TwinLifecycleMutation {\n /*\n * `saveTwinInstance` 는 **없앴다**(2026-08-08, ADR-0028).\n *\n * 트윈의 구조를 임의의 model 로 덮어쓰는 문이었고, 유일한 소비자는 손저작 편집기였다.\n * 구조는 원본에서 온다 — 템플릿(`createFromTemplate`)이나 커넥터(`ingestMaster`)가 그 길이다.\n * 문을 열어 두면 화면만 지워도 다음 소비자가 다시 생긴다.\n *\n * 시뮬 트윈을 세우는 것은 여전히 가능하다. 다만 그 입구는 **원본을 만드는 쪽**이지\n * 트윈을 고치는 쪽이 아니다(ADR-0029 — 시뮬레이터는 원본의 한 종류).\n */\n\n /** 기동 — model 를 주면 provision+start, 없으면 레지스트리에 저장된 구조로 start. */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => Boolean, {\n description: 'Start a twin runtime instance and begin the worker tick. If model is given it provisions first; otherwise it starts from the saved registry model.'\n })\n async startTwinInstance(\n @Arg('instanceId') instanceId: string,\n @Ctx() context: ResolverContext,\n @Arg('kind', { nullable: true }) kind?: string,\n @Arg('model', type => ScalarObject, { nullable: true }) model?: any,\n /** 새 시뮬레이션은 재기동 때 저널을 이어야 한다. 엔진에는 이미 필수지만 이 API가 누락해\n * zero-to-twin 경로가 기동 단계에서 막혔다. */\n @Arg('restartPolicy', { nullable: true }) restartPolicy?: 'resync' | 'resume' | 'reset'\n ): Promise<boolean> {\n const domainId = context.state.domain.id\n // 시각 기준은 **공간**이 갖는다(테넌트 아님) — 교대의 HH:MM 을 어느 기준으로 읽나.\n if (model) {\n /* model 를 함께 주는 것은 프로비저닝이다 — 그러면 종류도 말해야 한다.\n 예전에는 `kind ?? 'wms'` 로 메웠는데, 종류를 빠뜨린 야드/생산 트윈이 **창고로 알리지 않고 기동**했다. */\n if (!kind) throw new Error('kind is required when model is given — it decides which domain kernel runs this twin')\n TwinEngine.start(instanceId, domainId, kind, await TwinEngine.withSpaceTimeBase(model as TwinModelDef, domainId), restartPolicy)\n }\n else await TwinEngine.startFromRegistry(domainId, instanceId)\n return true\n }\n\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => Boolean, { description: 'Stop a running twin runtime instance by its id (only if it belongs to the current domain).' })\n async stopTwinInstance(@Arg('instanceId') instanceId: string, @Ctx() context: ResolverContext): Promise<boolean> {\n if (!TwinEngine.owns(context.state.domain.id, instanceId)) return false // 테넌트 격리 — 남의 도메인 트윈 정지 차단\n await TwinEngine.stop(context.state.domain.id, instanceId)\n return true\n }\n\n /**\n * 이 트윈이 **자기 과거를 어떻게 대할 것인가**를 다시 선언한다 (2026-08-20).\n *\n * ── 왜 이 문이 필요한가 ────────────────────────────────────────────────────\n * `restartPolicy` 는 사람이 하는 선언인데, 만들 때 한 번 정하면 **고칠 길이 없었다.** 그래서 「씨앗\n * 재현용」으로 세운 트윈이 나중에 사람들이 이력을 보는 트윈이 되어도 선언은 `reset` 으로 남았다.\n * 선언과 쓰임이 어긋난 채로 두면, 선언대로 구현하는 순간(저널 초기화) 이력을 잃는다.\n *\n * ── 지금 도는 것에는 손대지 않는다 ─────────────────────────────────────────\n * 이 값은 **다음 기동**의 규칙이다. 돌고 있는 런타임의 구동 방식을 바꾸려면 정지·기동이 필요하고,\n * 그 판단은 사람의 것이다(도는 트윈을 알리지 않고 갈아 끼우면 그 사이의 사실이 어디로 갔는지 알 수 없다).\n * 그래서 결과에 「지금 도는 것과 다르다」는 사실을 함께 낸다.\n */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, {\n description:\n \"Re-declare how a twin treats its own past on the next boot: 'resync' (re-read from the source), 'resume' (carry its own journal forward) or 'reset' (reproduce from the seed, journal cleared). The value is read strictly — an unknown or legacy word is refused with the accepted words named, because a silent default would decide whether a twin's history survives. This changes the declaration only: a running runtime keeps the mode it was started with, and the result says so (appliesAtNextStart) rather than swapping it underneath.\"\n })\n async setTwinRestartPolicy(\n @Arg('instanceId') instanceId: string,\n @Arg('restartPolicy') restartPolicy: string,\n @Ctx() context: ResolverContext\n ): Promise<any> {\n const domainId = context.state.domain.id\n /* 값은 **엄격히** 읽는다 — 모르는 말을 기본값으로 메우면 그 트윈의 이력이 남을지가 알리지 않고 결정된다. */\n const policy = readRestartPolicy(restartPolicy, `twin \"${instanceId}\"`)\n const repo = getRepository(TwinInstance)\n const row = await repo.findOne({ where: { domain: { id: domainId }, instanceId } })\n if (!row) throw new Error(`unknown twin \"${instanceId}\" in this domain — nothing was changed`)\n\n const before = row.restartPolicy\n if (before === policy) return { instanceId, restartPolicy: policy, changed: false, appliesAtNextStart: false }\n\n await repo.update({ id: row.id }, { restartPolicy: policy })\n /* 도는 런타임의 구동 방식은 그대로다 — 그 사실을 숨기지 않는다. */\n const running = !!TwinEngine.metrics(domainId, instanceId)\n return { instanceId, restartPolicy: policy, previous: before, changed: true, appliesAtNextStart: running }\n }\n\n /**\n * 트윈의 **이름을 다시 붙인다** (2026-08-26).\n *\n * ── 왜 이 문이 필요한가 ────────────────────────────────────────────────────\n * 이름은 트윈을 처음 세울 때 한 번 정해지고(`provision`), 다시 읽기는 그것을 건드리지 않는다. 그\n * 자체는 안전하다 — 사람이 붙인 이름을 연결이 덮어쓰면 안 된다. 그런데 그래서 **첫날의 이름에\n * 갇힌다.**\n *\n * 실제로 그런 일이 났다: 태양광 발전소를 붙일 때 커넥터가 아는 이름이 연결된 시스템의 코드\n * (`ES202307202202`)뿐이었다. 뒤에 사람이 부르는 이름(`호현에너지`)이 어디 있는지 알아내 커넥터를\n * 고쳤는데, 트윈 이름은 그대로였다. **연결된 시스템을 따라가는 트윈에서는 이름이 나중에 좋아진다** —\n * 커넥터가 나아질 때마다 이 일이 난다.\n *\n * 그때 남은 유일한 길이 트윈을 지우고 다시 세우는 것이었다. 그러면 지난 기록이 사라진다 — **이름\n * 하나 때문에 사실을 버리는 선택**이 된다. 그래서 이름만 바꾸는 문을 낸다.\n *\n * ── 연결이 따라오게 하지 않는다 ────────────────────────────────────────────\n * 다시 읽기가 이름을 따라가게 하면 사람이 붙인 이름을 덮어쓴다. 「사람이 손대지 않았으면 따라간다」가\n * 옳아 보이지만, 그러려면 「손댔다」는 표시를 새로 만들어야 하고 그것은 이름 하나 때문에 지기에 무거운\n * 빚이다. 사람이 바꾸는 길만 낸다.\n *\n * ── 빈 이름은 받지 않는다 ──────────────────────────────────────────────────\n * 이름을 지우는 것과 바꾸는 것은 다른 일이다. 빈 값을 받으면 그 트윈은 목록에서 식별자만 남는데,\n * 그것을 「이름을 바꿨다」로 보고하면 거짓이다. 지우고 싶으면 그 뜻의 문이 따로 있어야 한다.\n */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, {\n description:\n 'Rename a twin instance. The name is set when the twin is first provisioned and a re-read never touches it, ' +\n 'so a twin stays with the name its connector knew on day one — often a code from the connected system. ' +\n 'This changes the name only; nothing about the twin data, its journal or its runtime is affected. ' +\n 'An empty name is refused: clearing a name and changing it are different acts.'\n })\n async renameTwinInstance(\n @Arg('instanceId') instanceId: string,\n @Arg('name') name: string,\n @Ctx() context: ResolverContext\n ): Promise<any> {\n const domainId = context.state.domain.id\n const next = String(name ?? '').trim()\n if (!next) throw new Error(`a twin name cannot be empty — \"${instanceId}\" was left as it was`)\n\n const repo = getRepository(TwinInstance)\n const row = await repo.findOne({ where: { domain: { id: domainId }, instanceId } })\n if (!row) throw new Error(`unknown twin \"${instanceId}\" in this domain — nothing was changed`)\n\n const before = row.name ?? ''\n if (before === next) return { instanceId, name: next, changed: false }\n\n await repo.update({ id: row.id }, { name: next, updater: context.state.user })\n /* 도는 런타임은 이름을 쓰지 않는다(식별자로 돈다) — 그래서 정지·기동이 필요하지 않다. */\n return { instanceId, name: next, previous: before || null, changed: true }\n }\n\n /**\n * 폐기 전에 사라질 것을 읽는다.\n *\n * 삭제는 레지스트리 행 하나만 없애는 일이 아니다. `TwinEngine.remove` 는 같은 트윈의 저널도\n * purge 한다. 그래서 확인 창이 추측한 숫자를 쓰지 않고, **삭제와 같은 domain·instance 범위**를\n * 직접 센다. 저널이 비어 있는 트윈도 `0` 과 null 기간을 돌려준다 — 없는 기록을 기간으로\n * 꾸며내면 \"아무것도 안 지워진다\"와 \"언제부터인지 모른다\"가 섞인다.\n */\n @Query(returns => ScalarObject, {\n nullable: true,\n description:\n 'Describe what deleting one twin would purge before it is deleted: its registry identity and the count plus first/last event time of its journal. Null means this twin is not in the current domain.'\n })\n async twinDeletionPreview(@Arg('instanceId') instanceId: string, @Ctx() context: ResolverContext): Promise<any | null> {\n const domainId = context.state.domain.id\n const instance = await getRepository(TwinInstance).findOne({ where: { domain: { id: domainId }, instanceId } })\n\n if (!instance) return null\n\n const journal = await getRepository(TwinEvent)\n .createQueryBuilder('event')\n .select('COUNT(event.id)', 'count')\n .addSelect('MIN(event.eventTime)', 'firstEventAt')\n .addSelect('MAX(event.eventTime)', 'lastEventAt')\n .where('event.domain = :domainId', { domainId })\n .andWhere('event.instanceId = :instanceId', { instanceId })\n .getRawOne<{ count?: string | number; firstEventAt?: unknown; lastEventAt?: unknown }>()\n\n return {\n instanceId,\n name: instance.name ?? null,\n journalCount: Number(journal?.count ?? 0),\n firstEventAt: journal?.firstEventAt ?? null,\n lastEventAt: journal?.lastEventAt ?? null\n }\n }\n\n /** 삭제 — 정지 + 레지스트리 삭제 + 저널 purge(domain 스코프). */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => Boolean, { description: 'Delete a twin instance: stop it, remove the registry entry and purge its event journal.' })\n async deleteTwinInstance(@Arg('instanceId') instanceId: string, @Ctx() context: ResolverContext): Promise<boolean> {\n await TwinEngine.remove(context.state.domain.id, instanceId)\n return true\n }\n\n /** 관리 목록 — 등록된 인스턴스 전체(kind/status/running/revision/노드·무버 수). 데이터 없으면 빈 배열. */\n /**\n * **이 공간의 트윈 id 들** — 한 현실을 여러 렌즈가 비추므로 공간을 물으면 그 렌즈 전부를 답한다.\n *\n * 화면이 전체 목록을 받아 걸러 쓰던 규칙을 서버로 모은다. 예측 화면은 같은 확장을 한 파일에서\n * **두 번** 복제하고 있었고, 규칙이 흩어지면 한쪽만 고쳐진다. 떠 있지 않은 것도 포함한다 —\n * 공간에 무엇이 있는지는 기동 여부와 다른 사실이다.\n */\n @Query(returns => [String], { description: 'Ids of the twins that live in a space (all its lenses), running or not.' })\n async twinSpaceInstanceIds(@Arg('spaceId') spaceId: string, @Ctx() context: ResolverContext): Promise<string[]> {\n return TwinEngine.instanceIdsOfSpace(context.state.domain.id, spaceId)\n }\n\n @Query(returns => [ScalarObject], {\n description:\n 'List provisioned twin instances in the current domain with management metadata. Each row also carries `sync`: a one-line answer to \"is this twin keeping in step with the floor?\" — a verdict (syncing | mapping-blocked | no-traffic | not-connected | not-applicable | unknown), the accepted share of the last closed 10-minute window, and up to 36 closed windows of offered/rejected counts for a sparkline. Reasons and rejected-record samples are deliberately absent here; ask twinIngestHealth for one twin when you need them, because carrying raw records for every row would make this list heavy for no gain. acceptedRatio is null while the first window is still filling — a partially filled window under-counts, and drawing it as zero would read as \"nothing passed\".'\n })\n async twinInstanceList(@Ctx() context: ResolverContext): Promise<any[]> {\n const domainId = context.state.domain.id\n const list = await TwinEngine.list(domainId)\n /*\n * 동기화 요약을 **같은 응답에 얹는다.** 따로 질의하면 트윈 수만큼 왕복이 생기고(N+1), 그 사이 화면은\n * 판정 없는 목록을 먼저 그린다 — 사람이 그 순간을 「정상」으로 읽는다.\n */\n const brief = new Map(\n TwinEngine.ingestHealthBrief(\n domainId,\n list.map((x: any) => x.instanceId)\n ).map(b => [b.instanceId, b])\n )\n /*\n * **쓰기 사실도 같은 응답에 얹는다** (2026-08-20).\n *\n * 「이 트윈이 사실을 적고 있나」는 운영자가 트윈 화면에서 묻는 질문인데, 그 답은 벤치(슈퍼유저)의\n * 부하 계기판에만 있었다. 유입(`sync`)과 나란히 두는 이유는 둘이 한 사슬이기 때문이다 — 받은 것이\n * 있어도 적히지 않으면 저널이 비고, 그러면 성과·시간여행이 함께 빈다.\n *\n * 계기는 메모리라 왕복이 없다. **도는 트윈만** 답이 있고, 멈춘 트윈은 `null` 이다(0 이 아니다 —\n * 「쓰지 않았다」와 「돌지 않는다」는 다른 사실이다).\n */\n return list.map((x: any) => {\n const m = TwinEngine.metrics(domainId, x.instanceId)\n return {\n ...x,\n sync: brief.get(x.instanceId) ?? null,\n write: m ? { rate: m.journalRate, journaled: m.journaledTotal, backlog: m.backlog } : null\n }\n })\n }\n\n /** 단건 상세 — 프로비저닝 에디터용 model(구조+layout) 포함. */\n @Query(returns => ScalarObject, { nullable: true, description: 'Get one provisioned twin instance with its TwinModelDef (structure + layout) for editing.' })\n async twinInstanceDetail(@Arg('instanceId') instanceId: string, @Ctx() context: ResolverContext): Promise<any | null> {\n return TwinEngine.detail(context.state.domain.id, instanceId)\n }\n\n /**\n * 라이브(메모리) 인스턴스 id 목록 — 콘솔 셀렉터·포트폴리오용. **현재 도메인만**(테넌트 격리).\n * 과거엔 Object.keys(TwinEngine.instances) 로 전역 반환 → 다른 도메인 트윈이 종합현황/셀렉터에 새던 버그.\n * 도메인 스코프 목록(twinInstanceList 와 동일 소스)에서 running 만 추린다.\n */\n @Query(returns => [String], { description: 'List ids of running (in-memory) twin runtime instances in the current domain (tenant-isolated).' })\n async twinInstances(@Ctx() context: ResolverContext): Promise<string[]> {\n const list = await TwinEngine.list(context.state.domain.id)\n return list.filter((x: any) => x.running).map((x: any) => x.instanceId)\n }\n\n /**\n * 도메인 어휘 카탈로그 — 커널(레퍼런스) SSOT 를 그대로 노출. { wms:{label,locationTypes[]}, yms:…, mes:… }.\n * 프로비저닝 UI 는 이걸 소싱해 노드 타입/시스템을 채운다(앱 내 하드코딩 금지).\n */\n @Query(returns => ScalarObject, { description: 'Domain vocabulary catalog (per-system location location types) sourced from the twin kernel — SSOT for provisioning UI.' })\n twinDomainCatalog(): any {\n return domainCatalog()\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/headless-twin",
3
- "version": "10.1.8",
3
+ "version": "10.1.11",
4
4
  "main": "dist-server/index.js",
5
5
  "things-factory": true,
6
6
  "author": "heartyoh <heartyoh@hatiolab.com>",
@@ -27,13 +27,13 @@
27
27
  "clean:shared": "rm -rf dist-shared tsconfig.shared.tsbuildinfo"
28
28
  },
29
29
  "dependencies": {
30
- "@operato/ops-contract": "^0.9.17",
31
- "@operato/twin-kernel": "^0.11.10",
30
+ "@operato/ops-contract": "^0.9.19",
31
+ "@operato/twin-kernel": "^0.11.11",
32
32
  "@things-factory/auth-base": "^10.1.7",
33
33
  "@things-factory/cache-service": "^10.1.7",
34
34
  "@things-factory/env": "^10.1.3",
35
35
  "@things-factory/ingest": "^10.1.7",
36
36
  "@things-factory/shell": "^10.1.7"
37
37
  },
38
- "gitHead": "c402982e29aab3b49e56bea4e41948be6a749c7f"
38
+ "gitHead": "3079d77730de84c80f8ac2ed8343d6f624a94ed2"
39
39
  }
@@ -0,0 +1,39 @@
1
+ import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm'
2
+
3
+ /* 번호 대사는 원본 범위를 실제로 읽은 뒤의 최신 상태를 재기동 뒤에도 보존한다. */
4
+ const TABLE = 'twin_reference_reconciliations'
5
+
6
+ export class CreateTwinReferenceReconciliations1786800000000 implements MigrationInterface {
7
+ public async up(queryRunner: QueryRunner): Promise<void> {
8
+ if (await queryRunner.hasTable(TABLE)) return
9
+ const dateType = queryRunner.connection.driver.mappedDataTypes.createDate as string
10
+ await queryRunner.createTable(new Table({
11
+ name: TABLE,
12
+ columns: [
13
+ { name: 'id', type: 'varchar', length: '36', isPrimary: true },
14
+ { name: 'domain_id', type: 'varchar', length: '36', isNullable: true },
15
+ { name: 'instance_id', type: 'varchar', isNullable: false },
16
+ { name: 'scope', type: 'varchar', isNullable: false },
17
+ { name: 'status', type: 'varchar', isNullable: false },
18
+ { name: 'source_first', type: 'int', isNullable: true },
19
+ { name: 'source_last', type: 'int', isNullable: true },
20
+ { name: 'cursor_seq', type: 'int', isNullable: true },
21
+ { name: 'first_seq_seen', type: 'int', isNullable: true },
22
+ { name: 'head_missing', type: 'int', isNullable: true },
23
+ { name: 'behind', type: 'int', isNullable: true },
24
+ { name: 'reason', type: 'varchar', isNullable: true },
25
+ { name: 'checked_at', type: dateType, isNullable: false },
26
+ { name: 'created_at', type: dateType, isNullable: true },
27
+ { name: 'updated_at', type: dateType, isNullable: true }
28
+ ]
29
+ }), true)
30
+ const table = await queryRunner.getTable(TABLE)
31
+ if (!table) return
32
+ await queryRunner.createIndex(table, new TableIndex({ name: 'uq_twin_reference_reconciliation_0', columnNames: ['domain_id', 'instance_id', 'scope'], isUnique: true }))
33
+ await queryRunner.createIndex(table, new TableIndex({ name: 'ix_twin_reference_reconciliation_0', columnNames: ['domain_id', 'instance_id', 'checked_at'] }))
34
+ }
35
+
36
+ public async down(queryRunner: QueryRunner): Promise<void> {
37
+ if (await queryRunner.hasTable(TABLE)) await queryRunner.dropTable(TABLE)
38
+ }
39
+ }
@@ -9,6 +9,7 @@ import { IndexEventTypeByTime1786300000000 } from './1786300000000-IndexEventTyp
9
9
  import { IndexEventCreatedAt1786400000000 } from './1786400000000-IndexEventCreatedAt.js'
10
10
  import { CreateTwinSubjectEvents1786500000000 } from './1786500000000-CreateTwinSubjectEvents.js'
11
11
  import { AddDispatchRetryColumns1786700000000 } from './1786700000000-AddDispatchRetryColumns.js'
12
+ import { CreateTwinReferenceReconciliations1786800000000 } from './1786800000000-CreateTwinReferenceReconciliations.js'
12
13
  import { RenameOperatoMesAdapterType1786600000000 } from './1786600000000-RenameOperatoMesAdapterType.js'
13
14
 
14
15
  export const migrations = [
@@ -19,5 +20,6 @@ export const migrations = [
19
20
  IndexEventCreatedAt1786400000000,
20
21
  CreateTwinSubjectEvents1786500000000,
21
22
  RenameOperatoMesAdapterType1786600000000,
22
- AddDispatchRetryColumns1786700000000
23
+ AddDispatchRetryColumns1786700000000,
24
+ CreateTwinReferenceReconciliations1786800000000
23
25
  ]
@@ -15,6 +15,8 @@ export * from './reference/reference-adapter.js'
15
15
  export * from './reference/live-cadence.js'
16
16
  export * from './reference/reference-live.js'
17
17
  export * from './reference/twin-reference.js'
18
+ export * from './reference/twin-reference-reconciliation.js'
19
+ export * from './reference/reference-reconciliation.js'
18
20
  export * from './reference/connection-portability.js'
19
21
  export * from './reference/template-registry.js'
20
22
  /* 커넥터 점검 하네스 — 실 원본에 붙여 무엇이 채워지고 무엇이 비는지 센다(앱을 띄우지 않는다). */
@@ -2,9 +2,11 @@ import { ConnectionPortabilityResolver } from './connection-portability-resolver
2
2
  import { TwinReferenceResolver } from './reference-resolver.js'
3
3
  import { TwinReferenceProgressSubscription } from './reference-progress-subscription.js'
4
4
  import { TwinReference } from './twin-reference.js'
5
+ import { TwinReferenceReconciliation } from './twin-reference-reconciliation.js'
6
+ import { TwinReferenceReconciliationQuery } from './reference-reconciliation-query.js'
5
7
 
6
- export const entities = [TwinReference]
7
- export const resolvers = [TwinReferenceResolver, TwinReferenceProgressSubscription, ConnectionPortabilityResolver]
8
+ export const entities = [TwinReference, TwinReferenceReconciliation]
9
+ export const resolvers = [TwinReferenceResolver, TwinReferenceProgressSubscription, ConnectionPortabilityResolver, TwinReferenceReconciliationQuery]
8
10
  export * from './control-routing.js'
9
11
  /* 진행 단계 어휘 — **모든 커넥터가 같은 낱말로 말한다.** 화면은 단계마다 번역 하나만 두면 된다. */
10
12
  export * from './reference-progress.js'
@@ -12,4 +14,5 @@ export * from './reference-progress.js'
12
14
  export * from './reference-probe.js'
13
15
  /* 놓친 구간을 backfill 해 채우는 길 — 밀어 받는 길과 같은 번호 검사를 지난다. */
14
16
  export * from './reference-fill.js'
17
+ export * from './reference-reconciliation.js'
15
18
  export * from './connection-portability.js'
@@ -28,9 +28,10 @@ import { getRepository } from '@things-factory/shell'
28
28
  import { TwinInstance } from '../twin-instance/twin-instance.js'
29
29
  import { TwinReference } from './twin-reference.js'
30
30
  import { getAdapter, type SeqReport, type SiteDescriptor } from './reference-adapter.js'
31
- import { pushCursorKey, withSeq } from './hook-contract.js'
31
+ import { firstSeqOf, pushCursorKey, seqOf, withSeq } from './hook-contract.js'
32
32
  import { fillSince, type FillOutcome } from './reference-fill.js'
33
33
  import { liveIngest } from './live-ingest.js'
34
+ import { recordReferenceReconciliation } from './reference-reconciliation-writer.js'
34
35
  import { journalIdentities, spanOf } from './fill-dedupe.js'
35
36
  import { TwinEngine } from '../../engine/index.js'
36
37
  import { twinLog, twinWarn } from '../../engine/log.js'
@@ -87,6 +88,8 @@ export async function runFill(args: {
87
88
  * 가 나아간 만큼만 적으므로, 잃은 구간을 되찾은 뒤 커서는 원래 자리보다 앞서지 않습니다.
88
89
  */
89
90
  const cursor = from === undefined ? ((ref as any).liveCursor ?? {}) : withSeq((ref as any).liveCursor, cursorKey, from)
91
+ /* fillSince가 커서를 저장할 때 이 지역 값도 같이 바꾼다. 저장본을 다시 읽어 대사를 늦추지 않는다. */
92
+ let persistedCursor: unknown = cursor
90
93
 
91
94
  const outcome = await fillSince({
92
95
  cfg: ref.connectionConfig ?? {},
@@ -119,10 +122,21 @@ export async function runFill(args: {
119
122
  },
120
123
  saveCursor: async next => {
121
124
  await repo.update({ id: (ref as any).id }, { liveCursor: next } as any)
125
+ persistedCursor = next
122
126
  },
123
127
  ...(maxPages ? { maxPages } : {})
124
128
  })
125
129
 
130
+ /* 원본이 실제로 답한 범위가 있을 때만 기록한다. 범위를 모르는 것은 "정상"으로 덮지 않는다. */
131
+ await recordReferenceReconciliation({
132
+ domainId,
133
+ instanceId,
134
+ scope,
135
+ sourceRange: outcome.sourceRange,
136
+ cursorSeq: seqOf(persistedCursor, cursorKey),
137
+ firstSeqSeen: firstSeqOf(persistedCursor, cursorKey)
138
+ }).catch(err => twinWarn(`[twin-reconcile] "${instanceId}" · ${scope} 상태를 적지 못했습니다: ${err?.message ?? err}`))
139
+
126
140
  const say = `[twin-fill] "${instanceId}" · ${scope} — ${outcome.applied}건 넣음 · ${outcome.pages}쪽 · ${outcome.stopped}${outcome.detail ? ` (${outcome.detail})` : ''}`
127
141
  /* 끝난 것과 막힌 것을 다른 소리로 남깁니다 — 구멍은 사람이 원본을 봐야 합니다. */
128
142
  if (outcome.stopped === 'done' || outcome.stopped === 'no-progress') twinLog(say)
@@ -0,0 +1,28 @@
1
+ import { Arg, Ctx, Query, Resolver } from 'type-graphql'
2
+ import { getRepository } from '@things-factory/shell'
3
+ import { TwinReferenceReconciliation } from './twin-reference-reconciliation.js'
4
+
5
+ @Resolver()
6
+ export class TwinReferenceReconciliationQuery {
7
+ @Query(returns => [TwinReferenceReconciliation], { description: 'All latest durable sequence reconciliations for one Twin. An empty list means no numbered source range has been read yet, not that every source is aligned.' })
8
+ async twinReferenceReconciliations(
9
+ @Arg('instanceId') instanceId: string,
10
+ @Ctx() context: ResolverContext
11
+ ): Promise<TwinReferenceReconciliation[]> {
12
+ return getRepository(TwinReferenceReconciliation).find({
13
+ where: { domain: { id: context.state.domain.id }, instanceId },
14
+ order: { scope: 'ASC' }
15
+ })
16
+ }
17
+
18
+ @Query(returns => TwinReferenceReconciliation, { nullable: true, description: 'Latest durable source-range versus Twin-cursor reconciliation for one numbered scope. Null means no backfill has yet obtained a source range; it is not an aligned result.' })
19
+ async twinReferenceReconciliation(
20
+ @Arg('instanceId') instanceId: string,
21
+ @Arg('scope') scope: string,
22
+ @Ctx() context: ResolverContext
23
+ ): Promise<TwinReferenceReconciliation | null> {
24
+ return getRepository(TwinReferenceReconciliation).findOne({
25
+ where: { domain: { id: context.state.domain.id }, instanceId, scope }
26
+ })
27
+ }
28
+ }
@@ -0,0 +1,38 @@
1
+ import { getRepository } from '@things-factory/shell'
2
+ import { TwinReferenceReconciliation } from './twin-reference-reconciliation.js'
3
+ import { reconcileReferenceSequence, type ReferenceRange } from './reference-reconciliation.js'
4
+
5
+ /** backfill이 원본 범위를 실제로 읽은 뒤에만 최신 대사 상태를 갱신한다. */
6
+ export async function recordReferenceReconciliation(args: {
7
+ domainId: string
8
+ instanceId: string
9
+ scope: string
10
+ sourceRange?: ReferenceRange
11
+ cursorSeq?: number
12
+ firstSeqSeen?: number
13
+ }): Promise<void> {
14
+ const { domainId, instanceId, scope, sourceRange, cursorSeq, firstSeqSeen } = args
15
+ if (!sourceRange) return
16
+ const verdict = reconcileReferenceSequence({ source: sourceRange, cursorSeq, firstSeqSeen })
17
+ const repo = getRepository(TwinReferenceReconciliation)
18
+ const previous = await repo.findOne({ where: { domain: { id: domainId }, instanceId, scope } })
19
+ await repo.save(
20
+ repo.create({
21
+ ...(previous ?? {}),
22
+ domain: { id: domainId } as any,
23
+ instanceId,
24
+ scope,
25
+ status: verdict.status,
26
+ /* TypeORM은 undefined를 "갱신하지 않음"으로 읽는다. 현재 대사가 맞아진 뒤 옛 gap 수가
27
+ * 남지 않게 nullable 칸은 명시적으로 null로 지운다. */
28
+ sourceFirst: verdict.sourceFirst ?? null,
29
+ sourceLast: verdict.sourceLast ?? null,
30
+ cursorSeq: verdict.cursorSeq ?? null,
31
+ firstSeqSeen: verdict.firstSeqSeen ?? null,
32
+ headMissing: verdict.headMissing ?? null,
33
+ behind: verdict.behind ?? null,
34
+ reason: verdict.reason ?? null,
35
+ checkedAt: new Date()
36
+ })
37
+ )
38
+ }
@@ -0,0 +1,55 @@
1
+ /*
2
+ * 원본 번호 범위와 Twin 커서를 맞춘 **현재 판정**이다.
3
+ *
4
+ * 이 계산은 봉투 수와 저널 행 수를 비교하지 않는다. 재전송과 backfill 때문에 그 둘은 같은
5
+ * 사실을 여러 번 셀 수 있다. 여기서 답하는 것은 번호를 매기는 원본에서만 가능한, 더 좁고
6
+ * 검증 가능한 질문이다: "원본의 양 끝과 우리가 처음/마지막으로 받은 번호가 이어지는가?"
7
+ */
8
+ export type ReferenceRange = { first?: number; last?: number }
9
+
10
+ export type ReferenceReconciliation = {
11
+ sourceFirst?: number
12
+ sourceLast?: number
13
+ cursorSeq?: number
14
+ firstSeqSeen?: number
15
+ headMissing?: number
16
+ behind?: number
17
+ status: 'aligned' | 'gap' | 'unknown'
18
+ reason?: string
19
+ }
20
+
21
+ export function reconcileReferenceSequence(args: {
22
+ source: ReferenceRange | undefined
23
+ cursorSeq?: number
24
+ firstSeqSeen?: number
25
+ }): ReferenceReconciliation {
26
+ const { source, cursorSeq, firstSeqSeen } = args
27
+ const sourceFirst = source?.first
28
+ const sourceLast = source?.last
29
+ const base = { sourceFirst, sourceLast, cursorSeq, firstSeqSeen }
30
+
31
+ if (sourceFirst === undefined || sourceLast === undefined) {
32
+ return { ...base, status: 'unknown', reason: '원본이 번호 범위의 양 끝을 주지 않아 대사할 수 없습니다' }
33
+ }
34
+ if (cursorSeq === undefined) {
35
+ return { ...base, status: 'unknown', reason: 'Twin이 아직 이 단위의 번호를 받은 적이 없습니다' }
36
+ }
37
+ if (cursorSeq < sourceFirst) {
38
+ return {
39
+ ...base,
40
+ status: 'unknown',
41
+ reason: 'Twin 커서가 원본의 첫 번호보다 앞입니다. 원본 번호가 다시 매겨졌거나 다른 단위의 커서입니다'
42
+ }
43
+ }
44
+
45
+ const behind = Math.max(0, sourceLast - cursorSeq)
46
+ /* 처음 받은 번호를 적지 않았으면 앞머리는 "없음"이 아니라 "모름"이다. */
47
+ const headMissing = firstSeqSeen === undefined ? undefined : Math.max(0, firstSeqSeen - sourceFirst)
48
+ if (behind || headMissing) {
49
+ return { ...base, ...(behind ? { behind } : {}), ...(headMissing ? { headMissing } : {}), status: 'gap' }
50
+ }
51
+ if (firstSeqSeen === undefined) {
52
+ return { ...base, status: 'unknown', reason: '처음 받은 번호가 기록되지 않아 앞머리 연속성은 확인할 수 없습니다' }
53
+ }
54
+ return { ...base, status: 'aligned' }
55
+ }
@@ -0,0 +1,27 @@
1
+ import { Column, CreateDateColumn, Entity, Index, ManyToOne, PrimaryGeneratedColumn, RelationId, UpdateDateColumn } from 'typeorm'
2
+ import { Field, ID, Int, ObjectType } from 'type-graphql'
3
+ import { Domain } from '@things-factory/shell'
4
+
5
+ /* 한 연결·Twin·단위의 최신 번호 대사 상태. 과거 추이는 ingest window가, 현재 조치 판단은 이 표가 답한다. */
6
+ @Entity()
7
+ @Index('uq_twin_reference_reconciliation_0', (e: TwinReferenceReconciliation) => [e.domain, e.instanceId, e.scope], { unique: true })
8
+ @Index('ix_twin_reference_reconciliation_0', (e: TwinReferenceReconciliation) => [e.domain, e.instanceId, e.checkedAt], { unique: false })
9
+ @ObjectType({ description: 'Latest durable sequence reconciliation for one connected source scope. It reports only what numbered source ranges can prove; it never substitutes replay-prone ingest-window counts for source facts.' })
10
+ export class TwinReferenceReconciliation {
11
+ @PrimaryGeneratedColumn('uuid') @Field(type => ID) readonly id: string
12
+ @ManyToOne(type => Domain) @Field(type => Domain, { nullable: true }) domain?: Domain
13
+ @RelationId((e: TwinReferenceReconciliation) => e.domain) domainId?: string
14
+ @Column() @Field() instanceId: string
15
+ @Column() @Field() scope: string
16
+ @Column() @Field() status: 'aligned' | 'gap' | 'unknown'
17
+ @Column({ type: 'int', nullable: true }) @Field(type => Int, { nullable: true }) sourceFirst?: number
18
+ @Column({ type: 'int', nullable: true }) @Field(type => Int, { nullable: true }) sourceLast?: number
19
+ @Column({ type: 'int', nullable: true }) @Field(type => Int, { nullable: true }) cursorSeq?: number
20
+ @Column({ type: 'int', nullable: true }) @Field(type => Int, { nullable: true }) firstSeqSeen?: number
21
+ @Column({ type: 'int', nullable: true }) @Field(type => Int, { nullable: true }) headMissing?: number
22
+ @Column({ type: 'int', nullable: true }) @Field(type => Int, { nullable: true }) behind?: number
23
+ @Column({ nullable: true }) @Field({ nullable: true }) reason?: string
24
+ @Column() @Field() checkedAt: Date
25
+ @CreateDateColumn() createdAt?: Date
26
+ @UpdateDateColumn() updatedAt?: Date
27
+ }