@things-factory/figure-service 10.1.34 → 10.1.36
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/figure/figure-gate.d.ts +13 -0
- package/dist-server/service/figure/figure-gate.js +133 -0
- package/dist-server/service/figure/figure-gate.js.map +1 -1
- package/dist-server/service/figure/figure-mutation.d.ts +1 -0
- package/dist-server/service/figure/figure-mutation.js +42 -0
- package/dist-server/service/figure/figure-mutation.js.map +1 -1
- package/dist-server/service/figure/figure-propose.js +9 -1
- package/dist-server/service/figure/figure-propose.js.map +1 -1
- package/dist-server/service/figure/figure-type.d.ts +6 -0
- package/dist-server/service/figure/figure-type.js +26 -1
- package/dist-server/service/figure/figure-type.js.map +1 -1
- package/dist-server/service/figure/figure-v3-write.d.ts +21 -0
- package/dist-server/service/figure/figure-v3-write.js +30 -0
- package/dist-server/service/figure/figure-v3-write.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/server/service/figure/figure-gate.test.ts +20 -0
- package/server/service/figure/figure-gate.ts +145 -0
- package/server/service/figure/figure-mutation.ts +38 -0
- package/server/service/figure/figure-propose.test.ts +23 -3
- package/server/service/figure/figure-propose.ts +10 -1
- package/server/service/figure/figure-type.ts +18 -0
- package/server/service/figure/figure-v3-write.ts +35 -0
|
@@ -10,6 +10,17 @@ export interface GateFix {
|
|
|
10
10
|
/** True only when rechecking every sizing case leaves no release-blocking split. */
|
|
11
11
|
safe: boolean;
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* A coordinated repair, measured from the current source all the way through the sizing suite.
|
|
15
|
+
* A split often involves several independently anchored parts, so no single anchor edit can be
|
|
16
|
+
* called a solution. This remains a draft-only edit; it is never saved or released by the gate.
|
|
17
|
+
*/
|
|
18
|
+
export interface GateFixPlan {
|
|
19
|
+
fixes: GateFix[];
|
|
20
|
+
resolved: number;
|
|
21
|
+
/** True only after the complete plan leaves no release-blocking sizing split. */
|
|
22
|
+
safe: boolean;
|
|
23
|
+
}
|
|
13
24
|
/**
|
|
14
25
|
* 발행해도 되나 — **서버가 판정한다.**
|
|
15
26
|
*
|
|
@@ -61,6 +72,8 @@ export interface GateFinding {
|
|
|
61
72
|
* 수락하기 전에는 정본을 바꾸지 않는다.
|
|
62
73
|
*/
|
|
63
74
|
fixes?: GateFix[];
|
|
75
|
+
/** The one-click, fully remeasured repair when individual candidates are insufficient. */
|
|
76
|
+
fixPlan?: GateFixPlan;
|
|
64
77
|
blocking: boolean;
|
|
65
78
|
}
|
|
66
79
|
export interface Inspection {
|
|
@@ -50,6 +50,136 @@ function measuredFixes(source, factor) {
|
|
|
50
50
|
}
|
|
51
51
|
return fixes.sort((a, b) => b.resolved - a.resolved || a.part.localeCompare(b.part)).slice(0, 12);
|
|
52
52
|
}
|
|
53
|
+
function sourceWithFix(source, fix) {
|
|
54
|
+
return {
|
|
55
|
+
...source,
|
|
56
|
+
parts: source.parts.map(part => part.name === fix.part
|
|
57
|
+
? { ...part, anchor: { ...(part.anchor ?? {}), [fix.axis]: fix.to } }
|
|
58
|
+
: part)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function splitCount(source, factor) {
|
|
62
|
+
return (0, figure_model_1.sizingReport)((0, figure_model_1.compile)(source), factor).findings.filter(finding => finding.code === 'sizing-splits').length;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Find a small coordinated repair by repeatedly choosing the measured anchor edit that removes
|
|
66
|
+
* the most remaining splits. Every step is evaluated against the real compiler; prose is never
|
|
67
|
+
* interpreted. If the complete suite is not clear, no "recommended" button is exposed.
|
|
68
|
+
*/
|
|
69
|
+
function measuredFixPlan(source, factor) {
|
|
70
|
+
const originalCount = splitCount(source, factor);
|
|
71
|
+
if (originalCount === 0)
|
|
72
|
+
return undefined;
|
|
73
|
+
let working = source;
|
|
74
|
+
let remaining = originalCount;
|
|
75
|
+
const used = new Set();
|
|
76
|
+
const fixes = [];
|
|
77
|
+
for (let step = 0; step < source.parts.length * 3 && remaining > 0; step++) {
|
|
78
|
+
const report = (0, figure_model_1.sizingReport)((0, figure_model_1.compile)(working), factor);
|
|
79
|
+
let best;
|
|
80
|
+
for (const finding of report.findings) {
|
|
81
|
+
if (finding.code !== 'sizing-splits' || !finding.way)
|
|
82
|
+
continue;
|
|
83
|
+
const axes = finding.way.split('-').filter((axis) => ['x', 'y', 'z'].includes(axis));
|
|
84
|
+
const names = new Set(finding.clumps.flat());
|
|
85
|
+
for (const part of working.parts) {
|
|
86
|
+
if (!names.has(part.name))
|
|
87
|
+
continue;
|
|
88
|
+
for (const axis of axes) {
|
|
89
|
+
const key = `${part.name}:${axis}`;
|
|
90
|
+
if (used.has(key))
|
|
91
|
+
continue;
|
|
92
|
+
for (const to of ['min', 'center', 'max', 'span']) {
|
|
93
|
+
if (part.anchor?.[axis] === to)
|
|
94
|
+
continue;
|
|
95
|
+
const afterCount = splitCount(sourceWithFix(working, { part: part.name, axis, to }), factor);
|
|
96
|
+
const resolved = remaining - afterCount;
|
|
97
|
+
if (resolved <= 0)
|
|
98
|
+
continue;
|
|
99
|
+
const candidate = {
|
|
100
|
+
findingWay: finding.way,
|
|
101
|
+
part: part.name,
|
|
102
|
+
axis,
|
|
103
|
+
from: source.parts.find(one => one.name === part.name)?.anchor?.[axis],
|
|
104
|
+
to,
|
|
105
|
+
resolved,
|
|
106
|
+
safe: false
|
|
107
|
+
};
|
|
108
|
+
if (!best || candidate.resolved > best.resolved ||
|
|
109
|
+
(candidate.resolved === best.resolved && `${candidate.part}:${candidate.axis}:${candidate.to}` < `${best.part}:${best.axis}:${best.to}`)) {
|
|
110
|
+
best = candidate;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (!best)
|
|
117
|
+
break;
|
|
118
|
+
fixes.push(best);
|
|
119
|
+
used.add(`${best.part}:${best.axis}`);
|
|
120
|
+
working = sourceWithFix(working, best);
|
|
121
|
+
remaining = splitCount(working, factor);
|
|
122
|
+
}
|
|
123
|
+
if (remaining === 0 && fixes.length > 0) {
|
|
124
|
+
return { fixes: fixes.map(fix => ({ ...fix, safe: true })), resolved: originalCount, safe: true };
|
|
125
|
+
}
|
|
126
|
+
/*
|
|
127
|
+
* Some assemblies need two anchors to move together. Neither first move removes a whole split,
|
|
128
|
+
* so greedy improvement alone cannot see the repair. Search a small, deterministic beam of those
|
|
129
|
+
* coordinated moves, and still return only a plan that the compiler proves clear.
|
|
130
|
+
*/
|
|
131
|
+
const firstReport = (0, figure_model_1.sizingReport)((0, figure_model_1.compile)(source), factor);
|
|
132
|
+
const candidates = new Map();
|
|
133
|
+
for (const finding of firstReport.findings) {
|
|
134
|
+
if (finding.code !== 'sizing-splits' || !finding.way)
|
|
135
|
+
continue;
|
|
136
|
+
const axes = finding.way.split('-').filter((axis) => ['x', 'y', 'z'].includes(axis));
|
|
137
|
+
const names = new Set(finding.clumps.flat());
|
|
138
|
+
for (const part of source.parts) {
|
|
139
|
+
if (!names.has(part.name))
|
|
140
|
+
continue;
|
|
141
|
+
for (const axis of axes)
|
|
142
|
+
candidates.set(`${part.name}:${axis}`, { part: part.name, axis });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
let frontier = [{ source, fixes: [], used: new Set(), remaining: originalCount }];
|
|
146
|
+
const visited = new Set();
|
|
147
|
+
for (let depth = 0; depth < 8 && frontier.length; depth++) {
|
|
148
|
+
const next = [];
|
|
149
|
+
for (const state of frontier) {
|
|
150
|
+
for (const [key, target] of candidates) {
|
|
151
|
+
if (state.used.has(key))
|
|
152
|
+
continue;
|
|
153
|
+
const current = state.source.parts.find(part => part.name === target.part);
|
|
154
|
+
if (!current)
|
|
155
|
+
continue;
|
|
156
|
+
for (const to of ['min', 'center', 'max', 'span']) {
|
|
157
|
+
if (current.anchor?.[target.axis] === to)
|
|
158
|
+
continue;
|
|
159
|
+
const changed = sourceWithFix(state.source, { ...target, to });
|
|
160
|
+
const signature = JSON.stringify(changed.parts.map(part => [part.name, part.anchor]));
|
|
161
|
+
if (visited.has(signature))
|
|
162
|
+
continue;
|
|
163
|
+
visited.add(signature);
|
|
164
|
+
const after = splitCount(changed, factor);
|
|
165
|
+
const fix = {
|
|
166
|
+
findingWay: 'coordinated', part: target.part, axis: target.axis,
|
|
167
|
+
from: source.parts.find(part => part.name === target.part)?.anchor?.[target.axis], to,
|
|
168
|
+
resolved: state.remaining - after, safe: false
|
|
169
|
+
};
|
|
170
|
+
const fixes = [...state.fixes, fix];
|
|
171
|
+
if (after === 0)
|
|
172
|
+
return { fixes: fixes.map(one => ({ ...one, safe: true })), resolved: originalCount, safe: true };
|
|
173
|
+
next.push({ source: changed, fixes, used: new Set([...state.used, key]), remaining: after });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/* Keep enough alternatives to discover a paired repair without making release inspection expensive. */
|
|
178
|
+
frontier = next.sort((a, b) => a.remaining - b.remaining || a.fixes.length - b.fixes.length ||
|
|
179
|
+
JSON.stringify(a.fixes).localeCompare(JSON.stringify(b.fixes))).slice(0, 24);
|
|
180
|
+
}
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
53
183
|
/**
|
|
54
184
|
* 정본 하나를 판정한다.
|
|
55
185
|
*
|
|
@@ -114,6 +244,7 @@ function inspectSource(sourceJson, releasedJson) {
|
|
|
114
244
|
const source = parsed;
|
|
115
245
|
const report = (0, figure_model_1.sizingReport)((0, figure_model_1.compile)(source));
|
|
116
246
|
const fixes = measuredFixes(source, report.factor);
|
|
247
|
+
const fixPlan = measuredFixPlan(source, report.factor);
|
|
117
248
|
for (const finding of report.findings) {
|
|
118
249
|
findings.push({
|
|
119
250
|
code: finding.code,
|
|
@@ -124,6 +255,8 @@ function inspectSource(sourceJson, releasedJson) {
|
|
|
124
255
|
/* 숫자가 아니라 자리를 말한다 — 어느 부품들이 갈라져 나왔나. */
|
|
125
256
|
at: finding.clumps.map(clump => [...clump].sort().join('+')).join(' | ') || undefined,
|
|
126
257
|
fixes: finding.code === 'sizing-splits' ? fixes.filter(fix => fix.findingWay === finding.way) : undefined,
|
|
258
|
+
/* A plan is attached once: it clears the full suite, not merely this card's symptom. */
|
|
259
|
+
fixPlan: finding.code === 'sizing-splits' && finding === report.findings.find(one => one.code === 'sizing-splits') ? fixPlan : undefined,
|
|
127
260
|
blocking: finding.code === 'sizing-splits'
|
|
128
261
|
});
|
|
129
262
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"figure-gate.js","sourceRoot":"","sources":["../../../server/service/figure/figure-gate.ts"],"names":[],"mappings":"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","sourcesContent":["import { blocksRelease, compile, contractChanges, sizingReport, validate } from '@hatiolab/figure-model'\nimport type { Axis, FigureSource } from '@hatiolab/figure-model'\n\n/** Service transport shape. It deliberately contains only an explicit, reversible FigureSource edit. */\nexport interface GateFix {\n findingWay: string\n part: string\n axis: Axis\n from?: string\n to: 'min' | 'center' | 'max' | 'span'\n resolved: number\n /** True only when rechecking every sizing case leaves no release-blocking split. */\n safe: boolean\n}\n\n/**\n * 발행해도 되나 — **서버가 판정한다.**\n *\n * ## 왜 서버인가\n *\n * 이 검사는 브라우저 하네스에만 있었다(`test/browser/check-figure-scaling.mjs`). 사람이 돌릴 때만\n * 돌았고, 발행 경로에는 아무 검사도 없었다 — 형식만 맞으면 무엇이든 발행되었다. 발행은 소비처에게\n * 하는 약속이므로, 그 약속을 지킬 수 있는지는 약속하는 자리가 재야 한다.\n *\n * ## 무엇을 막고 무엇을 알리나\n *\n * 판정은 `figure-model` 의 `gate.ts` 가 이미 문서로 정해 두었다. 여기서 새로 정하지 않는다.\n *\n * sizing-splits 크기를 바꾸면 조각이 갈라진다 **막는다**\n * part-outside-base 부품이 기준 상자를 넘는다 **막는다**\n * parts-off-placement-face 기준면에 안 닿는다 **막는다**\n * sizing-loose 1배에서 이미 여러 조각이다 알린다\n * 그 밖의 violations 형식이 권하지 않는 것 알린다 (저장 때 이미 보여 준 것들)\n *\n * 갈라짐을 막는 이유는 사람 눈에 「부서졌다」로 보이기 때문이다 — 시그널타워를 3배로 세우니 램프가\n * 기둥 위 공중에 떴던 그것이고, 앵커 규칙이 생긴 이유다. 소비처가 형상을 늘려 쓰는 것은 막을 수\n * 없으니(그것이 이 형식의 값이다), 늘려도 견디는지를 발행 전에 본다.\n *\n * ## 몇 개가 걸리나는 여기 안 적는다\n *\n * 한동안 「표본 15개 중 갈라짐 1 · 조용함 14」를 적어 두고 **게이트가 벽이 아니라는 근거**로\n * 삼았다. 수는 굳고 카탈로그는 움직인다 — 기준 상자 판정이 붙자 그 비율이 하루 만에 뒤집혔다.\n * 굳은 수를 근거로 든 문장은 그때부터 사실이 아닌 채로 사람을 안심시킨다.\n *\n * 무엇이 걸리고 얼마나 넘쳤나는 **저작 도구가 재서 보여 준다.** 그것을 볼 사람은 고치는\n * 사람이고, 그 사람이 보는 자리는 여기가 아니다.\n */\n\n/** 게이트가 낸 한 가지. `blocking` 이 참이면 발행이 안 된다. */\nexport interface GateFinding {\n code: string\n message: string\n /**\n * **왜 그것이 문제인가.** 잰 것만 보여 주면 읽는 사람이 「그래서?」에서 멈춘다 — 화면에\n * 「1 조각이 2 조각으로 갈라진다」만 떴을 때 실제로 그랬다.\n */\n why?: string\n /** **무엇을 바꾸면 되나.** 부품 이름과 값으로. 형식이 만든 문장을 그대로 옮긴다. */\n how?: string\n /** 어디를 가리키나 — 갈라짐은 갈라진 부품 이름들, 형식 위반은 정본 안의 경로. */\n at?: string\n /**\n * 실제로 재 본, 되돌릴 수 있는 한 앵커 수정안들. 문장을 파싱해서 고치는 것이 아니라\n * FigureSource 에 적용할 정확한 값만 내보낸다. 화면은 이것을 후보로만 보여 주고 저작자가\n * 수락하기 전에는 정본을 바꾸지 않는다.\n */\n fixes?: GateFix[]\n blocking: boolean\n}\n\nexport interface Inspection {\n findings: GateFinding[]\n /** 막는 것이 하나라도 있나. 화면이 이것으로 단추를 잠근다. */\n blocked: boolean\n}\n\n/**\n * Figure model publishes the same capability for direct model consumers. Keep the service calculation\n * here as well because this package is deployed independently and receives a compiled local model package.\n * Each alternative is remeasured; prose from `how` is never parsed into an edit.\n */\nfunction measuredFixes(source: FigureSource, factor: number): GateFix[] {\n const baseline = sizingReport(compile(source), factor)\n const baselineCount = baseline.findings.filter(finding => finding.code === 'sizing-splits').length\n const seen = new Set<string>()\n const fixes: GateFix[] = []\n\n for (const finding of baseline.findings) {\n if (finding.code !== 'sizing-splits' || !finding.way) continue\n const axes = finding.way.split('-').filter((axis): axis is Axis => ['x', 'y', 'z'].includes(axis))\n const names = new Set(finding.clumps.flat())\n\n for (const part of source.parts) {\n if (!names.has(part.name)) continue\n for (const axis of axes) {\n for (const to of ['min', 'center', 'max', 'span'] as const) {\n if (part.anchor?.[axis] === to) continue\n const changed: FigureSource = {\n ...source,\n parts: source.parts.map(one => one.name === part.name ? { ...one, anchor: { ...(one.anchor ?? {}), [axis]: to } } : one)\n }\n const afterCount = sizingReport(compile(changed), factor).findings.filter(one => one.code === 'sizing-splits').length\n const resolved = baselineCount - afterCount\n const key = `${finding.way}:${part.name}:${axis}:${to}`\n if (resolved <= 0 || seen.has(key)) continue\n seen.add(key)\n fixes.push({\n findingWay: finding.way,\n part: part.name,\n axis,\n from: part.anchor?.[axis],\n to,\n resolved,\n /* 한 finding만 없앤 것이 아니라 전수 검사에서 막는 split이 0일 때만 원클릭 후보가 된다. */\n safe: afterCount === 0\n })\n }\n }\n }\n }\n\n return fixes.sort((a, b) => b.resolved - a.resolved || a.part.localeCompare(b.part)).slice(0, 12)\n}\n\n/**\n * 정본 하나를 판정한다.\n *\n * 정본이 없거나(아직 저장 전) JSON 이 아니면 **막는다** — 발행할 대상이 없는 것이다. 그 경우를\n * 조용히 통과시키면 빈 것이 발행된다.\n *\n * `releasedJson` 은 **직전 발행판의 정본**이다. 주면 그것과 견주어 「이미 놓인 인스턴스를 깨는\n * 발행인가」를 함께 낸다(ADR-0063 결정 3). 한 번도 발행된 적 없으면 견줄 것이 없으므로 안 준다.\n */\nexport function inspectSource(sourceJson?: string | null, releasedJson?: string | null): Inspection {\n if (!sourceJson) {\n return {\n blocked: true,\n findings: [\n {\n code: 'no-source',\n message: '저장된 정본이 없다. 저장하면 발행할 수 있다',\n blocking: true\n }\n ]\n }\n }\n\n let parsed: unknown\n try {\n parsed = JSON.parse(sourceJson)\n } catch (e) {\n return {\n blocked: true,\n findings: [{ code: 'source-not-json', message: `정본이 JSON 이 아니다: ${(e as Error).message}`, blocking: true }]\n }\n }\n\n const result = validate(parsed)\n const findings: GateFinding[] = []\n\n /*\n 형식 오류는 **저장을 통과할 수 없다**(`parseSource` 가 거절한다). 그래도 여기서 다시 보는 이유는,\n 이 판정이 저장 경로와 독립이어야 하기 때문이다 — 옛 정본이 남아 있거나 형식이 엄해진 뒤라면\n 저장 때는 맞았던 것이 지금은 틀릴 수 있다. 그때 발행이 통과하면 안 된다.\n */\n for (const error of result.errors) {\n findings.push({ code: error.code, message: error.message, at: error.path, blocking: true })\n }\n\n for (const violation of result.violations) {\n findings.push({\n code: violation.code,\n message: violation.message,\n /*\n 왜·어떻게는 형식이 만든 것을 그대로 옮긴다. 여기서 서버가 다시 쓰면 두 벌이 되고 곧 어긋난다 —\n 크기 반응 판정이 이미 그 규율이다(아래 `report.findings`).\n\n 이 두 줄이 없던 동안 발행 화면은 막는 위반에 문장 하나만 보여 줬다. 표본 47 개를 게이트에 넣어\n 보니 막는 판정 99 건 중 왜·어떻게를 가진 것이 2 건이었고, 나머지 97 건은 저작자가 상자를 키울지\n 부품을 옮길지 알 길이 없었다(2026-09-19, figure-model 0.1.29 가 그 값을 싣기 시작했다).\n */\n why: violation.why,\n how: violation.how,\n /* 어느 위반이 막나는 형식이 안다 — 여기 목록을 또 들면 둘이 갈린다(ADR-0044 결정 ②). */\n blocking: blocksRelease(violation.code)\n })\n }\n\n if (result.errors.length === 0) {\n const source = parsed as FigureSource\n const report = sizingReport(compile(source))\n const fixes = measuredFixes(source, report.factor)\n\n for (const finding of report.findings) {\n findings.push({\n code: finding.code,\n message: finding.message,\n /* 왜·어떻게는 형식이 소유한다. 서버가 다시 쓰면 두 벌이 되고 곧 어긋난다. */\n why: finding.why,\n how: finding.how,\n /* 숫자가 아니라 자리를 말한다 — 어느 부품들이 갈라져 나왔나. */\n at: finding.clumps.map(clump => [...clump].sort().join('+')).join(' | ') || undefined,\n fixes: finding.code === 'sizing-splits' ? fixes.filter(fix => fix.findingWay === finding.way) : undefined,\n blocking: finding.code === 'sizing-splits'\n })\n }\n\n findings.push(...contractFindings(source, releasedJson))\n }\n\n return { findings, blocked: findings.some(finding => finding.blocking) }\n}\n\n/**\n * 직전 발행판과 견준 것 — **약속을 깨는 발행인가**(ADR-0063 결정 3).\n *\n * 무엇이 달라졌고 그것이 막는 것인지는 형식이 정한다(`contractChanges`). 여기서는 화면과 발행 문이\n * 읽는 모양으로 옮기기만 한다.\n *\n * **앞 판을 오늘 형식으로 못 읽으면 조용히 넘어가지 않는다.** 판 1 은 오늘 형식이 거절하는 판으로\n * 적혀 있을 수 있고, 그때 견주기를 건너뛰면 아무 말 없이 관문이 없는 것이 된다. 막지는 않는다 —\n * 그러면 그 도형은 영영 다시 발행할 수 없다.\n */\nfunction contractFindings(source: FigureSource, releasedJson?: string | null): GateFinding[] {\n if (!releasedJson) return []\n\n let released: FigureSource\n try {\n released = JSON.parse(releasedJson) as FigureSource\n return contractChanges(released, source).map(change => ({\n code: change.code,\n message: change.message,\n why: change.why,\n at: change.subject,\n blocking: change.blocking\n }))\n } catch (e) {\n return [\n {\n code: 'released-unreadable',\n message: `직전 발행판을 오늘 형식으로 읽을 수 없어 견주지 못했다: ${(e as Error).message}`,\n why: '이 발행이 이미 놓인 인스턴스를 깨는지는 확인되지 않았다.',\n blocking: false\n }\n ]\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"figure-gate.js","sourceRoot":"","sources":["../../../server/service/figure/figure-gate.ts"],"names":[],"mappings":"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","sourcesContent":["import { blocksRelease, compile, contractChanges, sizingReport, validate } from '@hatiolab/figure-model'\nimport type { Axis, FigureSource } from '@hatiolab/figure-model'\n\n/** Service transport shape. It deliberately contains only an explicit, reversible FigureSource edit. */\nexport interface GateFix {\n findingWay: string\n part: string\n axis: Axis\n from?: string\n to: 'min' | 'center' | 'max' | 'span'\n resolved: number\n /** True only when rechecking every sizing case leaves no release-blocking split. */\n safe: boolean\n}\n\n/**\n * A coordinated repair, measured from the current source all the way through the sizing suite.\n * A split often involves several independently anchored parts, so no single anchor edit can be\n * called a solution. This remains a draft-only edit; it is never saved or released by the gate.\n */\nexport interface GateFixPlan {\n fixes: GateFix[]\n resolved: number\n /** True only after the complete plan leaves no release-blocking sizing split. */\n safe: boolean\n}\n\n/**\n * 발행해도 되나 — **서버가 판정한다.**\n *\n * ## 왜 서버인가\n *\n * 이 검사는 브라우저 하네스에만 있었다(`test/browser/check-figure-scaling.mjs`). 사람이 돌릴 때만\n * 돌았고, 발행 경로에는 아무 검사도 없었다 — 형식만 맞으면 무엇이든 발행되었다. 발행은 소비처에게\n * 하는 약속이므로, 그 약속을 지킬 수 있는지는 약속하는 자리가 재야 한다.\n *\n * ## 무엇을 막고 무엇을 알리나\n *\n * 판정은 `figure-model` 의 `gate.ts` 가 이미 문서로 정해 두었다. 여기서 새로 정하지 않는다.\n *\n * sizing-splits 크기를 바꾸면 조각이 갈라진다 **막는다**\n * part-outside-base 부품이 기준 상자를 넘는다 **막는다**\n * parts-off-placement-face 기준면에 안 닿는다 **막는다**\n * sizing-loose 1배에서 이미 여러 조각이다 알린다\n * 그 밖의 violations 형식이 권하지 않는 것 알린다 (저장 때 이미 보여 준 것들)\n *\n * 갈라짐을 막는 이유는 사람 눈에 「부서졌다」로 보이기 때문이다 — 시그널타워를 3배로 세우니 램프가\n * 기둥 위 공중에 떴던 그것이고, 앵커 규칙이 생긴 이유다. 소비처가 형상을 늘려 쓰는 것은 막을 수\n * 없으니(그것이 이 형식의 값이다), 늘려도 견디는지를 발행 전에 본다.\n *\n * ## 몇 개가 걸리나는 여기 안 적는다\n *\n * 한동안 「표본 15개 중 갈라짐 1 · 조용함 14」를 적어 두고 **게이트가 벽이 아니라는 근거**로\n * 삼았다. 수는 굳고 카탈로그는 움직인다 — 기준 상자 판정이 붙자 그 비율이 하루 만에 뒤집혔다.\n * 굳은 수를 근거로 든 문장은 그때부터 사실이 아닌 채로 사람을 안심시킨다.\n *\n * 무엇이 걸리고 얼마나 넘쳤나는 **저작 도구가 재서 보여 준다.** 그것을 볼 사람은 고치는\n * 사람이고, 그 사람이 보는 자리는 여기가 아니다.\n */\n\n/** 게이트가 낸 한 가지. `blocking` 이 참이면 발행이 안 된다. */\nexport interface GateFinding {\n code: string\n message: string\n /**\n * **왜 그것이 문제인가.** 잰 것만 보여 주면 읽는 사람이 「그래서?」에서 멈춘다 — 화면에\n * 「1 조각이 2 조각으로 갈라진다」만 떴을 때 실제로 그랬다.\n */\n why?: string\n /** **무엇을 바꾸면 되나.** 부품 이름과 값으로. 형식이 만든 문장을 그대로 옮긴다. */\n how?: string\n /** 어디를 가리키나 — 갈라짐은 갈라진 부품 이름들, 형식 위반은 정본 안의 경로. */\n at?: string\n /**\n * 실제로 재 본, 되돌릴 수 있는 한 앵커 수정안들. 문장을 파싱해서 고치는 것이 아니라\n * FigureSource 에 적용할 정확한 값만 내보낸다. 화면은 이것을 후보로만 보여 주고 저작자가\n * 수락하기 전에는 정본을 바꾸지 않는다.\n */\n fixes?: GateFix[]\n /** The one-click, fully remeasured repair when individual candidates are insufficient. */\n fixPlan?: GateFixPlan\n blocking: boolean\n}\n\nexport interface Inspection {\n findings: GateFinding[]\n /** 막는 것이 하나라도 있나. 화면이 이것으로 단추를 잠근다. */\n blocked: boolean\n}\n\n/**\n * Figure model publishes the same capability for direct model consumers. Keep the service calculation\n * here as well because this package is deployed independently and receives a compiled local model package.\n * Each alternative is remeasured; prose from `how` is never parsed into an edit.\n */\nfunction measuredFixes(source: FigureSource, factor: number): GateFix[] {\n const baseline = sizingReport(compile(source), factor)\n const baselineCount = baseline.findings.filter(finding => finding.code === 'sizing-splits').length\n const seen = new Set<string>()\n const fixes: GateFix[] = []\n\n for (const finding of baseline.findings) {\n if (finding.code !== 'sizing-splits' || !finding.way) continue\n const axes = finding.way.split('-').filter((axis): axis is Axis => ['x', 'y', 'z'].includes(axis))\n const names = new Set(finding.clumps.flat())\n\n for (const part of source.parts) {\n if (!names.has(part.name)) continue\n for (const axis of axes) {\n for (const to of ['min', 'center', 'max', 'span'] as const) {\n if (part.anchor?.[axis] === to) continue\n const changed: FigureSource = {\n ...source,\n parts: source.parts.map(one => one.name === part.name ? { ...one, anchor: { ...(one.anchor ?? {}), [axis]: to } } : one)\n }\n const afterCount = sizingReport(compile(changed), factor).findings.filter(one => one.code === 'sizing-splits').length\n const resolved = baselineCount - afterCount\n const key = `${finding.way}:${part.name}:${axis}:${to}`\n if (resolved <= 0 || seen.has(key)) continue\n seen.add(key)\n fixes.push({\n findingWay: finding.way,\n part: part.name,\n axis,\n from: part.anchor?.[axis],\n to,\n resolved,\n /* 한 finding만 없앤 것이 아니라 전수 검사에서 막는 split이 0일 때만 원클릭 후보가 된다. */\n safe: afterCount === 0\n })\n }\n }\n }\n }\n\n return fixes.sort((a, b) => b.resolved - a.resolved || a.part.localeCompare(b.part)).slice(0, 12)\n}\n\nfunction sourceWithFix(source: FigureSource, fix: Pick<GateFix, 'part' | 'axis' | 'to'>): FigureSource {\n return {\n ...source,\n parts: source.parts.map(part => part.name === fix.part\n ? { ...part, anchor: { ...(part.anchor ?? {}), [fix.axis]: fix.to } }\n : part)\n }\n}\n\nfunction splitCount(source: FigureSource, factor: number) {\n return sizingReport(compile(source), factor).findings.filter(finding => finding.code === 'sizing-splits').length\n}\n\n/**\n * Find a small coordinated repair by repeatedly choosing the measured anchor edit that removes\n * the most remaining splits. Every step is evaluated against the real compiler; prose is never\n * interpreted. If the complete suite is not clear, no \"recommended\" button is exposed.\n */\nfunction measuredFixPlan(source: FigureSource, factor: number): GateFixPlan | undefined {\n const originalCount = splitCount(source, factor)\n if (originalCount === 0) return undefined\n\n let working = source\n let remaining = originalCount\n const used = new Set<string>()\n const fixes: GateFix[] = []\n\n for (let step = 0; step < source.parts.length * 3 && remaining > 0; step++) {\n const report = sizingReport(compile(working), factor)\n let best: GateFix | undefined\n\n for (const finding of report.findings) {\n if (finding.code !== 'sizing-splits' || !finding.way) continue\n const axes = finding.way.split('-').filter((axis): axis is Axis => ['x', 'y', 'z'].includes(axis))\n const names = new Set(finding.clumps.flat())\n\n for (const part of working.parts) {\n if (!names.has(part.name)) continue\n for (const axis of axes) {\n const key = `${part.name}:${axis}`\n if (used.has(key)) continue\n for (const to of ['min', 'center', 'max', 'span'] as const) {\n if (part.anchor?.[axis] === to) continue\n const afterCount = splitCount(sourceWithFix(working, { part: part.name, axis, to }), factor)\n const resolved = remaining - afterCount\n if (resolved <= 0) continue\n const candidate: GateFix = {\n findingWay: finding.way,\n part: part.name,\n axis,\n from: source.parts.find(one => one.name === part.name)?.anchor?.[axis],\n to,\n resolved,\n safe: false\n }\n if (!best || candidate.resolved > best.resolved ||\n (candidate.resolved === best.resolved && `${candidate.part}:${candidate.axis}:${candidate.to}` < `${best.part}:${best.axis}:${best.to}`)) {\n best = candidate\n }\n }\n }\n }\n }\n\n if (!best) break\n fixes.push(best)\n used.add(`${best.part}:${best.axis}`)\n working = sourceWithFix(working, best)\n remaining = splitCount(working, factor)\n }\n\n if (remaining === 0 && fixes.length > 0) {\n return { fixes: fixes.map(fix => ({ ...fix, safe: true })), resolved: originalCount, safe: true }\n }\n\n /*\n * Some assemblies need two anchors to move together. Neither first move removes a whole split,\n * so greedy improvement alone cannot see the repair. Search a small, deterministic beam of those\n * coordinated moves, and still return only a plan that the compiler proves clear.\n */\n const firstReport = sizingReport(compile(source), factor)\n const candidates = new Map<string, { part: string; axis: Axis }>()\n for (const finding of firstReport.findings) {\n if (finding.code !== 'sizing-splits' || !finding.way) continue\n const axes = finding.way.split('-').filter((axis): axis is Axis => ['x', 'y', 'z'].includes(axis))\n const names = new Set(finding.clumps.flat())\n for (const part of source.parts) {\n if (!names.has(part.name)) continue\n for (const axis of axes) candidates.set(`${part.name}:${axis}`, { part: part.name, axis })\n }\n }\n\n type SearchState = { source: FigureSource; fixes: GateFix[]; used: Set<string>; remaining: number }\n let frontier: SearchState[] = [{ source, fixes: [], used: new Set(), remaining: originalCount }]\n const visited = new Set<string>()\n for (let depth = 0; depth < 8 && frontier.length; depth++) {\n const next: SearchState[] = []\n for (const state of frontier) {\n for (const [key, target] of candidates) {\n if (state.used.has(key)) continue\n const current = state.source.parts.find(part => part.name === target.part)\n if (!current) continue\n for (const to of ['min', 'center', 'max', 'span'] as const) {\n if (current.anchor?.[target.axis] === to) continue\n const changed = sourceWithFix(state.source, { ...target, to })\n const signature = JSON.stringify(changed.parts.map(part => [part.name, part.anchor]))\n if (visited.has(signature)) continue\n visited.add(signature)\n const after = splitCount(changed, factor)\n const fix: GateFix = {\n findingWay: 'coordinated', part: target.part, axis: target.axis,\n from: source.parts.find(part => part.name === target.part)?.anchor?.[target.axis], to,\n resolved: state.remaining - after, safe: false\n }\n const fixes = [...state.fixes, fix]\n if (after === 0) return { fixes: fixes.map(one => ({ ...one, safe: true })), resolved: originalCount, safe: true }\n next.push({ source: changed, fixes, used: new Set([...state.used, key]), remaining: after })\n }\n }\n }\n /* Keep enough alternatives to discover a paired repair without making release inspection expensive. */\n frontier = next.sort((a, b) => a.remaining - b.remaining || a.fixes.length - b.fixes.length ||\n JSON.stringify(a.fixes).localeCompare(JSON.stringify(b.fixes))).slice(0, 24)\n }\n\n return undefined\n}\n\n/**\n * 정본 하나를 판정한다.\n *\n * 정본이 없거나(아직 저장 전) JSON 이 아니면 **막는다** — 발행할 대상이 없는 것이다. 그 경우를\n * 조용히 통과시키면 빈 것이 발행된다.\n *\n * `releasedJson` 은 **직전 발행판의 정본**이다. 주면 그것과 견주어 「이미 놓인 인스턴스를 깨는\n * 발행인가」를 함께 낸다(ADR-0063 결정 3). 한 번도 발행된 적 없으면 견줄 것이 없으므로 안 준다.\n */\nexport function inspectSource(sourceJson?: string | null, releasedJson?: string | null): Inspection {\n if (!sourceJson) {\n return {\n blocked: true,\n findings: [\n {\n code: 'no-source',\n message: '저장된 정본이 없다. 저장하면 발행할 수 있다',\n blocking: true\n }\n ]\n }\n }\n\n let parsed: unknown\n try {\n parsed = JSON.parse(sourceJson)\n } catch (e) {\n return {\n blocked: true,\n findings: [{ code: 'source-not-json', message: `정본이 JSON 이 아니다: ${(e as Error).message}`, blocking: true }]\n }\n }\n\n const result = validate(parsed)\n const findings: GateFinding[] = []\n\n /*\n 형식 오류는 **저장을 통과할 수 없다**(`parseSource` 가 거절한다). 그래도 여기서 다시 보는 이유는,\n 이 판정이 저장 경로와 독립이어야 하기 때문이다 — 옛 정본이 남아 있거나 형식이 엄해진 뒤라면\n 저장 때는 맞았던 것이 지금은 틀릴 수 있다. 그때 발행이 통과하면 안 된다.\n */\n for (const error of result.errors) {\n findings.push({ code: error.code, message: error.message, at: error.path, blocking: true })\n }\n\n for (const violation of result.violations) {\n findings.push({\n code: violation.code,\n message: violation.message,\n /*\n 왜·어떻게는 형식이 만든 것을 그대로 옮긴다. 여기서 서버가 다시 쓰면 두 벌이 되고 곧 어긋난다 —\n 크기 반응 판정이 이미 그 규율이다(아래 `report.findings`).\n\n 이 두 줄이 없던 동안 발행 화면은 막는 위반에 문장 하나만 보여 줬다. 표본 47 개를 게이트에 넣어\n 보니 막는 판정 99 건 중 왜·어떻게를 가진 것이 2 건이었고, 나머지 97 건은 저작자가 상자를 키울지\n 부품을 옮길지 알 길이 없었다(2026-09-19, figure-model 0.1.29 가 그 값을 싣기 시작했다).\n */\n why: violation.why,\n how: violation.how,\n /* 어느 위반이 막나는 형식이 안다 — 여기 목록을 또 들면 둘이 갈린다(ADR-0044 결정 ②). */\n blocking: blocksRelease(violation.code)\n })\n }\n\n if (result.errors.length === 0) {\n const source = parsed as FigureSource\n const report = sizingReport(compile(source))\n const fixes = measuredFixes(source, report.factor)\n const fixPlan = measuredFixPlan(source, report.factor)\n\n for (const finding of report.findings) {\n findings.push({\n code: finding.code,\n message: finding.message,\n /* 왜·어떻게는 형식이 소유한다. 서버가 다시 쓰면 두 벌이 되고 곧 어긋난다. */\n why: finding.why,\n how: finding.how,\n /* 숫자가 아니라 자리를 말한다 — 어느 부품들이 갈라져 나왔나. */\n at: finding.clumps.map(clump => [...clump].sort().join('+')).join(' | ') || undefined,\n fixes: finding.code === 'sizing-splits' ? fixes.filter(fix => fix.findingWay === finding.way) : undefined,\n /* A plan is attached once: it clears the full suite, not merely this card's symptom. */\n fixPlan: finding.code === 'sizing-splits' && finding === report.findings.find(one => one.code === 'sizing-splits') ? fixPlan : undefined,\n blocking: finding.code === 'sizing-splits'\n })\n }\n\n findings.push(...contractFindings(source, releasedJson))\n }\n\n return { findings, blocked: findings.some(finding => finding.blocking) }\n}\n\n/**\n * 직전 발행판과 견준 것 — **약속을 깨는 발행인가**(ADR-0063 결정 3).\n *\n * 무엇이 달라졌고 그것이 막는 것인지는 형식이 정한다(`contractChanges`). 여기서는 화면과 발행 문이\n * 읽는 모양으로 옮기기만 한다.\n *\n * **앞 판을 오늘 형식으로 못 읽으면 조용히 넘어가지 않는다.** 판 1 은 오늘 형식이 거절하는 판으로\n * 적혀 있을 수 있고, 그때 견주기를 건너뛰면 아무 말 없이 관문이 없는 것이 된다. 막지는 않는다 —\n * 그러면 그 도형은 영영 다시 발행할 수 없다.\n */\nfunction contractFindings(source: FigureSource, releasedJson?: string | null): GateFinding[] {\n if (!releasedJson) return []\n\n let released: FigureSource\n try {\n released = JSON.parse(releasedJson) as FigureSource\n return contractChanges(released, source).map(change => ({\n code: change.code,\n message: change.message,\n why: change.why,\n at: change.subject,\n blocking: change.blocking\n }))\n } catch (e) {\n return [\n {\n code: 'released-unreadable',\n message: `직전 발행판을 오늘 형식으로 읽을 수 없어 견주지 못했다: ${(e as Error).message}`,\n why: '이 발행이 이미 놓인 인스턴스를 깨는지는 확인되지 않았다.',\n blocking: false\n }\n ]\n }\n}\n"]}
|
|
@@ -2,6 +2,7 @@ import { Figure } from './figure.js';
|
|
|
2
2
|
import { FigurePatch, FigureSaveResult, NewFigure } from './figure-type.js';
|
|
3
3
|
import { FigureProposal, ProposeRequest } from './figure-propose-type.js';
|
|
4
4
|
export declare class FigureMutation {
|
|
5
|
+
saveFigureV3(source: string, context: ResolverContext, id?: string, expectedSource?: string, creationId?: string): Promise<Figure>;
|
|
5
6
|
createFigure(figure: NewFigure, context: ResolverContext): Promise<FigureSaveResult>;
|
|
6
7
|
updateFigure(id: string, patch: FigurePatch, context: ResolverContext): Promise<FigureSaveResult>;
|
|
7
8
|
/**
|
|
@@ -12,7 +12,36 @@ const figure_gate_js_1 = require("./figure-gate.js");
|
|
|
12
12
|
const figure_rename_js_1 = require("./figure-rename.js");
|
|
13
13
|
const figure_source_js_1 = require("./figure-source.js");
|
|
14
14
|
const figure_propose_js_1 = require("./figure-propose.js");
|
|
15
|
+
const figure_v3_write_js_1 = require("./figure-v3-write.js");
|
|
15
16
|
let FigureMutation = class FigureMutation {
|
|
17
|
+
async saveFigureV3(source, context, id, expectedSource, creationId) {
|
|
18
|
+
const { domain, user, tx } = context.state;
|
|
19
|
+
const repository = tx.getRepository(figure_js_1.Figure);
|
|
20
|
+
if (!id) {
|
|
21
|
+
if (!creationId || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(creationId))
|
|
22
|
+
throw Error('새 모델 저장에는 생성 요청 식별자가 필요합니다.');
|
|
23
|
+
const prepared = (0, figure_v3_write_js_1.prepareV3Write)(source);
|
|
24
|
+
const previous = await repository.findOne({ where: { id: creationId, domain: { id: domain.id } } });
|
|
25
|
+
if (previous) {
|
|
26
|
+
if (previous.source !== prepared.source)
|
|
27
|
+
throw Error('이 생성 요청은 이미 다른 내용으로 저장되었습니다. 저장된 모델을 다시 열어 주세요.');
|
|
28
|
+
return previous;
|
|
29
|
+
}
|
|
30
|
+
// insert, not save: a concurrent retry must never turn into an unchecked overwrite.
|
|
31
|
+
await repository.insert({ ...prepared, id: creationId, type: `FIGURE_${creationId.replace(/-/g, '').toUpperCase()}`,
|
|
32
|
+
domain, creator: user, updater: user });
|
|
33
|
+
return repository.findOneOrFail({ where: { id: creationId, domain: { id: domain.id } } });
|
|
34
|
+
}
|
|
35
|
+
// Exact tenant ownership: an inherited catalog entry is not writable in the child domain.
|
|
36
|
+
const figure = await repository.findOne({ where: { id, domain: { id: domain.id } } });
|
|
37
|
+
if (!figure)
|
|
38
|
+
throw Error('편집할 Figure를 찾을 수 없습니다.');
|
|
39
|
+
const prepared = (0, figure_v3_write_js_1.prepareV3Write)(source, figure, expectedSource);
|
|
40
|
+
// Compare-and-swap in the UPDATE itself: supported by SQLite as well as PostgreSQL.
|
|
41
|
+
// A pessimistic row lock is not supported by the product's SQLite deployment.
|
|
42
|
+
await (0, figure_v3_write_js_1.compareAndSwapV3)(criteria => repository.update(criteria, { ...prepared, updater: user }), id, domain.id, expectedSource);
|
|
43
|
+
return repository.findOneOrFail({ where: { id, domain: { id: domain.id } } });
|
|
44
|
+
}
|
|
16
45
|
async createFigure(figure, context) {
|
|
17
46
|
const { domain, user, tx } = context.state;
|
|
18
47
|
const repository = tx.getRepository(figure_js_1.Figure);
|
|
@@ -319,6 +348,19 @@ let FigureMutation = class FigureMutation {
|
|
|
319
348
|
}
|
|
320
349
|
};
|
|
321
350
|
exports.FigureMutation = FigureMutation;
|
|
351
|
+
tslib_1.__decorate([
|
|
352
|
+
(0, type_graphql_1.Directive)('@transaction'),
|
|
353
|
+
(0, type_graphql_1.Directive)('@privilege(category: "figure", privilege: "mutation", domainOwnerGranted: true)'),
|
|
354
|
+
(0, type_graphql_1.Mutation)(returns => figure_js_1.Figure, { description: 'Save a V3 asset draft using an exact-source concurrency check.' }),
|
|
355
|
+
tslib_1.__param(0, (0, type_graphql_1.Arg)('source')),
|
|
356
|
+
tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
|
|
357
|
+
tslib_1.__param(2, (0, type_graphql_1.Arg)('id', { nullable: true })),
|
|
358
|
+
tslib_1.__param(3, (0, type_graphql_1.Arg)('expectedSource', { nullable: true })),
|
|
359
|
+
tslib_1.__param(4, (0, type_graphql_1.Arg)('creationId', { nullable: true })),
|
|
360
|
+
tslib_1.__metadata("design:type", Function),
|
|
361
|
+
tslib_1.__metadata("design:paramtypes", [String, Object, String, String, String]),
|
|
362
|
+
tslib_1.__metadata("design:returntype", Promise)
|
|
363
|
+
], FigureMutation.prototype, "saveFigureV3", null);
|
|
322
364
|
tslib_1.__decorate([
|
|
323
365
|
(0, type_graphql_1.Directive)('@transaction'),
|
|
324
366
|
(0, type_graphql_1.Directive)('@privilege(category: "figure", privilege: "mutation", domainOwnerGranted: true)'),
|
|
@@ -1 +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 { FigureHistory } from './figure-history.js'\nimport { FigurePatch, FigureSaveResult, NewFigure } from './figure-type.js'\nimport { FigureProposal, ProposeRequest } from './figure-propose-type.js'\nimport { inspectSource } from './figure-gate.js'\nimport { renameDraftType, renameFacts } from './figure-rename.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 /*\n **타입은 발행 전에만 바뀐다** — ADR-0063 결정 4.\n\n 발행된 뒤에는 이미 배치된 도면이 이 이름을 가리키고, 바꾸면 그 도면이 아무 오류 없이\n 빈 자리가 된다. 발행된 적 없는 도형은 가리키는 도면이 있을 수 없다 — 도면은 최신\n 발행판을 그리므로 그려질 자리가 없다.\n\n 행과 정본 둘 다 고친다. 정본 안에도 `type` 이 있어서 한쪽만 고치면 저장된 JSON 과\n 행이 다른 이름을 말하고, 발행할 때 어느 쪽이 도면에 적히는지가 갈린다.\n */\n let renamed: string | undefined\n if (patch.type !== undefined && patch.type !== figure.type) {\n const change = await this.renameType(figure, domain, patch.type, source, tx)\n renamed = change.type\n source = change.source\n }\n\n /*\n **판 번호는 여기서 손대지 않는다.** 저장은 저작이고, 판은 발행에서 생긴다(`releaseFigure`).\n board 의 `updateBoard` 도 같다 — 저장 경로에는 version 이 없다.\n */\n const saved = await repository.save({\n /*\n `domain` 을 명시로 넣는다 — 읽어 온 엔티티에는 관계가 안 실려 있다.\n\n 판본 스냅샷은 넘겨받은 엔티티를 베끼므로, 여기서 빠지면 판본의 도메인이 비고 그 판은\n 도메인으로 거르는 조회에 **한 줄도 안 걸린다**(실제로 그랬다). board 의 저장들도 같은\n 자리에서 domain 을 함께 넣는다.\n */\n domain,\n ...figure,\n ...patch,\n /* 앞뒤 공백을 떼고 겹침까지 본 이름. `patch.type` 을 그대로 두면 다듬기 전 값이 들어간다. */\n ...(renamed !== undefined ? { type: renamed } : {}),\n source,\n /*\n **고치면 발행이 풀린다.**\n\n 발행은 「이 판을 써도 된다」는 약속이고, 그 뒤에 정본이 바뀌면 약속한 것과 배포된 것이\n 달라진다. 그래서 저장은 언제나 초안으로 되돌리고, 다시 내보내려면 발행을 한 번 더 한다\n (판 번호가 하나 오르고 판본이 하나 더 남는다). board 의 `updateBoard` 가 같은 한 줄을 쓴다.\n */\n state: 'draft',\n ...(derived ?? {}),\n // 썸네일이 **썸네일로서** 바뀐 때만 시각을 갱신한다. 이름·설명 변경에는 안 건드린다.\n thumbnailUpdatedAt:\n patch.thumbnail !== undefined && patch.thumbnail !== figure.thumbnail\n ? new Date()\n : figure.thumbnailUpdatedAt,\n updater: user\n })\n\n return { figure: saved, violations }\n }\n\n /**\n * 초안의 타입을 고친다 — 규칙은 `figure-rename.ts`, 여기서는 그 규칙이 볼 **사실만** 읽는다.\n *\n * 겹침은 DB 인덱스(`ix_figure_1`)도 잡지만 먼저 읽어 본다. 인덱스가 던지는 말은 저작자가\n * 읽을 것이 못 되고, 어느 이름이 이미 쓰였는지도 안 알려 준다.\n */\n private async renameType(\n figure: Figure,\n domain: { id: string },\n wanted: string,\n source: string,\n tx: { getRepository: <T>(target: new () => T) => any }\n ): Promise<{ type: string; source: string }> {\n /* 읽어 온 엔티티에는 관계가 안 실려 있다 — 도메인은 문맥에서 온 것을 쓴다. */\n const facts = await renameFacts(\n { figures: tx.getRepository(Figure), histories: tx.getRepository(FigureHistory) },\n figure,\n domain,\n wanted\n )\n\n return renameDraftType({ type: figure.type, source }, wanted, facts)\n }\n\n /**\n * 발행한다 — **판이 생기는 유일한 자리.**\n *\n * ## 무엇이 일어나나\n *\n * `state` 가 `released` 가 되고, 판 번호가 하나 오르고, 그 순간의 도형이 판본으로 남는다\n * (`FigureHistory` — 스냅샷은 구독자가 뜬다). 저작 중 저장은 이 셋 중 아무것도 하지 않는다.\n *\n * ## 왜 이미 발행된 것을 거절하나\n *\n * 발행은 소비처에게 하는 약속이다. 같은 판을 두 번 발행하면 무엇이 약속인지 흐려진다. 고쳐서\n * 다시 내려면 초안으로 되돌린 뒤 발행한다 — board 가 같은 규칙이다.\n *\n * ## 번호는 판본에서 센다\n *\n * 도형의 현재 번호가 아니라 **판본의 최대 번호 + 1** 이다. 되돌리기로 옛 판이 현재가 되어도\n * 다음 발행이 이미 쓴 번호를 다시 쓰지 않는다.\n *\n * ## 게이트를 통과해야 한다\n *\n * 크기를 바꿨을 때 조각이 갈라지는 도형은 발행되지 않는다(`figure-gate.ts`). 소비처는 형상을\n * 늘려 쓰므로 — 그것이 이 형식의 값이다 — 늘려도 견디는지가 약속의 일부다. 화면은 같은 판정을\n * `inspectFigure` 로 미리 물어 이유를 보여 준다.\n */\n @Directive('@transaction')\n @Directive('@privilege(category: \"figure\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Figure, { description: 'Releases a figure — raises its version and keeps the released state as history.' })\n async releaseFigure(\n @Arg('id') id: string,\n @Arg('comment', { nullable: true }) comment: string,\n @Ctx() context: ResolverContext\n ): Promise<Figure> {\n const { domain, user, tx } = context.state\n const repository = tx.getRepository(Figure)\n const historyRepository = tx.getRepository(FigureHistory)\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 if (figure.state === 'released') {\n throw new Error(`Figure '${figure.type}' 은 이미 발행되어 있다. 고쳐서 다시 내려면 초안으로 되돌린 뒤 발행한다`)\n }\n\n /*\n ── 게이트는 여기 있어야 한다 ────────────────────────────────────────────\n 화면도 같은 판정을 미리 물어(`inspectFigure`) 이유를 보여 주지만, **막는 자리는 이곳뿐이다.**\n 물어본 뒤 정본이 바뀌었을 수 있고, 화면을 거치지 않는 부름도 있다.\n\n 막는 이유는 사유와 함께 낸다. 「발행할 수 없습니다」로 뭉치면 무엇을 고쳐야 하는지 모른다.\n */\n /*\n 직전 발행판을 먼저 꺼낸다. 판 번호를 세는 데도 쓰고, **약속을 깨는 발행인가**를 견주는 데도\n 쓴다(ADR-0063 결정 3) — 도면은 type 만 적고 늘 최신 발행판을 그리므로, 다시 발행하는 것은\n 그 type 을 놓아 둔 모든 도면에게 같은 약속을 다시 하는 일이다.\n */\n const released = await historyRepository\n .createQueryBuilder('history')\n .where('history.originalId = :id', { id: figure.id })\n .orderBy('history.version', 'DESC')\n .limit(1)\n .getOne()\n\n const inspection = inspectSource(figure.source, released?.source)\n if (inspection.blocked) {\n const reasons = inspection.findings\n .filter(finding => finding.blocking)\n .map(finding => (finding.at ? `${finding.message} (${finding.at})` : finding.message))\n\n throw new Error(`Figure '${figure.type}' 은 아직 발행할 수 없다 — ${reasons.join(' · ')}`)\n }\n\n const nextVersion = released?.version ? Number(released.version) + 1 : 1\n\n return await repository.save({\n /*\n `domain` 을 명시로 넣는다 — 읽어 온 엔티티에는 관계가 안 실려 있다.\n\n 판본 스냅샷은 넘겨받은 엔티티를 베끼므로, 여기서 빠지면 판본의 도메인이 비고 그 판은\n 도메인으로 거르는 조회에 **한 줄도 안 걸린다**(실제로 그랬다). board 의 저장들도 같은\n 자리에서 domain 을 함께 넣는다.\n */\n domain,\n ...figure,\n version: nextVersion,\n state: 'released',\n updater: user,\n /* 판본 구독자가 이것을 판본의 comment 로 옮긴다. Figure 의 칸이 아니다. */\n __releaseComment: comment ?? null\n } as any)\n }\n\n /**\n * 옛 판으로 되돌린다.\n *\n * ## 무엇이 돌아오나\n *\n * 그 판의 **정본과 속성 정의, 그리고 그림**이다. 이름·설명·분류는 그대로 둔다 — 그것들은 언제든\n * 바꿔도 안전한 카탈로그의 말이고, 되돌리기가 개명까지 되돌리면 사람이 시키지 않은 일을 한다.\n *\n * ## 되돌려도 발행은 아니다\n *\n * 초안으로 돌아온다. 다시 내보내려면 발행을 한 번 더 하고, 그때 판 번호는 **이미 쓴 번호 다음**을\n * 받는다 — 3판을 되살렸다고 다시 3판이 되지 않는다. board 도 같다.\n *\n * ## 비용은 다시 잰다\n *\n * 판본이 든 점수·삼각형은 **그때의 형식**으로 잰 값이다. 지금 이 도형이 그 내용이 되었으므로\n * 오늘 기준으로 다시 계산한다. 옛 값은 판본에 그대로 남아 있다.\n */\n @Directive('@transaction')\n @Directive('@privilege(category: \"figure\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => FigureSaveResult, { description: 'Reverts a figure to one of its released versions. It comes back as a draft.' })\n async revertFigureVersion(\n @Arg('id') id: string,\n @Arg('version') version: number,\n @Ctx() context: ResolverContext\n ): Promise<FigureSaveResult> {\n const { domain, user, tx } = context.state\n const repository = tx.getRepository(Figure)\n const historyRepository = tx.getRepository(FigureHistory)\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 const history = await historyRepository.findOne({\n where: { domain: { id: In([domain.id, domain.parentId].filter(Boolean)) }, originalId: id, version }\n })\n if (!history) {\n throw new Error(`Figure '${figure.type}' 에 ${version} 판이 없다`)\n }\n\n /* 형식 검사도 다시 받는다 — 옛 판이 오늘의 형식을 어길 수 있고, 그것을 조용히 들이지 않는다. */\n const { source, derived, violations } = parseSource(history.source)\n\n const saved = await repository.save({\n /*\n `domain` 을 명시로 넣는다 — 읽어 온 엔티티에는 관계가 안 실려 있다.\n\n 판본 스냅샷은 넘겨받은 엔티티를 베끼므로, 여기서 빠지면 판본의 도메인이 비고 그 판은\n 도메인으로 거르는 조회에 **한 줄도 안 걸린다**(실제로 그랬다). board 의 저장들도 같은\n 자리에서 domain 을 함께 넣는다.\n */\n domain,\n ...figure,\n source,\n ...derived,\n properties: history.properties,\n thumbnail: history.thumbnail,\n thumbnailUpdatedAt: history.thumbnail === figure.thumbnail ? figure.thumbnailUpdatedAt : new Date(),\n state: 'draft',\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 refine: request.refine,\n feedback: request.feedback,\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 quality: {\n ...result.quality,\n visualRegions: result.quality.visual.regions,\n visualCoverage: result.quality.visual.coverage,\n visualReadability: result.quality.visual.readability\n },\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"]}
|
|
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 { FigureHistory } from './figure-history.js'\nimport { FigurePatch, FigureSaveResult, NewFigure } from './figure-type.js'\nimport { FigureProposal, ProposeRequest } from './figure-propose-type.js'\nimport { inspectSource } from './figure-gate.js'\nimport { renameDraftType, renameFacts } from './figure-rename.js'\nimport { parseSource } from './figure-source.js'\nimport { ProposeFailure, proposeFigure } from './figure-propose.js'\nimport { prepareV3Write, compareAndSwapV3 } from './figure-v3-write.js'\n\n@Resolver(Figure)\nexport class FigureMutation {\n @Directive('@transaction')\n @Directive('@privilege(category: \"figure\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Figure, { description: 'Save a V3 asset draft using an exact-source concurrency check.' })\n async saveFigureV3(\n @Arg('source') source: string,\n @Ctx() context: ResolverContext,\n @Arg('id', { nullable: true }) id?: string,\n @Arg('expectedSource', { nullable: true }) expectedSource?: string,\n @Arg('creationId', { nullable: true }) creationId?: string\n ): Promise<Figure> {\n const { domain, user, tx } = context.state\n const repository = tx.getRepository(Figure)\n if (!id) {\n if (!creationId || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(creationId))\n throw Error('새 모델 저장에는 생성 요청 식별자가 필요합니다.')\n const prepared = prepareV3Write(source)\n const previous = await repository.findOne({ where: { id: creationId, domain: { id: domain.id } } })\n if (previous) {\n if (previous.source !== prepared.source) throw Error('이 생성 요청은 이미 다른 내용으로 저장되었습니다. 저장된 모델을 다시 열어 주세요.')\n return previous\n }\n // insert, not save: a concurrent retry must never turn into an unchecked overwrite.\n await repository.insert({ ...prepared, id: creationId, type: `FIGURE_${creationId.replace(/-/g, '').toUpperCase()}`,\n domain, creator: user, updater: user })\n return repository.findOneOrFail({ where: { id: creationId, domain: { id: domain.id } } })\n }\n // Exact tenant ownership: an inherited catalog entry is not writable in the child domain.\n const figure = await repository.findOne({ where: { id, domain: { id: domain.id } } })\n if (!figure) throw Error('편집할 Figure를 찾을 수 없습니다.')\n const prepared = prepareV3Write(source, figure, expectedSource)\n // Compare-and-swap in the UPDATE itself: supported by SQLite as well as PostgreSQL.\n // A pessimistic row lock is not supported by the product's SQLite deployment.\n await compareAndSwapV3(criteria => repository.update(criteria, { ...prepared, updater: user }),\n id, domain.id, expectedSource)\n return repository.findOneOrFail({ where: { id, domain: { id: domain.id } } })\n }\n\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 /*\n **타입은 발행 전에만 바뀐다** — ADR-0063 결정 4.\n\n 발행된 뒤에는 이미 배치된 도면이 이 이름을 가리키고, 바꾸면 그 도면이 아무 오류 없이\n 빈 자리가 된다. 발행된 적 없는 도형은 가리키는 도면이 있을 수 없다 — 도면은 최신\n 발행판을 그리므로 그려질 자리가 없다.\n\n 행과 정본 둘 다 고친다. 정본 안에도 `type` 이 있어서 한쪽만 고치면 저장된 JSON 과\n 행이 다른 이름을 말하고, 발행할 때 어느 쪽이 도면에 적히는지가 갈린다.\n */\n let renamed: string | undefined\n if (patch.type !== undefined && patch.type !== figure.type) {\n const change = await this.renameType(figure, domain, patch.type, source, tx)\n renamed = change.type\n source = change.source\n }\n\n /*\n **판 번호는 여기서 손대지 않는다.** 저장은 저작이고, 판은 발행에서 생긴다(`releaseFigure`).\n board 의 `updateBoard` 도 같다 — 저장 경로에는 version 이 없다.\n */\n const saved = await repository.save({\n /*\n `domain` 을 명시로 넣는다 — 읽어 온 엔티티에는 관계가 안 실려 있다.\n\n 판본 스냅샷은 넘겨받은 엔티티를 베끼므로, 여기서 빠지면 판본의 도메인이 비고 그 판은\n 도메인으로 거르는 조회에 **한 줄도 안 걸린다**(실제로 그랬다). board 의 저장들도 같은\n 자리에서 domain 을 함께 넣는다.\n */\n domain,\n ...figure,\n ...patch,\n /* 앞뒤 공백을 떼고 겹침까지 본 이름. `patch.type` 을 그대로 두면 다듬기 전 값이 들어간다. */\n ...(renamed !== undefined ? { type: renamed } : {}),\n source,\n /*\n **고치면 발행이 풀린다.**\n\n 발행은 「이 판을 써도 된다」는 약속이고, 그 뒤에 정본이 바뀌면 약속한 것과 배포된 것이\n 달라진다. 그래서 저장은 언제나 초안으로 되돌리고, 다시 내보내려면 발행을 한 번 더 한다\n (판 번호가 하나 오르고 판본이 하나 더 남는다). board 의 `updateBoard` 가 같은 한 줄을 쓴다.\n */\n state: 'draft',\n ...(derived ?? {}),\n // 썸네일이 **썸네일로서** 바뀐 때만 시각을 갱신한다. 이름·설명 변경에는 안 건드린다.\n thumbnailUpdatedAt:\n patch.thumbnail !== undefined && patch.thumbnail !== figure.thumbnail\n ? new Date()\n : figure.thumbnailUpdatedAt,\n updater: user\n })\n\n return { figure: saved, violations }\n }\n\n /**\n * 초안의 타입을 고친다 — 규칙은 `figure-rename.ts`, 여기서는 그 규칙이 볼 **사실만** 읽는다.\n *\n * 겹침은 DB 인덱스(`ix_figure_1`)도 잡지만 먼저 읽어 본다. 인덱스가 던지는 말은 저작자가\n * 읽을 것이 못 되고, 어느 이름이 이미 쓰였는지도 안 알려 준다.\n */\n private async renameType(\n figure: Figure,\n domain: { id: string },\n wanted: string,\n source: string,\n tx: { getRepository: <T>(target: new () => T) => any }\n ): Promise<{ type: string; source: string }> {\n /* 읽어 온 엔티티에는 관계가 안 실려 있다 — 도메인은 문맥에서 온 것을 쓴다. */\n const facts = await renameFacts(\n { figures: tx.getRepository(Figure), histories: tx.getRepository(FigureHistory) },\n figure,\n domain,\n wanted\n )\n\n return renameDraftType({ type: figure.type, source }, wanted, facts)\n }\n\n /**\n * 발행한다 — **판이 생기는 유일한 자리.**\n *\n * ## 무엇이 일어나나\n *\n * `state` 가 `released` 가 되고, 판 번호가 하나 오르고, 그 순간의 도형이 판본으로 남는다\n * (`FigureHistory` — 스냅샷은 구독자가 뜬다). 저작 중 저장은 이 셋 중 아무것도 하지 않는다.\n *\n * ## 왜 이미 발행된 것을 거절하나\n *\n * 발행은 소비처에게 하는 약속이다. 같은 판을 두 번 발행하면 무엇이 약속인지 흐려진다. 고쳐서\n * 다시 내려면 초안으로 되돌린 뒤 발행한다 — board 가 같은 규칙이다.\n *\n * ## 번호는 판본에서 센다\n *\n * 도형의 현재 번호가 아니라 **판본의 최대 번호 + 1** 이다. 되돌리기로 옛 판이 현재가 되어도\n * 다음 발행이 이미 쓴 번호를 다시 쓰지 않는다.\n *\n * ## 게이트를 통과해야 한다\n *\n * 크기를 바꿨을 때 조각이 갈라지는 도형은 발행되지 않는다(`figure-gate.ts`). 소비처는 형상을\n * 늘려 쓰므로 — 그것이 이 형식의 값이다 — 늘려도 견디는지가 약속의 일부다. 화면은 같은 판정을\n * `inspectFigure` 로 미리 물어 이유를 보여 준다.\n */\n @Directive('@transaction')\n @Directive('@privilege(category: \"figure\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Figure, { description: 'Releases a figure — raises its version and keeps the released state as history.' })\n async releaseFigure(\n @Arg('id') id: string,\n @Arg('comment', { nullable: true }) comment: string,\n @Ctx() context: ResolverContext\n ): Promise<Figure> {\n const { domain, user, tx } = context.state\n const repository = tx.getRepository(Figure)\n const historyRepository = tx.getRepository(FigureHistory)\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 if (figure.state === 'released') {\n throw new Error(`Figure '${figure.type}' 은 이미 발행되어 있다. 고쳐서 다시 내려면 초안으로 되돌린 뒤 발행한다`)\n }\n\n /*\n ── 게이트는 여기 있어야 한다 ────────────────────────────────────────────\n 화면도 같은 판정을 미리 물어(`inspectFigure`) 이유를 보여 주지만, **막는 자리는 이곳뿐이다.**\n 물어본 뒤 정본이 바뀌었을 수 있고, 화면을 거치지 않는 부름도 있다.\n\n 막는 이유는 사유와 함께 낸다. 「발행할 수 없습니다」로 뭉치면 무엇을 고쳐야 하는지 모른다.\n */\n /*\n 직전 발행판을 먼저 꺼낸다. 판 번호를 세는 데도 쓰고, **약속을 깨는 발행인가**를 견주는 데도\n 쓴다(ADR-0063 결정 3) — 도면은 type 만 적고 늘 최신 발행판을 그리므로, 다시 발행하는 것은\n 그 type 을 놓아 둔 모든 도면에게 같은 약속을 다시 하는 일이다.\n */\n const released = await historyRepository\n .createQueryBuilder('history')\n .where('history.originalId = :id', { id: figure.id })\n .orderBy('history.version', 'DESC')\n .limit(1)\n .getOne()\n\n const inspection = inspectSource(figure.source, released?.source)\n if (inspection.blocked) {\n const reasons = inspection.findings\n .filter(finding => finding.blocking)\n .map(finding => (finding.at ? `${finding.message} (${finding.at})` : finding.message))\n\n throw new Error(`Figure '${figure.type}' 은 아직 발행할 수 없다 — ${reasons.join(' · ')}`)\n }\n\n const nextVersion = released?.version ? Number(released.version) + 1 : 1\n\n return await repository.save({\n /*\n `domain` 을 명시로 넣는다 — 읽어 온 엔티티에는 관계가 안 실려 있다.\n\n 판본 스냅샷은 넘겨받은 엔티티를 베끼므로, 여기서 빠지면 판본의 도메인이 비고 그 판은\n 도메인으로 거르는 조회에 **한 줄도 안 걸린다**(실제로 그랬다). board 의 저장들도 같은\n 자리에서 domain 을 함께 넣는다.\n */\n domain,\n ...figure,\n version: nextVersion,\n state: 'released',\n updater: user,\n /* 판본 구독자가 이것을 판본의 comment 로 옮긴다. Figure 의 칸이 아니다. */\n __releaseComment: comment ?? null\n } as any)\n }\n\n /**\n * 옛 판으로 되돌린다.\n *\n * ## 무엇이 돌아오나\n *\n * 그 판의 **정본과 속성 정의, 그리고 그림**이다. 이름·설명·분류는 그대로 둔다 — 그것들은 언제든\n * 바꿔도 안전한 카탈로그의 말이고, 되돌리기가 개명까지 되돌리면 사람이 시키지 않은 일을 한다.\n *\n * ## 되돌려도 발행은 아니다\n *\n * 초안으로 돌아온다. 다시 내보내려면 발행을 한 번 더 하고, 그때 판 번호는 **이미 쓴 번호 다음**을\n * 받는다 — 3판을 되살렸다고 다시 3판이 되지 않는다. board 도 같다.\n *\n * ## 비용은 다시 잰다\n *\n * 판본이 든 점수·삼각형은 **그때의 형식**으로 잰 값이다. 지금 이 도형이 그 내용이 되었으므로\n * 오늘 기준으로 다시 계산한다. 옛 값은 판본에 그대로 남아 있다.\n */\n @Directive('@transaction')\n @Directive('@privilege(category: \"figure\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => FigureSaveResult, { description: 'Reverts a figure to one of its released versions. It comes back as a draft.' })\n async revertFigureVersion(\n @Arg('id') id: string,\n @Arg('version') version: number,\n @Ctx() context: ResolverContext\n ): Promise<FigureSaveResult> {\n const { domain, user, tx } = context.state\n const repository = tx.getRepository(Figure)\n const historyRepository = tx.getRepository(FigureHistory)\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 const history = await historyRepository.findOne({\n where: { domain: { id: In([domain.id, domain.parentId].filter(Boolean)) }, originalId: id, version }\n })\n if (!history) {\n throw new Error(`Figure '${figure.type}' 에 ${version} 판이 없다`)\n }\n\n /* 형식 검사도 다시 받는다 — 옛 판이 오늘의 형식을 어길 수 있고, 그것을 조용히 들이지 않는다. */\n const { source, derived, violations } = parseSource(history.source)\n\n const saved = await repository.save({\n /*\n `domain` 을 명시로 넣는다 — 읽어 온 엔티티에는 관계가 안 실려 있다.\n\n 판본 스냅샷은 넘겨받은 엔티티를 베끼므로, 여기서 빠지면 판본의 도메인이 비고 그 판은\n 도메인으로 거르는 조회에 **한 줄도 안 걸린다**(실제로 그랬다). board 의 저장들도 같은\n 자리에서 domain 을 함께 넣는다.\n */\n domain,\n ...figure,\n source,\n ...derived,\n properties: history.properties,\n thumbnail: history.thumbnail,\n thumbnailUpdatedAt: history.thumbnail === figure.thumbnail ? figure.thumbnailUpdatedAt : new Date(),\n state: 'draft',\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 refine: request.refine,\n feedback: request.feedback,\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 quality: {\n ...result.quality,\n visualRegions: result.quality.visual.regions,\n visualCoverage: result.quality.visual.coverage,\n visualReadability: result.quality.visual.readability\n },\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"]}
|
|
@@ -48,6 +48,11 @@ const figure_quality_js_1 = require("./figure-quality.js");
|
|
|
48
48
|
*/
|
|
49
49
|
/** 몇 번까지 다시 시키나. 이보다 늘려도 대개 같은 자리에서 막힌다. */
|
|
50
50
|
const MAX_ATTEMPTS = 3;
|
|
51
|
+
/*
|
|
52
|
+
* Figure 후보는 새로 만들든 기존 것을 고치든 완결된 정본 하나를 JSON으로 낸다. 기본 4,096
|
|
53
|
+
* 토큰으로는 부품 수가 조금만 늘어도 JSON이 중간에서 끊겨 닫는 괄호를 잃는다.
|
|
54
|
+
*/
|
|
55
|
+
const FIGURE_GENERATE_MAX_TOKENS = 16384;
|
|
51
56
|
/** 못 만들었을 때. 조용히 빈 것을 돌려주지 않는다. */
|
|
52
57
|
class ProposeFailure extends Error {
|
|
53
58
|
constructor(message,
|
|
@@ -370,7 +375,10 @@ async function proposeFigure(input) {
|
|
|
370
375
|
// 코드 울타리나 앞뒤 설명을 우리가 걷어내지 않아도 된다.
|
|
371
376
|
let answer;
|
|
372
377
|
try {
|
|
373
|
-
answer = await client.generateJSON(messages, {
|
|
378
|
+
answer = await client.generateJSON(messages, {
|
|
379
|
+
systemPrompt: system,
|
|
380
|
+
maxTokens: FIGURE_GENERATE_MAX_TOKENS
|
|
381
|
+
});
|
|
374
382
|
}
|
|
375
383
|
catch (e) {
|
|
376
384
|
// 모델이 JSON 을 못 냈다. 그것도 사유이므로 돌려주고 다시 시킨다.
|