@things-factory/figure-service 10.1.66 → 10.1.67

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 (60) hide show
  1. package/README.md +5 -8
  2. package/dist-server/service/figure/figure-format.d.ts +49 -31
  3. package/dist-server/service/figure/figure-format.js +47 -90
  4. package/dist-server/service/figure/figure-format.js.map +1 -1
  5. package/dist-server/service/figure/figure-history.js +1 -1
  6. package/dist-server/service/figure/figure-history.js.map +1 -1
  7. package/dist-server/service/figure/figure-mutation.js +5 -5
  8. package/dist-server/service/figure/figure-mutation.js.map +1 -1
  9. package/dist-server/service/figure/figure-propose-v3.d.ts +1 -1
  10. package/dist-server/service/figure/figure-propose-v3.js +1 -1
  11. package/dist-server/service/figure/figure-propose-v3.js.map +1 -1
  12. package/dist-server/service/figure/figure-query.d.ts +6 -11
  13. package/dist-server/service/figure/figure-query.js +12 -40
  14. package/dist-server/service/figure/figure-query.js.map +1 -1
  15. package/dist-server/service/figure/figure-rename.js +2 -11
  16. package/dist-server/service/figure/figure-rename.js.map +1 -1
  17. package/dist-server/service/figure/figure-tools.js +32 -112
  18. package/dist-server/service/figure/figure-tools.js.map +1 -1
  19. package/dist-server/service/figure/figure-type.d.ts +0 -17
  20. package/dist-server/service/figure/figure-type.js +4 -71
  21. package/dist-server/service/figure/figure-type.js.map +1 -1
  22. package/dist-server/service/figure/figure-v3-write.d.ts +1 -1
  23. package/dist-server/service/figure/figure-v3-write.js +2 -3
  24. package/dist-server/service/figure/figure-v3-write.js.map +1 -1
  25. package/dist-server/service/figure/figure.d.ts +5 -10
  26. package/dist-server/service/figure/figure.js +6 -11
  27. package/dist-server/service/figure/figure.js.map +1 -1
  28. package/dist-server/service/index.d.ts +1 -1
  29. package/dist-server/service/index.js +1 -1
  30. package/dist-server/service/index.js.map +1 -1
  31. package/dist-server/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +3 -3
  33. package/server/service/figure/figure-blueprint-query.test.ts +29 -48
  34. package/server/service/figure/figure-format.test.ts +54 -110
  35. package/server/service/figure/figure-format.ts +77 -87
  36. package/server/service/figure/figure-history.ts +1 -1
  37. package/server/service/figure/figure-inspect-db.test.ts +1 -11
  38. package/server/service/figure/figure-mutation.ts +7 -7
  39. package/server/service/figure/figure-propose-v3.test.ts +2 -2
  40. package/server/service/figure/figure-propose-v3.ts +1 -1
  41. package/server/service/figure/figure-query.ts +14 -36
  42. package/server/service/figure/figure-rename-db.test.ts +1 -1
  43. package/server/service/figure/figure-rename.test.ts +4 -18
  44. package/server/service/figure/figure-rename.ts +2 -12
  45. package/server/service/figure/figure-tools.test.ts +17 -80
  46. package/server/service/figure/figure-tools.ts +30 -125
  47. package/server/service/figure/figure-type.ts +3 -52
  48. package/server/service/figure/figure-v3-write.ts +3 -4
  49. package/server/service/figure/figure.ts +7 -12
  50. package/server/service/index.ts +1 -1
  51. package/dist-server/service/figure/figure-gate.d.ts +0 -93
  52. package/dist-server/service/figure/figure-gate.js +0 -302
  53. package/dist-server/service/figure/figure-gate.js.map +0 -1
  54. package/dist-server/service/figure/figure-source.d.ts +0 -45
  55. package/dist-server/service/figure/figure-source.js +0 -59
  56. package/dist-server/service/figure/figure-source.js.map +0 -1
  57. package/server/service/figure/figure-gate.test.ts +0 -155
  58. package/server/service/figure/figure-gate.ts +0 -392
  59. package/server/service/figure/figure-source.test.ts +0 -149
  60. package/server/service/figure/figure-source.ts +0 -94
@@ -1,302 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.inspectSource = inspectSource;
4
- const figure_model_1 = require("@hatiolab/figure-model");
5
- /**
6
- * Figure model publishes the same capability for direct model consumers. Keep the service calculation
7
- * here as well because this package is deployed independently and receives a compiled local model package.
8
- * Each alternative is remeasured; prose from `how` is never parsed into an edit.
9
- */
10
- function measuredFixes(source, factor) {
11
- const baseline = (0, figure_model_1.sizingReport)((0, figure_model_1.compile)(source), factor);
12
- const baselineCount = baseline.findings.filter(finding => finding.code === 'sizing-splits').length;
13
- const seen = new Set();
14
- const fixes = [];
15
- for (const finding of baseline.findings) {
16
- if (finding.code !== 'sizing-splits' || !finding.way)
17
- continue;
18
- const axes = finding.way.split('-').filter((axis) => ['x', 'y', 'z'].includes(axis));
19
- const names = new Set(finding.clumps.flat());
20
- for (const part of source.parts) {
21
- if (!names.has(part.name))
22
- continue;
23
- for (const axis of axes) {
24
- for (const to of ['min', 'center', 'max', 'span']) {
25
- if (part.anchor?.[axis] === to)
26
- continue;
27
- const changed = {
28
- ...source,
29
- parts: source.parts.map(one => one.name === part.name ? { ...one, anchor: { ...(one.anchor ?? {}), [axis]: to } } : one)
30
- };
31
- const afterCount = (0, figure_model_1.sizingReport)((0, figure_model_1.compile)(changed), factor).findings.filter(one => one.code === 'sizing-splits').length;
32
- const resolved = baselineCount - afterCount;
33
- const key = `${finding.way}:${part.name}:${axis}:${to}`;
34
- if (resolved <= 0 || seen.has(key))
35
- continue;
36
- seen.add(key);
37
- fixes.push({
38
- findingWay: finding.way,
39
- part: part.name,
40
- axis,
41
- from: part.anchor?.[axis],
42
- to,
43
- resolved,
44
- /* 한 finding만 없앤 것이 아니라 전수 검사에서 막는 split이 0일 때만 원클릭 후보가 된다. */
45
- safe: afterCount === 0
46
- });
47
- }
48
- }
49
- }
50
- }
51
- return fixes.sort((a, b) => b.resolved - a.resolved || a.part.localeCompare(b.part)).slice(0, 12);
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
- }
183
- /**
184
- * 정본 하나를 판정한다.
185
- *
186
- * 정본이 없거나(아직 저장 전) JSON 이 아니면 **막는다** — 발행할 대상이 없는 것이다. 그 경우를
187
- * 조용히 통과시키면 빈 것이 발행된다.
188
- *
189
- * `releasedJson` 은 **직전 발행판의 정본**이다. 주면 그것과 견주어 「이미 놓인 인스턴스를 깨는
190
- * 발행인가」를 함께 낸다(ADR-0063 결정 3). 한 번도 발행된 적 없으면 견줄 것이 없으므로 안 준다.
191
- */
192
- function inspectSource(sourceJson, releasedJson) {
193
- if (!sourceJson) {
194
- return {
195
- blocked: true,
196
- findings: [
197
- {
198
- code: 'no-source',
199
- message: '저장된 정본이 없다. 저장하면 발행할 수 있다',
200
- blocking: true
201
- }
202
- ]
203
- };
204
- }
205
- let parsed;
206
- try {
207
- parsed = JSON.parse(sourceJson);
208
- }
209
- catch (e) {
210
- return {
211
- blocked: true,
212
- findings: [{ code: 'source-not-json', message: `정본이 JSON 이 아니다: ${e.message}`, blocking: true }]
213
- };
214
- }
215
- const result = (0, figure_model_1.validate)(parsed);
216
- const findings = [];
217
- /*
218
- 형식 오류는 **저장을 통과할 수 없다**(`parseSource` 가 거절한다). 그래도 여기서 다시 보는 이유는,
219
- 이 판정이 저장 경로와 독립이어야 하기 때문이다 — 옛 정본이 남아 있거나 형식이 엄해진 뒤라면
220
- 저장 때는 맞았던 것이 지금은 틀릴 수 있다. 그때 발행이 통과하면 안 된다.
221
- */
222
- for (const error of result.errors) {
223
- findings.push({ code: error.code, message: error.message, at: error.path, blocking: true });
224
- }
225
- for (const violation of result.violations) {
226
- findings.push({
227
- code: violation.code,
228
- message: violation.message,
229
- /*
230
- 왜·어떻게는 형식이 만든 것을 그대로 옮긴다. 여기서 서버가 다시 쓰면 두 벌이 되고 곧 어긋난다 —
231
- 크기 반응 판정이 이미 그 규율이다(아래 `report.findings`).
232
-
233
- 이 두 줄이 없던 동안 발행 화면은 막는 위반에 문장 하나만 보여 줬다. 표본 47 개를 게이트에 넣어
234
- 보니 막는 판정 99 건 중 왜·어떻게를 가진 것이 2 건이었고, 나머지 97 건은 저작자가 상자를 키울지
235
- 부품을 옮길지 알 길이 없었다(2026-09-19, figure-model 0.1.29 가 그 값을 싣기 시작했다).
236
- */
237
- why: violation.why,
238
- how: violation.how,
239
- /* 어느 위반이 막나는 형식이 안다 — 여기 목록을 또 들면 둘이 갈린다(ADR-0044 결정 ②). */
240
- blocking: (0, figure_model_1.blocksRelease)(violation.code)
241
- });
242
- }
243
- if (result.errors.length === 0) {
244
- const source = parsed;
245
- const report = (0, figure_model_1.sizingReport)((0, figure_model_1.compile)(source));
246
- const fixes = measuredFixes(source, report.factor);
247
- const fixPlan = measuredFixPlan(source, report.factor);
248
- for (const finding of report.findings) {
249
- findings.push({
250
- code: finding.code,
251
- message: finding.message,
252
- /* 왜·어떻게는 형식이 소유한다. 서버가 다시 쓰면 두 벌이 되고 곧 어긋난다. */
253
- why: finding.why,
254
- how: finding.how,
255
- /* 숫자가 아니라 자리를 말한다 — 어느 부품들이 갈라져 나왔나. */
256
- at: finding.clumps.map(clump => [...clump].sort().join('+')).join(' | ') || undefined,
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,
260
- blocking: finding.code === 'sizing-splits'
261
- });
262
- }
263
- findings.push(...contractFindings(source, releasedJson));
264
- }
265
- return { findings, blocked: findings.some(finding => finding.blocking) };
266
- }
267
- /**
268
- * 직전 발행판과 견준 것 — **약속을 깨는 발행인가**(ADR-0063 결정 3).
269
- *
270
- * 무엇이 달라졌고 그것이 막는 것인지는 형식이 정한다(`contractChanges`). 여기서는 화면과 발행 문이
271
- * 읽는 모양으로 옮기기만 한다.
272
- *
273
- * **앞 판을 오늘 형식으로 못 읽으면 조용히 넘어가지 않는다.** 판 1 은 오늘 형식이 거절하는 판으로
274
- * 적혀 있을 수 있고, 그때 견주기를 건너뛰면 아무 말 없이 관문이 없는 것이 된다. 막지는 않는다 —
275
- * 그러면 그 도형은 영영 다시 발행할 수 없다.
276
- */
277
- function contractFindings(source, releasedJson) {
278
- if (!releasedJson)
279
- return [];
280
- let released;
281
- try {
282
- released = JSON.parse(releasedJson);
283
- return (0, figure_model_1.contractChanges)(released, source).map(change => ({
284
- code: change.code,
285
- message: change.message,
286
- why: change.why,
287
- at: change.subject,
288
- blocking: change.blocking
289
- }));
290
- }
291
- catch (e) {
292
- return [
293
- {
294
- code: 'released-unreadable',
295
- message: `직전 발행판을 오늘 형식으로 읽을 수 없어 견주지 못했다: ${e.message}`,
296
- why: '이 발행이 이미 놓인 인스턴스를 깨는지는 확인되지 않았다.',
297
- blocking: false
298
- }
299
- ];
300
- }
301
- }
302
- //# sourceMappingURL=figure-gate.js.map
@@ -1 +0,0 @@
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"]}
@@ -1,45 +0,0 @@
1
- import { FigureFinding } from './figure-type.js';
2
- /**
3
- * 정본을 받아 **검증하고 파생값을 계산한다.**
4
- *
5
- * ## 왜 서버가 검증하나
6
- *
7
- * 저작 화면을 못 믿어서가 아니다. **저작 화면을 거치지 않는 길이 있다** — 가져오기,
8
- * API 직접 호출, AI 어시스턴트. 그 길로 들어온 정본이 형식을 어기면 도면에서 조용히
9
- * 안 그려진다. 들어오는 문에서 막는다.
10
- *
11
- * ## 막는 것과 알리는 것
12
- *
13
- * `figure-model` 이 두 단계로 나눠 놓았고, 서버가 그 경계를 다시 정하지 않는다.
14
- *
15
- * errors 형식을 어긴 것. **거절한다**
16
- * violations 한도를 넘은 것. 거절하지 않고 **함께 돌려준다**
17
- *
18
- * ## 파생값은 서버가 계산한다
19
- *
20
- * `score`·`triangles`·`groups` 를 클라이언트에서 받지 않는다. 받으면 거짓이 들어오고
21
- * 목록 화면이 실물과 다른 것을 보여 준다. 정본에서 계산한다.
22
- */
23
- export interface ParsedSource {
24
- /** 저장할 정본 JSON 문자열. 들어온 것을 그대로 쓴다 — 단, version 누락 시에만 정규화 직렬화한다. */
25
- source: string;
26
- /** 목록 화면이 정렬·필터에 쓰는 파생값. */
27
- derived: {
28
- score: number;
29
- triangles: number;
30
- groups: number;
31
- };
32
- /** 막지 않는 발견. 호출한 쪽이 응답에 실어 보낸다. */
33
- violations: FigureFinding[];
34
- }
35
- /** 정본이 형식을 어겼다. 저장하지 않는다. */
36
- export declare class FigureSourceError extends Error {
37
- readonly findings: FigureFinding[];
38
- constructor(findings: FigureFinding[]);
39
- }
40
- /**
41
- * 정본 JSON 문자열을 검증하고 파생값을 계산한다.
42
- *
43
- * @throws {FigureSourceError} JSON 이 아니거나 형식을 어긴 경우
44
- */
45
- export declare function parseSource(sourceJson: string): ParsedSource;
@@ -1,59 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FigureSourceError = void 0;
4
- exports.parseSource = parseSource;
5
- const figure_model_1 = require("@hatiolab/figure-model");
6
- /** 정본이 형식을 어겼다. 저장하지 않는다. */
7
- class FigureSourceError extends Error {
8
- constructor(findings) {
9
- super(`정본이 형식을 어겼다 — ${findings.length}건: ${findings.map(f => f.code).join(', ')}`);
10
- this.name = 'FigureSourceError';
11
- this.findings = findings;
12
- }
13
- }
14
- exports.FigureSourceError = FigureSourceError;
15
- /**
16
- * 정본 JSON 문자열을 검증하고 파생값을 계산한다.
17
- *
18
- * @throws {FigureSourceError} JSON 이 아니거나 형식을 어긴 경우
19
- */
20
- function parseSource(sourceJson) {
21
- let parsed;
22
- try {
23
- parsed = JSON.parse(sourceJson);
24
- }
25
- catch (e) {
26
- throw new FigureSourceError([
27
- { code: 'source-not-json', message: `정본이 JSON 이 아니다: ${e.message}`, at: 'source' }
28
- ]);
29
- }
30
- const result = (0, figure_model_1.validate)(parsed);
31
- if (result.errors.length > 0) {
32
- throw new FigureSourceError(result.errors.map(e => ({ code: e.code, message: e.message, at: e.path })));
33
- }
34
- /*
35
- No version is stamped here. validate has already refused a source without one: every source saved
36
- without a version was written with y measured from the box centre, and stamping the current
37
- edition on it would move every part (ADR-0065).
38
- */
39
- const normalizedSource = sourceJson;
40
- // 여기까지 왔으면 형식은 맞다. 청사진을 만들어 셈에 쓴다.
41
- // 청사진 자체는 **저장하지 않는다** — 파생 캐시이므로 소비하는 쪽이 다시 만든다.
42
- const blueprint = (0, figure_model_1.compile)(parsed);
43
- const cost = (0, figure_model_1.costOf)(blueprint, 1);
44
- const score = (0, figure_model_1.scoreOf)(blueprint);
45
- return {
46
- source: normalizedSource,
47
- derived: {
48
- score: score.score,
49
- triangles: cost.triangles,
50
- groups: cost.groups
51
- },
52
- violations: result.violations.map(v => ({
53
- code: v.code,
54
- message: v.message,
55
- at: undefined
56
- }))
57
- };
58
- }
59
- //# sourceMappingURL=figure-source.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"figure-source.js","sourceRoot":"","sources":["../../../server/service/figure/figure-source.ts"],"names":[],"mappings":";;;AAkDA,kCA2CC;AA7FD,yDAA2E;AAkC3E,6BAA6B;AAC7B,MAAa,iBAAkB,SAAQ,KAAK;IAG1C,YAAY,QAAyB;QACnC,KAAK,CAAC,iBAAiB,QAAQ,CAAC,MAAM,MAAM,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QACnF,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAA;QAC/B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;IAC1B,CAAC;CACF;AARD,8CAQC;AAED;;;;GAIG;AACH,SAAgB,WAAW,CAAC,UAAkB;IAC5C,IAAI,MAAe,CAAA;IACnB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;IACjC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,IAAI,iBAAiB,CAAC;YAC1B,EAAE,IAAI,EAAE,iBAAiB,EAAE,OAAO,EAAE,mBAAoB,CAAW,CAAC,OAAO,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE;SAC9F,CAAC,CAAA;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,IAAA,uBAAQ,EAAC,MAAM,CAAC,CAAA;IAC/B,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,iBAAiB,CACzB,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAC3E,CAAA;IACH,CAAC;IAED;;;;MAIE;IACF,MAAM,gBAAgB,GAAG,UAAU,CAAA;IAEnC,mCAAmC;IACnC,kDAAkD;IAClD,MAAM,SAAS,GAAG,IAAA,sBAAO,EAAC,MAAa,CAAC,CAAA;IACxC,MAAM,IAAI,GAAG,IAAA,qBAAM,EAAC,SAAS,EAAE,CAAC,CAAC,CAAA;IACjC,MAAM,KAAK,GAAG,IAAA,sBAAO,EAAC,SAAS,CAAC,CAAA;IAEhC,OAAO;QACL,MAAM,EAAE,gBAAgB;QACxB,OAAO,EAAE;YACP,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,MAAM,EAAE,IAAI,CAAC,MAAM;SACpB;QACD,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACtC,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,OAAO,EAAE,CAAC,CAAC,OAAO;YAClB,EAAE,EAAE,SAAS;SACd,CAAC,CAAC;KACJ,CAAA;AACH,CAAC","sourcesContent":["import { compile, costOf, scoreOf, validate } from '@hatiolab/figure-model'\n\nimport { FigureFinding } from './figure-type.js'\n\n/**\n * 정본을 받아 **검증하고 파생값을 계산한다.**\n *\n * ## 왜 서버가 검증하나\n *\n * 저작 화면을 못 믿어서가 아니다. **저작 화면을 거치지 않는 길이 있다** — 가져오기,\n * API 직접 호출, AI 어시스턴트. 그 길로 들어온 정본이 형식을 어기면 도면에서 조용히\n * 안 그려진다. 들어오는 문에서 막는다.\n *\n * ## 막는 것과 알리는 것\n *\n * `figure-model` 이 두 단계로 나눠 놓았고, 서버가 그 경계를 다시 정하지 않는다.\n *\n * errors 형식을 어긴 것. **거절한다**\n * violations 한도를 넘은 것. 거절하지 않고 **함께 돌려준다**\n *\n * ## 파생값은 서버가 계산한다\n *\n * `score`·`triangles`·`groups` 를 클라이언트에서 받지 않는다. 받으면 거짓이 들어오고\n * 목록 화면이 실물과 다른 것을 보여 준다. 정본에서 계산한다.\n */\nexport interface ParsedSource {\n /** 저장할 정본 JSON 문자열. 들어온 것을 그대로 쓴다 — 단, version 누락 시에만 정규화 직렬화한다. */\n source: string\n /** 목록 화면이 정렬·필터에 쓰는 파생값. */\n derived: { score: number; triangles: number; groups: number }\n /** 막지 않는 발견. 호출한 쪽이 응답에 실어 보낸다. */\n violations: FigureFinding[]\n}\n\n/** 정본이 형식을 어겼다. 저장하지 않는다. */\nexport class FigureSourceError extends Error {\n readonly findings: FigureFinding[]\n\n constructor(findings: FigureFinding[]) {\n super(`정본이 형식을 어겼다 — ${findings.length}건: ${findings.map(f => f.code).join(', ')}`)\n this.name = 'FigureSourceError'\n this.findings = findings\n }\n}\n\n/**\n * 정본 JSON 문자열을 검증하고 파생값을 계산한다.\n *\n * @throws {FigureSourceError} JSON 이 아니거나 형식을 어긴 경우\n */\nexport function parseSource(sourceJson: string): ParsedSource {\n let parsed: unknown\n try {\n parsed = JSON.parse(sourceJson)\n } catch (e) {\n throw new FigureSourceError([\n { code: 'source-not-json', message: `정본이 JSON 이 아니다: ${(e as Error).message}`, at: 'source' }\n ])\n }\n\n const result = validate(parsed)\n if (result.errors.length > 0) {\n throw new FigureSourceError(\n result.errors.map(e => ({ code: e.code, message: e.message, at: e.path }))\n )\n }\n\n /*\n No version is stamped here. validate has already refused a source without one: every source saved\n without a version was written with y measured from the box centre, and stamping the current\n edition on it would move every part (ADR-0065).\n */\n const normalizedSource = sourceJson\n\n // 여기까지 왔으면 형식은 맞다. 청사진을 만들어 셈에 쓴다.\n // 청사진 자체는 **저장하지 않는다** — 파생 캐시이므로 소비하는 쪽이 다시 만든다.\n const blueprint = compile(parsed as any)\n const cost = costOf(blueprint, 1)\n const score = scoreOf(blueprint)\n\n return {\n source: normalizedSource,\n derived: {\n score: score.score,\n triangles: cost.triangles,\n groups: cost.groups\n },\n violations: result.violations.map(v => ({\n code: v.code,\n message: v.message,\n at: undefined\n }))\n }\n}\n"]}
@@ -1,155 +0,0 @@
1
- import { inspectSource } from './figure-gate'
2
-
3
- describe('release-gate repair candidates', () => {
4
- it('returns data, not parsed prose, for a measured sizing split', () => {
5
- const source = {
6
- version: 2,
7
- type: 'STACK',
8
- base: { x: 100, y: 200, z: 100 },
9
- detailLevel: 'M',
10
- parts: [
11
- {
12
- name: 'foot', primitive: 'cube',
13
- transform: { position: { x: 0, y: 50, z: 0 }, size: { x: 100, y: 100, z: 100 } },
14
- material: { token: 'palette.primary' }, anchor: { y: 'min' }
15
- },
16
- {
17
- name: 'lamp', primitive: 'cube',
18
- transform: { position: { x: 0, y: 110, z: 0 }, size: { x: 60, y: 20, z: 60 } },
19
- material: { token: 'palette.accent' }, anchor: { y: 'max' }
20
- }
21
- ]
22
- }
23
-
24
- const inspection = inspectSource(JSON.stringify(source))
25
- const split = inspection.findings.find(finding => finding.code === 'sizing-splits')
26
-
27
- expect(inspection.blocked).toBe(true)
28
- expect(split?.fixes).toEqual(expect.arrayContaining([
29
- expect.objectContaining({ part: expect.any(String), axis: 'y', to: expect.any(String), resolved: expect.any(Number), safe: expect.any(Boolean) })
30
- ]))
31
- expect(split?.fixes?.every(fix => fix.resolved > 0)).toBe(true)
32
- })
33
-
34
- it('offers one coordinated draft repair only after the full sizing suite clears', () => {
35
- const source = {
36
- version: 2,
37
- type: 'STACK',
38
- base: { x: 100, y: 200, z: 100 },
39
- detailLevel: 'M',
40
- parts: [
41
- { name: 'body', primitive: 'cube', transform: { position: { x: 0, y: 50, z: 0 }, size: { x: 100, y: 100, z: 100 } }, material: { token: 'palette.primary' }, anchor: { y: 'min' } },
42
- { name: 'cap-a', primitive: 'cube', transform: { position: { x: -25, y: 110, z: 0 }, size: { x: 20, y: 20, z: 20 } }, material: { token: 'palette.accent' }, anchor: { y: 'max' } },
43
- { name: 'cap-b', primitive: 'cube', transform: { position: { x: 25, y: 110, z: 0 }, size: { x: 20, y: 20, z: 20 } }, material: { token: 'palette.accent' }, anchor: { y: 'max' } }
44
- ]
45
- }
46
-
47
- const split = inspectSource(JSON.stringify(source)).findings.find(finding => finding.code === 'sizing-splits')
48
-
49
- expect(split?.fixPlan).toEqual(expect.objectContaining({ safe: true, resolved: expect.any(Number) }))
50
- expect(split?.fixPlan?.fixes.length).toBeGreaterThan(0)
51
- expect(split?.fixPlan?.fixes.every(fix => fix.safe)).toBe(true)
52
- })
53
- })
54
-
55
- /*
56
- * The base box is a promise to whoever places this figure.
57
- *
58
- * `base` is the number an instance is scaled by (`instanceScale = width/base.x`) and the placement
59
- * height comes out of `base.y`. A figure whose box disagrees with its parts renders at the wrong
60
- * size and the wrong height the moment it is placed, so publishing it breaks that promise
61
- * even though the source itself is well formed.
62
- *
63
- * Saving is still allowed — these are violations, not errors — so a figure already standing on a
64
- * drawing keeps standing. It just cannot be published again until it is fixed.
65
- */
66
- describe('release gate — the base box', () => {
67
- /** A figure that sits correctly in its box: 200 tall, foot on the floor at 0. */
68
- const sound = {
69
- version: 2,
70
- type: 'POST',
71
- base: { x: 100, y: 200, z: 100 },
72
- detailLevel: 'M',
73
- parts: [
74
- {
75
- name: 'post', primitive: 'cube',
76
- transform: { position: { x: 0, y: 50, z: 0 }, size: { x: 100, y: 100, z: 100 } },
77
- material: { token: 'palette.primary' }
78
- }
79
- ]
80
- }
81
-
82
- const inspect = (source: unknown) => inspectSource(JSON.stringify(source))
83
-
84
- it('lets a sound figure through', () => {
85
- const inspection = inspect(sound)
86
- expect(inspection.blocked).toBe(false)
87
- })
88
-
89
- it('refuses a part that overflows the box', () => {
90
- const over = { ...sound, parts: [{ ...sound.parts[0], transform: { position: { x: 0, y: -20, z: 0 }, size: { x: 100, y: 100, z: 100 } } }] }
91
- const inspection = inspect(over)
92
- const finding = inspection.findings.find(f => f.code === 'part-outside-base')
93
-
94
- expect(finding?.blocking).toBe(true)
95
- expect(inspection.blocked).toBe(true)
96
- })
97
-
98
- it('refuses a figure that does not reach the face its placement names', () => {
99
- const floating = { ...sound, parts: [{ ...sound.parts[0], transform: { position: { x: 0, y: 100, z: 0 }, size: { x: 100, y: 100, z: 100 } } }] }
100
- const inspection = inspect(floating)
101
- const finding = inspection.findings.find(f => f.code === 'parts-off-placement-face')
102
-
103
- expect(finding?.blocking).toBe(true)
104
- expect(inspection.blocked).toBe(true)
105
- })
106
-
107
- /*
108
- The gate hands the screen what the format said, and for blocking violations it was handing only
109
- the sentence: `why` and `how` exist on GateFinding, the release panel renders both, and the query
110
- asks for them, but nothing filled them. Across the 47 sample figures, 99 blocking findings had
111
- 2 with all three -- the author saw "'beacon' overflows the box on y" and had to guess whether to
112
- grow the box or move the part.
113
-
114
- The format owns both (figure-model 0.1.29). This checks the gate carries them rather than writing
115
- its own, which is the same discipline the sizing findings already follow.
116
- */
117
- it('carries the reason and the remedy the format wrote', () => {
118
- const over = { ...sound, parts: [{ ...sound.parts[0], transform: { position: { x: 0, y: -20, z: 0 }, size: { x: 100, y: 100, z: 100 } } }] }
119
- const finding = inspect(over).findings.find(f => f.code === 'part-outside-base')
120
-
121
- expect(finding?.message).toMatch(/mm/)
122
- expect(finding?.why).toBeTruthy()
123
- /*
124
- 화면의 칸 이름으로 부른다 — 저작자는 `base.y` 를 본 적이 없고 Y 라고 적힌 칸을 본다. 이 부품은 바닥 아래로
125
- 넘었고, 기준 상자의 밑면은 Y 0 에 고정이라 상자 크기로는 닿지 않는다 — 고칠 길은 부품을 올리는 것뿐이다
126
- (figure-model 0.1.39, ADR-0065).
127
- */
128
- expect(finding?.how).toMatch(/Y 로 \d+(\.\d+)?mm 올리세요/)
129
- expect(finding?.how).not.toMatch(/기준 상자의 Y 를 \d+/)
130
- })
131
-
132
- it('carries them for the placement face too', () => {
133
- const floating = { ...sound, parts: [{ ...sound.parts[0], transform: { position: { x: 0, y: 100, z: 0 }, size: { x: 100, y: 100, z: 100 } } }] }
134
- const finding = inspect(floating).findings.find(f => f.code === 'parts-off-placement-face')
135
-
136
- expect(finding?.why).toBeTruthy()
137
- expect(finding?.how).toBeTruthy()
138
- })
139
-
140
- it('still only reports the violations that are about taste', () => {
141
- /*
142
- Slack in the box is a declaration, not a defect — clearance in front of a conveyor, the pitch
143
- of a rack bay. A roomy box with an odd segment count is told about and published.
144
- */
145
- const roomy = {
146
- ...sound,
147
- base: { x: 3000, y: 200, z: 3000 },
148
- parts: [{ ...sound.parts[0], primitive: 'cylinder', segments: 9 }]
149
- }
150
- const inspection = inspect(roomy)
151
-
152
- expect(inspection.findings.some(f => f.code === 'segments-off-preset')).toBe(true)
153
- expect(inspection.blocked).toBe(false)
154
- })
155
- })