@things-factory/figure-service 10.1.3

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 (50) hide show
  1. package/README.md +34 -0
  2. package/dist-server/index.d.ts +2 -0
  3. package/dist-server/index.js +6 -0
  4. package/dist-server/index.js.map +1 -0
  5. package/dist-server/migrations/index.d.ts +1 -0
  6. package/dist-server/migrations/index.js +12 -0
  7. package/dist-server/migrations/index.js.map +1 -0
  8. package/dist-server/service/figure/figure-mutation.d.ts +19 -0
  9. package/dist-server/service/figure/figure-mutation.js +171 -0
  10. package/dist-server/service/figure/figure-mutation.js.map +1 -0
  11. package/dist-server/service/figure/figure-propose-type.d.ts +43 -0
  12. package/dist-server/service/figure/figure-propose-type.js +89 -0
  13. package/dist-server/service/figure/figure-propose-type.js.map +1 -0
  14. package/dist-server/service/figure/figure-propose.d.ts +67 -0
  15. package/dist-server/service/figure/figure-propose.js +213 -0
  16. package/dist-server/service/figure/figure-propose.js.map +1 -0
  17. package/dist-server/service/figure/figure-query.d.ts +16 -0
  18. package/dist-server/service/figure/figure-query.js +77 -0
  19. package/dist-server/service/figure/figure-query.js.map +1 -0
  20. package/dist-server/service/figure/figure-source.d.ts +45 -0
  21. package/dist-server/service/figure/figure-source.js +53 -0
  22. package/dist-server/service/figure/figure-source.js.map +1 -0
  23. package/dist-server/service/figure/figure-type.d.ts +57 -0
  24. package/dist-server/service/figure/figure-type.js +149 -0
  25. package/dist-server/service/figure/figure-type.js.map +1 -0
  26. package/dist-server/service/figure/figure.d.ts +71 -0
  27. package/dist-server/service/figure/figure.js +160 -0
  28. package/dist-server/service/figure/figure.js.map +1 -0
  29. package/dist-server/service/figure/index.d.ts +5 -0
  30. package/dist-server/service/figure/index.js +9 -0
  31. package/dist-server/service/figure/index.js.map +1 -0
  32. package/dist-server/service/index.d.ts +5 -0
  33. package/dist-server/service/index.js +13 -0
  34. package/dist-server/service/index.js.map +1 -0
  35. package/package.json +33 -0
  36. package/server/index.ts +2 -0
  37. package/server/migrations/index.ts +9 -0
  38. package/server/service/figure/figure-mutation.ts +163 -0
  39. package/server/service/figure/figure-propose-type.ts +74 -0
  40. package/server/service/figure/figure-propose.test.ts +164 -0
  41. package/server/service/figure/figure-propose.ts +271 -0
  42. package/server/service/figure/figure-query.ts +60 -0
  43. package/server/service/figure/figure-source.test.ts +129 -0
  44. package/server/service/figure/figure-source.ts +87 -0
  45. package/server/service/figure/figure-type.ts +108 -0
  46. package/server/service/figure/figure.ts +168 -0
  47. package/server/service/figure/index.ts +6 -0
  48. package/server/service/index.ts +10 -0
  49. package/things-factory.config.js +1 -0
  50. package/tsconfig.json +14 -0
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # @things-factory/figure-service
2
+
3
+ 저작된 3D 컴포넌트 한 종류(**Figure**)를 저장·검증·조회한다. 컴포넌트 저작도구의
4
+ 서버 몫이고, 화면 몫은 `@things-factory/figure-ui` 다.
5
+
6
+ ## 무엇을 담나 — `FigureSource` 다
7
+
8
+ 정본 형식은 `@hatiolab/figure-model` 이 정한다. **씬 컴포넌트 트리를 담지 않는다.**
9
+ 청사진(`FigureBlueprint`)도 저장하지 않는다 — 정본에서 파생되는 캐시이므로 소비하는
10
+ 쪽이 `compile()` 로 만든다.
11
+
12
+ ## 저장 전에 검증한다
13
+
14
+ `createFigure`·`updateFigure` 는 `figure-model` 의 `validate()` 를 돌린다.
15
+
16
+ - `errors` — 형식을 어긴 것. **거절한다**
17
+ - `violations` — 한도를 넘은 것. 거절하지 않고 응답에 실어 보낸다
18
+
19
+ 저작 화면을 못 믿어서가 아니다. **저작 화면을 거치지 않는 길**(가져오기·API·AI)이
20
+ 있기 때문이다.
21
+
22
+ `score`·`triangles`·`groups` 는 클라이언트에서 받지 않고 정본에서 계산한다. 받으면
23
+ 거짓이 들어오고 목록 화면이 실물과 다른 것을 보여 준다.
24
+
25
+ ## `type` 은 고칠 수 없다
26
+
27
+ 컴포넌트 타입 이름은 **저장되는 식별자**다. 배치된 보드가 그 이름으로 타입을 찾으므로,
28
+ 바꾸면 오류 없이 빈 화면이 된다. 그래서 `FigurePatch` 에 `type` 이 없다 — 고칠 수 있는
29
+ 이름은 표시 이름(`name`)뿐이다.
30
+
31
+ ## 설계
32
+
33
+ - `docs/design/figure-modules.md` — 모듈 설계와 착수 순서
34
+ - `docs/design/component-authoring-tool.md` — 요구사항과 설계의 본체
@@ -0,0 +1,2 @@
1
+ export * from './service/index.js';
2
+ export * from './migrations/index.js';
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ tslib_1.__exportStar(require("./service/index.js"), exports);
5
+ tslib_1.__exportStar(require("./migrations/index.js"), exports);
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../server/index.ts"],"names":[],"mappings":";;;AAAA,6DAAkC;AAClC,gEAAqC","sourcesContent":["export * from './service/index.js'\nexport * from './migrations/index.js'\n"]}
@@ -0,0 +1 @@
1
+ export declare var migrations: any[];
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.migrations = void 0;
4
+ const glob = require('glob');
5
+ const path = require('path');
6
+ exports.migrations = [];
7
+ glob.sync(path.resolve(__dirname, '.', '**', '*.js')).forEach(function (file) {
8
+ if (file.indexOf('index.js') !== -1)
9
+ return;
10
+ exports.migrations = exports.migrations.concat(Object.values(require(path.resolve(file))) || []);
11
+ });
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/migrations/index.ts"],"names":[],"mappings":";;;AAAA,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;AAC5B,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;AAEjB,QAAA,UAAU,GAAG,EAAE,CAAA;AAE1B,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,UAAS,IAAI;IACzE,IAAI,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QAAE,OAAM;IAC3C,kBAAU,GAAG,kBAAU,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAA;AAClF,CAAC,CAAC,CAAA","sourcesContent":["const glob = require('glob')\nconst path = require('path')\n\nexport var migrations = []\n\nglob.sync(path.resolve(__dirname, '.', '**', '*.js')).forEach(function(file) {\n if (file.indexOf('index.js') !== -1) return\n migrations = migrations.concat(Object.values(require(path.resolve(file))) || [])\n})\n"]}
@@ -0,0 +1,19 @@
1
+ import { FigurePatch, FigureSaveResult, NewFigure } from './figure-type.js';
2
+ import { FigureProposal, ProposeRequest } from './figure-propose-type.js';
3
+ export declare class FigureMutation {
4
+ createFigure(figure: NewFigure, context: ResolverContext): Promise<FigureSaveResult>;
5
+ updateFigure(id: string, patch: FigurePatch, context: ResolverContext): Promise<FigureSaveResult>;
6
+ deleteFigure(id: string, context: ResolverContext): Promise<boolean>;
7
+ /**
8
+ * 저작 보조에게 후보를 시킨다.
9
+ *
10
+ * **저장하지 않는다.** 후보를 돌려줄 뿐이고, 정본이 되는 것은 저작자가 보고 받은
11
+ * 뒤다. AI 가 정본에 직접 쓰면 저작자가 무엇이 자기 것이고 무엇이 기계 것인지
12
+ * 모르게 된다.
13
+ *
14
+ * 형식을 어긴 후보는 여기까지 오지 않는다 — `proposeFigure` 가 사유를 모델에
15
+ * 돌려주고 다시 시킨다. 끝내 못 만들면 **사유와 함께 던진다.** 반쯤 만든 것을
16
+ * 돌려주지 않는다.
17
+ */
18
+ proposeFigure(request: ProposeRequest): Promise<FigureProposal>;
19
+ }
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FigureMutation = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const typeorm_1 = require("typeorm");
6
+ const type_graphql_1 = require("type-graphql");
7
+ const figure_js_1 = require("./figure.js");
8
+ const figure_type_js_1 = require("./figure-type.js");
9
+ const figure_propose_type_js_1 = require("./figure-propose-type.js");
10
+ const figure_source_js_1 = require("./figure-source.js");
11
+ const figure_propose_js_1 = require("./figure-propose.js");
12
+ let FigureMutation = class FigureMutation {
13
+ async createFigure(figure, context) {
14
+ const { domain, user, tx } = context.state;
15
+ const repository = tx.getRepository(figure_js_1.Figure);
16
+ // 타입 이름은 도메인 안에서 유일하다. 유니크 인덱스가 있지만 여기서 먼저 본다 —
17
+ // DB 오류 문구는 저작자에게 아무 말도 해 주지 못한다.
18
+ const taken = await repository.findOne({ where: { domain: { id: domain.id }, type: figure.type } });
19
+ if (taken) {
20
+ throw new Error(`타입 이름 '${figure.type}' 은 이미 쓰이고 있다. 만든 뒤에는 고칠 수 없으므로 다른 이름을 고른다`);
21
+ }
22
+ // 형식을 어기면 여기서 던진다. 파생값은 서버가 계산한다.
23
+ const { source, derived, violations } = (0, figure_source_js_1.parseSource)(figure.source);
24
+ const saved = await repository.save({
25
+ ...figure,
26
+ source,
27
+ ...derived,
28
+ state: figure.state ?? 'draft',
29
+ thumbnailUpdatedAt: figure.thumbnail ? new Date() : undefined,
30
+ domain,
31
+ creator: user,
32
+ updater: user
33
+ });
34
+ return { figure: saved, violations };
35
+ }
36
+ async updateFigure(id, patch, context) {
37
+ const { domain, user, tx } = context.state;
38
+ const repository = tx.getRepository(figure_js_1.Figure);
39
+ const figure = await repository.findOne({
40
+ where: { domain: { id: (0, typeorm_1.In)([domain.id, domain.parentId].filter(Boolean)) }, id }
41
+ });
42
+ if (!figure) {
43
+ throw new Error(`Figure ${id} 를 찾을 수 없다`);
44
+ }
45
+ // 정본이 바뀌면 파생값도 다시 계산한다. 안 바뀌었으면 손대지 않는다 —
46
+ // 이름만 고쳤는데 점수가 흔들리면 읽는 사람이 원인을 못 찾는다.
47
+ let derived;
48
+ let violations = [];
49
+ let source = figure.source;
50
+ if (patch.source !== undefined && patch.source !== figure.source) {
51
+ const parsed = (0, figure_source_js_1.parseSource)(patch.source);
52
+ source = parsed.source;
53
+ derived = parsed.derived;
54
+ violations = parsed.violations;
55
+ }
56
+ const saved = await repository.save({
57
+ ...figure,
58
+ ...patch,
59
+ source,
60
+ ...(derived ?? {}),
61
+ // 썸네일이 **썸네일로서** 바뀐 때만 시각을 갱신한다. 이름·설명 변경에는 안 건드린다.
62
+ thumbnailUpdatedAt: patch.thumbnail !== undefined && patch.thumbnail !== figure.thumbnail
63
+ ? new Date()
64
+ : figure.thumbnailUpdatedAt,
65
+ version: (figure.version ?? 1) + 1,
66
+ updater: user
67
+ });
68
+ return { figure: saved, violations };
69
+ }
70
+ async deleteFigure(id, context) {
71
+ const { domain, tx } = context.state;
72
+ // 소프트 삭제다 — 이미 배치된 보드가 이 타입을 참조할 수 있으므로 행을 지우지
73
+ // 않는다. 되살릴 길을 남긴다.
74
+ await tx.getRepository(figure_js_1.Figure).softDelete({ domain: { id: domain.id }, id });
75
+ return true;
76
+ }
77
+ /**
78
+ * 저작 보조에게 후보를 시킨다.
79
+ *
80
+ * **저장하지 않는다.** 후보를 돌려줄 뿐이고, 정본이 되는 것은 저작자가 보고 받은
81
+ * 뒤다. AI 가 정본에 직접 쓰면 저작자가 무엇이 자기 것이고 무엇이 기계 것인지
82
+ * 모르게 된다.
83
+ *
84
+ * 형식을 어긴 후보는 여기까지 오지 않는다 — `proposeFigure` 가 사유를 모델에
85
+ * 돌려주고 다시 시킨다. 끝내 못 만들면 **사유와 함께 던진다.** 반쯤 만든 것을
86
+ * 돌려주지 않는다.
87
+ */
88
+ async proposeFigure(request) {
89
+ // 그림은 메모리로 받는다. 참고용이라 저장하지 않는다 — 남기면 지울 사람이 없다.
90
+ let image;
91
+ if (request.image) {
92
+ const file = await request.image;
93
+ const chunks = [];
94
+ for await (const chunk of file.createReadStream()) {
95
+ chunks.push(chunk);
96
+ }
97
+ image = { data: Buffer.concat(chunks), mediaType: file.mimetype };
98
+ }
99
+ try {
100
+ const result = await (0, figure_propose_js_1.proposeFigure)({
101
+ prompt: request.prompt,
102
+ base: request.base ? JSON.parse(request.base) : undefined,
103
+ type: request.type,
104
+ palette: request.palette,
105
+ image
106
+ });
107
+ return {
108
+ source: JSON.stringify(result.source),
109
+ score: Math.round(result.score.score),
110
+ grade: result.score.grade,
111
+ triangles: result.cost.triangles,
112
+ groups: result.cost.groups,
113
+ violations: result.violations,
114
+ attempts: result.attempts
115
+ };
116
+ }
117
+ catch (e) {
118
+ if (e instanceof figure_propose_js_1.ProposeFailure) {
119
+ // 사유를 삼키지 않는다. 저작자가 무엇이 막혔는지 알아야 말을 고쳐 다시 시킨다.
120
+ throw new Error([e.message, ...e.reasons].join('\n'));
121
+ }
122
+ throw e;
123
+ }
124
+ }
125
+ };
126
+ exports.FigureMutation = FigureMutation;
127
+ tslib_1.__decorate([
128
+ (0, type_graphql_1.Directive)('@transaction'),
129
+ (0, type_graphql_1.Directive)('@privilege(category: "figure", privilege: "mutation", domainOwnerGranted: true)'),
130
+ (0, type_graphql_1.Mutation)(returns => figure_type_js_1.FigureSaveResult, { description: 'Creates a figure. Rejects when the source violates the format.' }),
131
+ tslib_1.__param(0, (0, type_graphql_1.Arg)('figure')),
132
+ tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
133
+ tslib_1.__metadata("design:type", Function),
134
+ tslib_1.__metadata("design:paramtypes", [figure_type_js_1.NewFigure, Object]),
135
+ tslib_1.__metadata("design:returntype", Promise)
136
+ ], FigureMutation.prototype, "createFigure", null);
137
+ tslib_1.__decorate([
138
+ (0, type_graphql_1.Directive)('@transaction'),
139
+ (0, type_graphql_1.Directive)('@privilege(category: "figure", privilege: "mutation", domainOwnerGranted: true)'),
140
+ (0, type_graphql_1.Mutation)(returns => figure_type_js_1.FigureSaveResult, { description: 'Updates a figure. The `type` field cannot be changed.' }),
141
+ tslib_1.__param(0, (0, type_graphql_1.Arg)('id')),
142
+ tslib_1.__param(1, (0, type_graphql_1.Arg)('patch')),
143
+ tslib_1.__param(2, (0, type_graphql_1.Ctx)()),
144
+ tslib_1.__metadata("design:type", Function),
145
+ tslib_1.__metadata("design:paramtypes", [String, figure_type_js_1.FigurePatch, Object]),
146
+ tslib_1.__metadata("design:returntype", Promise)
147
+ ], FigureMutation.prototype, "updateFigure", null);
148
+ tslib_1.__decorate([
149
+ (0, type_graphql_1.Directive)('@transaction'),
150
+ (0, type_graphql_1.Directive)('@privilege(category: "figure", privilege: "mutation", domainOwnerGranted: true)'),
151
+ (0, type_graphql_1.Mutation)(returns => Boolean, { description: 'Soft-deletes a figure.' }),
152
+ tslib_1.__param(0, (0, type_graphql_1.Arg)('id')),
153
+ tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
154
+ tslib_1.__metadata("design:type", Function),
155
+ tslib_1.__metadata("design:paramtypes", [String, Object]),
156
+ tslib_1.__metadata("design:returntype", Promise)
157
+ ], FigureMutation.prototype, "deleteFigure", null);
158
+ tslib_1.__decorate([
159
+ (0, type_graphql_1.Directive)('@privilege(category: "figure", privilege: "mutation", domainOwnerGranted: true)'),
160
+ (0, type_graphql_1.Mutation)(returns => figure_propose_type_js_1.FigureProposal, {
161
+ description: 'Asks the authoring assistant for a figure. Returns a candidate — nothing is saved.'
162
+ }),
163
+ tslib_1.__param(0, (0, type_graphql_1.Arg)('request')),
164
+ tslib_1.__metadata("design:type", Function),
165
+ tslib_1.__metadata("design:paramtypes", [figure_propose_type_js_1.ProposeRequest]),
166
+ tslib_1.__metadata("design:returntype", Promise)
167
+ ], FigureMutation.prototype, "proposeFigure", null);
168
+ exports.FigureMutation = FigureMutation = tslib_1.__decorate([
169
+ (0, type_graphql_1.Resolver)(figure_js_1.Figure)
170
+ ], FigureMutation);
171
+ //# sourceMappingURL=figure-mutation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"figure-mutation.js","sourceRoot":"","sources":["../../../server/service/figure/figure-mutation.ts"],"names":[],"mappings":"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","sourcesContent":["import { In } from 'typeorm'\nimport { Arg, Ctx, Directive, Mutation, Resolver } from 'type-graphql'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { Figure } from './figure.js'\nimport { FigurePatch, FigureSaveResult, NewFigure } from './figure-type.js'\nimport { FigureProposal, ProposeRequest } from './figure-propose-type.js'\nimport { parseSource } from './figure-source.js'\nimport { ProposeFailure, proposeFigure } from './figure-propose.js'\n\n@Resolver(Figure)\nexport class FigureMutation {\n @Directive('@transaction')\n @Directive('@privilege(category: \"figure\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => FigureSaveResult, { description: 'Creates a figure. Rejects when the source violates the format.' })\n async createFigure(\n @Arg('figure') figure: NewFigure,\n @Ctx() context: ResolverContext\n ): Promise<FigureSaveResult> {\n const { domain, user, tx } = context.state\n const repository = tx.getRepository(Figure)\n\n // 타입 이름은 도메인 안에서 유일하다. 유니크 인덱스가 있지만 여기서 먼저 본다 —\n // DB 오류 문구는 저작자에게 아무 말도 해 주지 못한다.\n const taken = await repository.findOne({ where: { domain: { id: domain.id }, type: figure.type } })\n if (taken) {\n throw new Error(`타입 이름 '${figure.type}' 은 이미 쓰이고 있다. 만든 뒤에는 고칠 수 없으므로 다른 이름을 고른다`)\n }\n\n // 형식을 어기면 여기서 던진다. 파생값은 서버가 계산한다.\n const { source, derived, violations } = parseSource(figure.source)\n\n const saved = await repository.save({\n ...figure,\n source,\n ...derived,\n state: figure.state ?? 'draft',\n thumbnailUpdatedAt: figure.thumbnail ? new Date() : undefined,\n domain,\n creator: user,\n updater: user\n })\n\n return { figure: saved, violations }\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"figure\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => FigureSaveResult, { description: 'Updates a figure. The `type` field cannot be changed.' })\n async updateFigure(\n @Arg('id') id: string,\n @Arg('patch') patch: FigurePatch,\n @Ctx() context: ResolverContext\n ): Promise<FigureSaveResult> {\n const { domain, user, tx } = context.state\n const repository = tx.getRepository(Figure)\n\n const figure = await repository.findOne({\n where: { domain: { id: In([domain.id, domain.parentId].filter(Boolean)) }, id }\n })\n if (!figure) {\n throw new Error(`Figure ${id} 를 찾을 수 없다`)\n }\n\n // 정본이 바뀌면 파생값도 다시 계산한다. 안 바뀌었으면 손대지 않는다 —\n // 이름만 고쳤는데 점수가 흔들리면 읽는 사람이 원인을 못 찾는다.\n let derived: { score: number; triangles: number; groups: number } | undefined\n let violations = []\n let source = figure.source\n\n if (patch.source !== undefined && patch.source !== figure.source) {\n const parsed = parseSource(patch.source)\n source = parsed.source\n derived = parsed.derived\n violations = parsed.violations\n }\n\n const saved = await repository.save({\n ...figure,\n ...patch,\n source,\n ...(derived ?? {}),\n // 썸네일이 **썸네일로서** 바뀐 때만 시각을 갱신한다. 이름·설명 변경에는 안 건드린다.\n thumbnailUpdatedAt:\n patch.thumbnail !== undefined && patch.thumbnail !== figure.thumbnail\n ? new Date()\n : figure.thumbnailUpdatedAt,\n version: (figure.version ?? 1) + 1,\n updater: user\n })\n\n return { figure: saved, violations }\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"figure\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Boolean, { description: 'Soft-deletes a figure.' })\n async deleteFigure(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<boolean> {\n const { domain, tx } = context.state\n\n // 소프트 삭제다 — 이미 배치된 보드가 이 타입을 참조할 수 있으므로 행을 지우지\n // 않는다. 되살릴 길을 남긴다.\n await tx.getRepository(Figure).softDelete({ domain: { id: domain.id }, id })\n\n return true\n }\n\n /**\n * 저작 보조에게 후보를 시킨다.\n *\n * **저장하지 않는다.** 후보를 돌려줄 뿐이고, 정본이 되는 것은 저작자가 보고 받은\n * 뒤다. AI 가 정본에 직접 쓰면 저작자가 무엇이 자기 것이고 무엇이 기계 것인지\n * 모르게 된다.\n *\n * 형식을 어긴 후보는 여기까지 오지 않는다 — `proposeFigure` 가 사유를 모델에\n * 돌려주고 다시 시킨다. 끝내 못 만들면 **사유와 함께 던진다.** 반쯤 만든 것을\n * 돌려주지 않는다.\n */\n @Directive('@privilege(category: \"figure\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => FigureProposal, {\n description: 'Asks the authoring assistant for a figure. Returns a candidate — nothing is saved.'\n })\n async proposeFigure(@Arg('request') request: ProposeRequest): Promise<FigureProposal> {\n // 그림은 메모리로 받는다. 참고용이라 저장하지 않는다 — 남기면 지울 사람이 없다.\n let image\n if (request.image) {\n const file = await request.image\n const chunks: Buffer[] = []\n for await (const chunk of file.createReadStream()) {\n chunks.push(chunk as Buffer)\n }\n image = { data: Buffer.concat(chunks), mediaType: file.mimetype as never }\n }\n\n try {\n const result = await proposeFigure({\n prompt: request.prompt,\n base: request.base ? JSON.parse(request.base) : undefined,\n type: request.type,\n palette: request.palette,\n image\n })\n\n return {\n source: JSON.stringify(result.source),\n score: Math.round(result.score.score),\n grade: result.score.grade,\n triangles: result.cost.triangles,\n groups: result.cost.groups,\n violations: result.violations,\n attempts: result.attempts\n }\n } catch (e) {\n if (e instanceof ProposeFailure) {\n // 사유를 삼키지 않는다. 저작자가 무엇이 막혔는지 알아야 말을 고쳐 다시 시킨다.\n throw new Error([e.message, ...e.reasons].join('\\n'))\n }\n throw e\n }\n }\n\n}\n"]}
@@ -0,0 +1,43 @@
1
+ import type { FileUpload } from 'graphql-upload/GraphQLUpload.js';
2
+ import { FigureFinding } from './figure-type.js';
3
+ /**
4
+ * 저작 보조에 넘기는 것.
5
+ *
6
+ * ## 팔레트를 클라이언트가 보낸다
7
+ *
8
+ * 서버가 팔레트를 알 수 없다. 팔레트는 things-scene 이 갖고 있는데, 그 패키지의
9
+ * node 조건은 transfer 번들로 가고 거기에는 `activePalette` 이 없다 [확인함].
10
+ * 그리는 쪽이 어떤 토큰을 풀 수 있는지 아는 유일한 자리이므로 그쪽이 보낸다.
11
+ *
12
+ * **그래서 저장 문(`parseSource`)은 토큰을 검사하지 못한다.** `validate()` 도
13
+ * 못 한다 — figure-model 이 형식은 갖고 있지만 색 어휘는 things-scene 에 있다.
14
+ * 색 어휘가 형식의 일부라면 figure-model 이 갖는 것이 맞다. **figure-model 에 올릴 것.**
15
+ */
16
+ export declare class ProposeRequest {
17
+ prompt: string;
18
+ base?: string;
19
+ type?: string;
20
+ /**
21
+ * 그림.
22
+ *
23
+ * 비율과 덩어리를 읽는 데 쓴다. 표면 무늬를 옮기는 데 쓰지 않는다 — 로우폴리는
24
+ * 텍스처가 0 인 것이 목표다.
25
+ */
26
+ image?: FileUpload;
27
+ palette: string[];
28
+ }
29
+ /**
30
+ * 후보 하나.
31
+ *
32
+ * **정본이 아니다.** 저작자가 보고 받아야 정본이 된다. 그래서 서버는 저장하지 않고
33
+ * 돌려주기만 한다.
34
+ */
35
+ export declare class FigureProposal {
36
+ source: string;
37
+ score: number;
38
+ grade: string;
39
+ triangles: number;
40
+ groups: number;
41
+ violations: FigureFinding[];
42
+ attempts: number;
43
+ }
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FigureProposal = exports.ProposeRequest = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const GraphQLUpload_js_1 = tslib_1.__importDefault(require("graphql-upload/GraphQLUpload.js"));
6
+ const type_graphql_1 = require("type-graphql");
7
+ const figure_type_js_1 = require("./figure-type.js");
8
+ /**
9
+ * 저작 보조에 넘기는 것.
10
+ *
11
+ * ## 팔레트를 클라이언트가 보낸다
12
+ *
13
+ * 서버가 팔레트를 알 수 없다. 팔레트는 things-scene 이 갖고 있는데, 그 패키지의
14
+ * node 조건은 transfer 번들로 가고 거기에는 `activePalette` 이 없다 [확인함].
15
+ * 그리는 쪽이 어떤 토큰을 풀 수 있는지 아는 유일한 자리이므로 그쪽이 보낸다.
16
+ *
17
+ * **그래서 저장 문(`parseSource`)은 토큰을 검사하지 못한다.** `validate()` 도
18
+ * 못 한다 — figure-model 이 형식은 갖고 있지만 색 어휘는 things-scene 에 있다.
19
+ * 색 어휘가 형식의 일부라면 figure-model 이 갖는 것이 맞다. **figure-model 에 올릴 것.**
20
+ */
21
+ let ProposeRequest = class ProposeRequest {
22
+ };
23
+ exports.ProposeRequest = ProposeRequest;
24
+ tslib_1.__decorate([
25
+ (0, type_graphql_1.Field)({ description: 'What the author asked for, in their own words' }),
26
+ tslib_1.__metadata("design:type", String)
27
+ ], ProposeRequest.prototype, "prompt", void 0);
28
+ tslib_1.__decorate([
29
+ (0, type_graphql_1.Field)({ nullable: true, description: 'The figure source to revise, as JSON. Omit to create a new one.' }),
30
+ tslib_1.__metadata("design:type", String)
31
+ ], ProposeRequest.prototype, "base", void 0);
32
+ tslib_1.__decorate([
33
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Type name for a new figure. Ignored when base is given.' }),
34
+ tslib_1.__metadata("design:type", String)
35
+ ], ProposeRequest.prototype, "type", void 0);
36
+ tslib_1.__decorate([
37
+ (0, type_graphql_1.Field)(type => GraphQLUpload_js_1.default, { nullable: true, description: 'Reference photo or render' }),
38
+ tslib_1.__metadata("design:type", Object)
39
+ ], ProposeRequest.prototype, "image", void 0);
40
+ tslib_1.__decorate([
41
+ (0, type_graphql_1.Field)(type => [String], { description: 'Palette tokens the client can resolve. Anything else is rejected.' }),
42
+ tslib_1.__metadata("design:type", Array)
43
+ ], ProposeRequest.prototype, "palette", void 0);
44
+ exports.ProposeRequest = ProposeRequest = tslib_1.__decorate([
45
+ (0, type_graphql_1.InputType)({ description: 'What to ask the authoring assistant for' })
46
+ ], ProposeRequest);
47
+ /**
48
+ * 후보 하나.
49
+ *
50
+ * **정본이 아니다.** 저작자가 보고 받아야 정본이 된다. 그래서 서버는 저장하지 않고
51
+ * 돌려주기만 한다.
52
+ */
53
+ let FigureProposal = class FigureProposal {
54
+ };
55
+ exports.FigureProposal = FigureProposal;
56
+ tslib_1.__decorate([
57
+ (0, type_graphql_1.Field)({ description: 'The proposed figure source, as JSON' }),
58
+ tslib_1.__metadata("design:type", String)
59
+ ], FigureProposal.prototype, "source", void 0);
60
+ tslib_1.__decorate([
61
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'Reuse score out of 100' }),
62
+ tslib_1.__metadata("design:type", Number)
63
+ ], FigureProposal.prototype, "score", void 0);
64
+ tslib_1.__decorate([
65
+ (0, type_graphql_1.Field)({ description: 'Grade from the score — A, B, C or D' }),
66
+ tslib_1.__metadata("design:type", String)
67
+ ], FigureProposal.prototype, "grade", void 0);
68
+ tslib_1.__decorate([
69
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'Triangles in one instance' }),
70
+ tslib_1.__metadata("design:type", Number)
71
+ ], FigureProposal.prototype, "triangles", void 0);
72
+ tslib_1.__decorate([
73
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'Material groups — the draw calls one instance costs' }),
74
+ tslib_1.__metadata("design:type", Number)
75
+ ], FigureProposal.prototype, "groups", void 0);
76
+ tslib_1.__decorate([
77
+ (0, type_graphql_1.Field)(type => [figure_type_js_1.FigureFinding], {
78
+ description: 'Policy violations. These do not block the proposal — the author decides.'
79
+ }),
80
+ tslib_1.__metadata("design:type", Array)
81
+ ], FigureProposal.prototype, "violations", void 0);
82
+ tslib_1.__decorate([
83
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'How many attempts the assistant needed. Diagnostic.' }),
84
+ tslib_1.__metadata("design:type", Number)
85
+ ], FigureProposal.prototype, "attempts", void 0);
86
+ exports.FigureProposal = FigureProposal = tslib_1.__decorate([
87
+ (0, type_graphql_1.ObjectType)({ description: 'A figure the assistant proposes. Not saved — the author accepts or discards it.' })
88
+ ], FigureProposal);
89
+ //# sourceMappingURL=figure-propose-type.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"figure-propose-type.js","sourceRoot":"","sources":["../../../server/service/figure/figure-propose-type.ts"],"names":[],"mappings":";;;;AACA,+FAA2D;AAC3D,+CAAgE;AAEhE,qDAAgD;AAEhD;;;;;;;;;;;;GAYG;AAEI,IAAM,cAAc,GAApB,MAAM,cAAc;CAqB1B,CAAA;AArBY,wCAAc;AAEzB;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,+CAA+C,EAAE,CAAC;;8CAC1D;AAGd;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,iEAAiE,EAAE,CAAC;;4CAC7F;AAGb;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,yDAAyD,EAAE,CAAC;;4CACrF;AASb;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,0BAAa,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,2BAA2B,EAAE,CAAC;;6CACzE;AAGlB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,WAAW,EAAE,mEAAmE,EAAE,CAAC;;+CAC7F;yBApBN,cAAc;IAD1B,IAAA,wBAAS,EAAC,EAAE,WAAW,EAAE,yCAAyC,EAAE,CAAC;GACzD,cAAc,CAqB1B;AAED;;;;;GAKG;AAEI,IAAM,cAAc,GAApB,MAAM,cAAc;CAuB1B,CAAA;AAvBY,wCAAc;AAEzB;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,qCAAqC,EAAE,CAAC;;8CAChD;AAGd;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,wBAAwB,EAAE,CAAC;;6CACjD;AAGb;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,qCAAqC,EAAE,CAAC;;6CACjD;AAGb;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,2BAA2B,EAAE,CAAC;;iDAChD;AAGjB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,qDAAqD,EAAE,CAAC;;8CAC7E;AAKd;IAHC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,CAAC,8BAAa,CAAC,EAAE;QAC9B,WAAW,EAAE,0EAA0E;KACxF,CAAC;;kDACyB;AAG3B;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,qDAAqD,EAAE,CAAC;;gDAC3E;yBAtBL,cAAc;IAD1B,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,iFAAiF,EAAE,CAAC;GAClG,cAAc,CAuB1B","sourcesContent":["import type { FileUpload } from 'graphql-upload/GraphQLUpload.js'\nimport GraphQLUpload from 'graphql-upload/GraphQLUpload.js'\nimport { Field, InputType, Int, ObjectType } from 'type-graphql'\n\nimport { FigureFinding } from './figure-type.js'\n\n/**\n * 저작 보조에 넘기는 것.\n *\n * ## 팔레트를 클라이언트가 보낸다\n *\n * 서버가 팔레트를 알 수 없다. 팔레트는 things-scene 이 갖고 있는데, 그 패키지의\n * node 조건은 transfer 번들로 가고 거기에는 `activePalette` 이 없다 [확인함].\n * 그리는 쪽이 어떤 토큰을 풀 수 있는지 아는 유일한 자리이므로 그쪽이 보낸다.\n *\n * **그래서 저장 문(`parseSource`)은 토큰을 검사하지 못한다.** `validate()` 도\n * 못 한다 — figure-model 이 형식은 갖고 있지만 색 어휘는 things-scene 에 있다.\n * 색 어휘가 형식의 일부라면 figure-model 이 갖는 것이 맞다. **figure-model 에 올릴 것.**\n */\n@InputType({ description: 'What to ask the authoring assistant for' })\nexport class ProposeRequest {\n @Field({ description: 'What the author asked for, in their own words' })\n prompt: string\n\n @Field({ nullable: true, description: 'The figure source to revise, as JSON. Omit to create a new one.' })\n base?: string\n\n @Field({ nullable: true, description: 'Type name for a new figure. Ignored when base is given.' })\n type?: string\n\n /**\n * 그림.\n *\n * 비율과 덩어리를 읽는 데 쓴다. 표면 무늬를 옮기는 데 쓰지 않는다 — 로우폴리는\n * 텍스처가 0 인 것이 목표다.\n */\n @Field(type => GraphQLUpload, { nullable: true, description: 'Reference photo or render' })\n image?: FileUpload\n\n @Field(type => [String], { description: 'Palette tokens the client can resolve. Anything else is rejected.' })\n palette: string[]\n}\n\n/**\n * 후보 하나.\n *\n * **정본이 아니다.** 저작자가 보고 받아야 정본이 된다. 그래서 서버는 저장하지 않고\n * 돌려주기만 한다.\n */\n@ObjectType({ description: 'A figure the assistant proposes. Not saved — the author accepts or discards it.' })\nexport class FigureProposal {\n @Field({ description: 'The proposed figure source, as JSON' })\n source: string\n\n @Field(type => Int, { description: 'Reuse score out of 100' })\n score: number\n\n @Field({ description: 'Grade from the score — A, B, C or D' })\n grade: string\n\n @Field(type => Int, { description: 'Triangles in one instance' })\n triangles: number\n\n @Field(type => Int, { description: 'Material groups — the draw calls one instance costs' })\n groups: number\n\n @Field(type => [FigureFinding], {\n description: 'Policy violations. These do not block the proposal — the author decides.'\n })\n violations: FigureFinding[]\n\n @Field(type => Int, { description: 'How many attempts the assistant needed. Diagnostic.' })\n attempts: number\n}\n"]}
@@ -0,0 +1,67 @@
1
+ import { costOf, scoreOf } from '@hatiolab/figure-model';
2
+ import type { FigureSource } from '@hatiolab/figure-model';
3
+ import type { AIImageMediaType } from '@things-factory/ai-client-base';
4
+ export interface ProposeInput {
5
+ /** 사람이 시킨 말. */
6
+ prompt: string;
7
+ /** 고쳐 달라는 것이면 지금 정본. 없으면 새로 만든다. */
8
+ base?: FigureSource;
9
+ /** 참고 그림. 설비 사진·렌더. */
10
+ image?: {
11
+ data: Buffer;
12
+ mediaType: AIImageMediaType;
13
+ };
14
+ /** 만들 타입 이름. 새로 만들 때 필요하다. */
15
+ type?: string;
16
+ /** 쓸 수 있는 팔레트 토큰. 여기 없는 토큰은 거절한다. */
17
+ palette: string[];
18
+ }
19
+ export interface ProposeResult {
20
+ source: FigureSource;
21
+ /** 재활용 점수와 한도 여유. */
22
+ score: ReturnType<typeof scoreOf>;
23
+ /** 무게. */
24
+ cost: ReturnType<typeof costOf>;
25
+ /** 형식은 맞으나 정책을 넘은 것. 막지 않고 알린다. */
26
+ violations: {
27
+ code: string;
28
+ message: string;
29
+ }[];
30
+ /** 몇 번 만에 됐나. 저작자에게 보여 줄 값은 아니지만 진단에 쓴다. */
31
+ attempts: number;
32
+ }
33
+ /** 못 만들었을 때. 조용히 빈 것을 돌려주지 않는다. */
34
+ export declare class ProposeFailure extends Error {
35
+ /** 마지막 시도가 낸 사유들. 저작자에게 그대로 보여 준다. */
36
+ readonly reasons: string[];
37
+ readonly attempts: number;
38
+ constructor(message: string,
39
+ /** 마지막 시도가 낸 사유들. 저작자에게 그대로 보여 준다. */
40
+ reasons: string[], attempts: number);
41
+ }
42
+ /**
43
+ * 규칙을 프롬프트로 옮긴다.
44
+ *
45
+ * 값을 손으로 적지 않는다 — figure-model 의 상수에서 만든다. 그래야 한도가 바뀌면
46
+ * 프롬프트도 같이 바뀐다.
47
+ */
48
+ export declare function rulesPrompt(palette: string[]): string;
49
+ /**
50
+ * 후보 하나를 받아 본다.
51
+ *
52
+ * 던지는 것이 아니라 **사유를 모아 돌려준다** — 사유가 다음 시도의 입력이 되기
53
+ * 때문이다. 사람에게 보여 줄 것과 모델에 돌려줄 것이 같은 글이다.
54
+ */
55
+ export declare function inspect(parsed: unknown, palette: string[]): {
56
+ source?: FigureSource;
57
+ reasons: string[];
58
+ };
59
+ /**
60
+ * 말과 그림으로 후보를 만든다.
61
+ *
62
+ * 생성-검사 루프다. 도구 루프가 아니다 — 첫 조각에 도구는 필요 없다.
63
+ *
64
+ * 도구가 필요해지면 `@things-factory/ai-client-base` 의 `runAgenticLoop` 을 부른다.
65
+ * 전에는 그것이 board-ai 안에만 있어 밖에서 쓸 수 없었는데, 공용으로 옮겨졌다.
66
+ */
67
+ export declare function proposeFigure(input: ProposeInput): Promise<ProposeResult>;