poker-mcp 1.0.0
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/README.md +241 -0
- package/config/mcp-manifest.json +1713 -0
- package/config/package-lock.json +1335 -0
- package/config/poker_mcp.code-workspace +8 -0
- package/package.json +51 -0
- package/src/config/ConfigManager.js +120 -0
- package/src/config/default.json +62 -0
- package/src/mcp/handlers/bodyHandlers.js +63 -0
- package/src/mcp/handlers/buildupFactorHandlers.js +43 -0
- package/src/mcp/handlers/calculationHandlers.js +265 -0
- package/src/mcp/handlers/detectorHandlers.js +201 -0
- package/src/mcp/handlers/index.js +43 -0
- package/src/mcp/handlers/sourceHandlers.js +25 -0
- package/src/mcp/handlers/transformHandlers.js +26 -0
- package/src/mcp/handlers/unitHandlers.js +305 -0
- package/src/mcp/handlers/zoneHandlers.js +83 -0
- package/src/mcp/middleware/errorHandler.js +49 -0
- package/src/mcp/middleware/requestValidator.js +129 -0
- package/src/mcp/server.js +76 -0
- package/src/mcp/tools/bodyTools.js +185 -0
- package/src/mcp/tools/buildupFactorTools.js +82 -0
- package/src/mcp/tools/calculationTools.js +58 -0
- package/src/mcp/tools/commonTools.js +22 -0
- package/src/mcp/tools/detectorTools.js +131 -0
- package/src/mcp/tools/index.js +34 -0
- package/src/mcp/tools/sourceTools.js +726 -0
- package/src/mcp/tools/sourceTools_fixed_part2.js +41 -0
- package/src/mcp/tools/transformTools.js +73 -0
- package/src/mcp/tools/unitTools.js +137 -0
- package/src/mcp/tools/zoneTools.js +65 -0
- package/src/mcp_server_stdio_v4.js +45 -0
- package/src/routes/restApi.js +184 -0
- package/src/services/CalculationService.js +525 -0
- package/src/services/DataManager.js +738 -0
- package/src/services/TaskManager.js +1622 -0
- package/src/utils/MaterialAlternatives.js +189 -0
- package/src/utils/errorHandler.js +68 -0
- package/src/utils/errors.js +37 -0
- package/src/utils/logger.js +24 -0
- package/src/utils/logger_no_color.js +28 -0
- package/src/utils/mcpErrors.js +225 -0
- package/src/validators/CMBValidator.js +191 -0
- package/src/validators/DetectorValidator.js +512 -0
- package/src/validators/ManifestValidator.js +249 -0
- package/src/validators/NuclideValidator.js +171 -0
- package/src/validators/PhysicsValidator.js +401 -0
- package/src/validators/PhysicsValidatorSimple.js +87 -0
- package/src/validators/SourceValidator.js +515 -0
- package/src/validators/TransformValidator.js +472 -0
- package/src/validators/UnitValidator.js +562 -0
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
// services/DataManager.js
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import yaml from 'js-yaml';
|
|
5
|
+
import { logger } from '../utils/logger.js';
|
|
6
|
+
import { DataError } from '../utils/errors.js';
|
|
7
|
+
|
|
8
|
+
export class SafeDataManager {
|
|
9
|
+
constructor(yamlFile, pendingFile) {
|
|
10
|
+
this.yamlFile = yamlFile;
|
|
11
|
+
this.pendingFile = pendingFile;
|
|
12
|
+
this.backupDir = 'backups';
|
|
13
|
+
this.maxBackups = 10;
|
|
14
|
+
this.data = null;
|
|
15
|
+
this.pendingChanges = [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async initialize() {
|
|
19
|
+
try {
|
|
20
|
+
// 必要なフォルダをすべて作成
|
|
21
|
+
await fs.mkdir(this.backupDir, { recursive: true }); // backups
|
|
22
|
+
await fs.mkdir('logs', { recursive: true }); // logs
|
|
23
|
+
await fs.mkdir('tasks', { recursive: true }); // tasks
|
|
24
|
+
|
|
25
|
+
// 初期ファイル配置(既存ファイルがない場合のみ)
|
|
26
|
+
await this.ensureInitialFiles();
|
|
27
|
+
|
|
28
|
+
// データ読み込み
|
|
29
|
+
await this.loadData();
|
|
30
|
+
await this.loadPendingChanges();
|
|
31
|
+
|
|
32
|
+
logger.info('データマネージャーを初期化しました');
|
|
33
|
+
} catch (error) {
|
|
34
|
+
logger.error('データマネージャーの初期化に失敗しました', { error: error.message });
|
|
35
|
+
throw new DataError(`初期化に失敗: ${error.message}`, 'INITIALIZATION');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 新規メソッド: 初期ファイルの確実な配置
|
|
40
|
+
async ensureInitialFiles() {
|
|
41
|
+
try {
|
|
42
|
+
// poker.yamlの初期配置
|
|
43
|
+
await this.ensureYamlFile();
|
|
44
|
+
|
|
45
|
+
// pending_changes.jsonの初期配置
|
|
46
|
+
await this.ensurePendingFile();
|
|
47
|
+
|
|
48
|
+
} catch (error) {
|
|
49
|
+
logger.error('初期ファイル配置エラー', { error: error.message });
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async ensureYamlFile() {
|
|
55
|
+
try {
|
|
56
|
+
// ファイル存在チェック
|
|
57
|
+
await fs.access(this.yamlFile);
|
|
58
|
+
logger.info('YAMLファイルは既に存在します', { file: this.yamlFile });
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error.code === 'ENOENT') {
|
|
61
|
+
// ファイルが存在しない場合のみ作成
|
|
62
|
+
const initialData = this.createInitialYamlData();
|
|
63
|
+
await fs.writeFile(this.yamlFile, yaml.dump(initialData, { flowLevel: 1 }));
|
|
64
|
+
logger.info('初期YAMLファイルを作成しました', { file: this.yamlFile });
|
|
65
|
+
} else {
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async ensurePendingFile() {
|
|
72
|
+
try {
|
|
73
|
+
// ファイル存在チェック
|
|
74
|
+
await fs.access(this.pendingFile);
|
|
75
|
+
logger.info('保留変更ファイルは既に存在します', { file: this.pendingFile });
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error.code === 'ENOENT') {
|
|
78
|
+
// ファイルが存在しない場合のみ作成
|
|
79
|
+
await fs.writeFile(this.pendingFile, JSON.stringify([], null, 2));
|
|
80
|
+
logger.info('初期保留変更ファイルを作成しました', { file: this.pendingFile });
|
|
81
|
+
} else {
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
createInitialYamlData() {
|
|
88
|
+
return {
|
|
89
|
+
"unit": {
|
|
90
|
+
"length": "cm",
|
|
91
|
+
"angle": "radian",
|
|
92
|
+
"density": "g/cm3",
|
|
93
|
+
"radioactivity": "Bq"
|
|
94
|
+
},
|
|
95
|
+
"transform": [],
|
|
96
|
+
"body": [],
|
|
97
|
+
"zone": [
|
|
98
|
+
{
|
|
99
|
+
"body_name": "ATMOSPHERE",
|
|
100
|
+
"material": "VOID"
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
"buildup_factor": [],
|
|
104
|
+
"source": [],
|
|
105
|
+
"detector": []
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async loadData() {
|
|
110
|
+
try {
|
|
111
|
+
const data = await fs.readFile(this.yamlFile, 'utf8');
|
|
112
|
+
this.data = yaml.load(data);
|
|
113
|
+
|
|
114
|
+
// Unit セクション自動検証・修復
|
|
115
|
+
await this.validateAndRepairUnit();
|
|
116
|
+
|
|
117
|
+
logger.info('YAMLデータを読み込みました', { file: this.yamlFile });
|
|
118
|
+
} catch (error) {
|
|
119
|
+
// ensureInitialFiles()で初期ファイルが既に配置されているため、
|
|
120
|
+
// 通常はENOENTエラーは発生しないはず
|
|
121
|
+
logger.error('YAMLファイルの読み込みに失敗しました', { error: error.message });
|
|
122
|
+
throw new DataError(`YAML読み込みに失敗: ${error.message}`, 'LOAD_DATA');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async loadPendingChanges() {
|
|
127
|
+
try {
|
|
128
|
+
const data = await fs.readFile(this.pendingFile, 'utf8');
|
|
129
|
+
this.pendingChanges = JSON.parse(data || '[]');
|
|
130
|
+
logger.info('保留変更を読み込みました', { count: this.pendingChanges.length });
|
|
131
|
+
} catch (error) {
|
|
132
|
+
// ensureInitialFiles()で初期ファイルが既に配置されているため、
|
|
133
|
+
// 通常はENOENTエラーは発生しないはず
|
|
134
|
+
logger.error('保留変更の読み込みに失敗しました', { error: error.message });
|
|
135
|
+
throw new DataError(`保留変更読み込みに失敗: ${error.message}`, 'LOAD_PENDING');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async createBackup() {
|
|
140
|
+
try {
|
|
141
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
142
|
+
const backupFile = path.join(this.backupDir, `${path.basename(this.yamlFile)}-${timestamp}`);
|
|
143
|
+
await fs.copyFile(this.yamlFile, backupFile);
|
|
144
|
+
|
|
145
|
+
// 古いバックアップを削除
|
|
146
|
+
await this.cleanupBackups();
|
|
147
|
+
|
|
148
|
+
logger.info('バックアップを作成しました', { backupFile });
|
|
149
|
+
} catch (error) {
|
|
150
|
+
logger.error('バックアップの作成に失敗しました', { error: error.message });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async cleanupBackups() {
|
|
155
|
+
try {
|
|
156
|
+
const files = await fs.readdir(this.backupDir);
|
|
157
|
+
const backupFiles = files.filter(f => f.startsWith(path.basename(this.yamlFile)))
|
|
158
|
+
.map(f => ({ name: f, path: path.join(this.backupDir, f) }))
|
|
159
|
+
.sort((a, b) => b.name.localeCompare(a.name));
|
|
160
|
+
|
|
161
|
+
if (backupFiles.length > this.maxBackups) {
|
|
162
|
+
const filesToDelete = backupFiles.slice(this.maxBackups);
|
|
163
|
+
for (const file of filesToDelete) {
|
|
164
|
+
await fs.unlink(file.path);
|
|
165
|
+
logger.info('古いバックアップを削除しました', { file: file.name });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} catch (error) {
|
|
169
|
+
logger.error('バックアップのクリーンアップに失敗しました', { error: error.message });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async saveData() {
|
|
174
|
+
try {
|
|
175
|
+
await this.createBackup();
|
|
176
|
+
const yamlData = yaml.dump(this.data, {
|
|
177
|
+
flowLevel: -1, // すべてのレベルでブロックスタイルを使用
|
|
178
|
+
lineWidth: 120,
|
|
179
|
+
noRefs: true,
|
|
180
|
+
quotingType: '"',
|
|
181
|
+
forceQuotes: false
|
|
182
|
+
});
|
|
183
|
+
await fs.writeFile(this.yamlFile, yamlData, 'utf8');
|
|
184
|
+
logger.info('YAMLデータを保存しました', { file: this.yamlFile });
|
|
185
|
+
} catch (error) {
|
|
186
|
+
logger.error('データの保存に失敗しました', { error: error.message });
|
|
187
|
+
throw new DataError(`データの保存に失敗: ${error.message}`, 'SAVE_DATA');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async savePendingChanges() {
|
|
192
|
+
try {
|
|
193
|
+
const jsonData = JSON.stringify(this.pendingChanges, null, 2);
|
|
194
|
+
await fs.writeFile(this.pendingFile, jsonData, 'utf8');
|
|
195
|
+
logger.info('保留変更を保存しました', { count: this.pendingChanges.length });
|
|
196
|
+
} catch (error) {
|
|
197
|
+
logger.error('保留変更の保存に失敗しました', { error: error.message });
|
|
198
|
+
throw new DataError(`保留変更の保存に失敗: ${error.message}`, 'SAVE_PENDING');
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async addPendingChange(change) {
|
|
203
|
+
this.pendingChanges.push({
|
|
204
|
+
...change,
|
|
205
|
+
timestamp: new Date().toISOString(),
|
|
206
|
+
id: Date.now()
|
|
207
|
+
});
|
|
208
|
+
await this.savePendingChanges();
|
|
209
|
+
logger.info('保留変更を追加しました', { action: change.action });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// pending changesからbody情報を取得する新規メソッド
|
|
213
|
+
getPendingBodies() {
|
|
214
|
+
const pending = this.pendingChanges.filter(change =>
|
|
215
|
+
change.action === 'add_body' || change.action === 'update_body'
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
return pending.map(change => change.data.body).filter(Boolean);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async applyChanges() {
|
|
222
|
+
if (this.pendingChanges.length === 0) {
|
|
223
|
+
return '適用する変更がありません';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
logger.info('変更の適用を開始します', { count: this.pendingChanges.length });
|
|
228
|
+
|
|
229
|
+
for (const change of this.pendingChanges) {
|
|
230
|
+
await this.applyChange(change);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
await this.saveData();
|
|
234
|
+
|
|
235
|
+
// 適用後は保留変更をクリア
|
|
236
|
+
this.pendingChanges = [];
|
|
237
|
+
await this.savePendingChanges();
|
|
238
|
+
|
|
239
|
+
logger.info('すべての変更を適用しました');
|
|
240
|
+
return '変更を正常に適用しました';
|
|
241
|
+
|
|
242
|
+
} catch (error) {
|
|
243
|
+
logger.error('変更の適用に失敗しました', { error: error.message });
|
|
244
|
+
throw new DataError(`変更の適用に失敗: ${error.message}`, 'APPLY_CHANGES');
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async applyChange(change) {
|
|
249
|
+
const { action, data } = change;
|
|
250
|
+
|
|
251
|
+
switch (action) {
|
|
252
|
+
case 'add_body':
|
|
253
|
+
if (!this.data.body) this.data.body = [];
|
|
254
|
+
const existingBody = this.data.body.find(b => b.name === data.body.name);
|
|
255
|
+
if (existingBody) {
|
|
256
|
+
Object.assign(existingBody, data.body);
|
|
257
|
+
} else {
|
|
258
|
+
this.data.body.push(data.body);
|
|
259
|
+
}
|
|
260
|
+
break;
|
|
261
|
+
|
|
262
|
+
case 'add_zone':
|
|
263
|
+
if (!this.data.zone) this.data.zone = [];
|
|
264
|
+
const existingZone = this.data.zone.find(z => z.body_name === data.zone.body_name);
|
|
265
|
+
if (existingZone) {
|
|
266
|
+
Object.assign(existingZone, data.zone);
|
|
267
|
+
} else {
|
|
268
|
+
this.data.zone.push(data.zone);
|
|
269
|
+
}
|
|
270
|
+
break;
|
|
271
|
+
|
|
272
|
+
case 'delete_body':
|
|
273
|
+
if (this.data.body) {
|
|
274
|
+
const bodyIndex = this.data.body.findIndex(b => b.name === data.name);
|
|
275
|
+
if (bodyIndex !== -1) {
|
|
276
|
+
this.data.body.splice(bodyIndex, 1);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
break;
|
|
280
|
+
|
|
281
|
+
case 'delete_zone':
|
|
282
|
+
if (data.body_name === 'ATMOSPHERE') break; // ATMOSPHERE削除禁止
|
|
283
|
+
if (this.data.zone) {
|
|
284
|
+
const zoneIndex = this.data.zone.findIndex(z => z.body_name === data.body_name);
|
|
285
|
+
if (zoneIndex !== -1) {
|
|
286
|
+
this.data.zone.splice(zoneIndex, 1);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
|
|
291
|
+
case 'updateBody':
|
|
292
|
+
if (this.data.body) {
|
|
293
|
+
const body = this.data.body.find(b => b.name === data.name);
|
|
294
|
+
if (body) {
|
|
295
|
+
Object.keys(data).forEach(key => {
|
|
296
|
+
if (key !== 'name') {
|
|
297
|
+
if (data[key] === null) {
|
|
298
|
+
delete body[key];
|
|
299
|
+
} else {
|
|
300
|
+
body[key] = data[key];
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
break;
|
|
307
|
+
|
|
308
|
+
case 'updateZone':
|
|
309
|
+
if (this.data.zone) {
|
|
310
|
+
const zone = this.data.zone.find(z => z.body_name === data.body_name);
|
|
311
|
+
if (zone) {
|
|
312
|
+
if (data.new_body_name !== undefined) zone.body_name = data.new_body_name;
|
|
313
|
+
if (data.material !== undefined) zone.material = data.material;
|
|
314
|
+
if (data.density !== undefined) {
|
|
315
|
+
if (data.density === null) {
|
|
316
|
+
delete zone.density;
|
|
317
|
+
} else {
|
|
318
|
+
zone.density = data.density;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
break;
|
|
324
|
+
|
|
325
|
+
case 'proposeTransform':
|
|
326
|
+
if (!this.data.transform) this.data.transform = [];
|
|
327
|
+
const existingTransform = this.data.transform.find(t => t.name === data.name);
|
|
328
|
+
if (existingTransform) {
|
|
329
|
+
existingTransform.operation = data.operations;
|
|
330
|
+
} else {
|
|
331
|
+
this.data.transform.push({
|
|
332
|
+
name: data.name,
|
|
333
|
+
operation: data.operations
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
break;
|
|
337
|
+
|
|
338
|
+
case 'updateTransform':
|
|
339
|
+
if (this.data.transform) {
|
|
340
|
+
const transform = this.data.transform.find(t => t.name === data.name);
|
|
341
|
+
if (transform) {
|
|
342
|
+
Object.keys(data).forEach(key => {
|
|
343
|
+
if (key !== 'name') {
|
|
344
|
+
transform[key] = data[key];
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
break;
|
|
350
|
+
|
|
351
|
+
case 'deleteTransform':
|
|
352
|
+
if (this.data.transform) {
|
|
353
|
+
const transformIndex = this.data.transform.findIndex(t => t.name === data.name);
|
|
354
|
+
if (transformIndex !== -1) {
|
|
355
|
+
this.data.transform.splice(transformIndex, 1);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
break;
|
|
359
|
+
|
|
360
|
+
case 'proposeBuildupFactor':
|
|
361
|
+
if (!this.data.buildup_factor) this.data.buildup_factor = [];
|
|
362
|
+
const existingBuildup = this.data.buildup_factor.find(bf => bf.material === data.material);
|
|
363
|
+
if (existingBuildup) {
|
|
364
|
+
Object.assign(existingBuildup, data);
|
|
365
|
+
} else {
|
|
366
|
+
this.data.buildup_factor.push({
|
|
367
|
+
material: data.material,
|
|
368
|
+
use_slant_correction: data.use_slant_correction,
|
|
369
|
+
use_finite_medium_correction: data.use_finite_medium_correction
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
break;
|
|
373
|
+
|
|
374
|
+
case 'updateBuildupFactor':
|
|
375
|
+
if (this.data.buildup_factor) {
|
|
376
|
+
const buildup = this.data.buildup_factor.find(bf => bf.material === data.material);
|
|
377
|
+
if (buildup) {
|
|
378
|
+
if ('use_slant_correction' in data) {
|
|
379
|
+
buildup.use_slant_correction = data.use_slant_correction;
|
|
380
|
+
}
|
|
381
|
+
if ('use_finite_medium_correction' in data) {
|
|
382
|
+
buildup.use_finite_medium_correction = data.use_finite_medium_correction;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
break;
|
|
387
|
+
|
|
388
|
+
case 'deleteBuildupFactor':
|
|
389
|
+
if (this.data.buildup_factor) {
|
|
390
|
+
const buildupIndex = this.data.buildup_factor.findIndex(bf => bf.material === data.material);
|
|
391
|
+
if (buildupIndex !== -1) {
|
|
392
|
+
this.data.buildup_factor.splice(buildupIndex, 1);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
break;
|
|
396
|
+
|
|
397
|
+
case 'changeOrderBuildupFactor':
|
|
398
|
+
if (this.data.buildup_factor) {
|
|
399
|
+
const buildupIndex = this.data.buildup_factor.findIndex(bf => bf.material === data.material);
|
|
400
|
+
if (buildupIndex !== -1 && typeof data.newIndex === 'number' &&
|
|
401
|
+
data.newIndex >= 0 && data.newIndex < this.data.buildup_factor.length) {
|
|
402
|
+
const [item] = this.data.buildup_factor.splice(buildupIndex, 1);
|
|
403
|
+
this.data.buildup_factor.splice(data.newIndex, 0, item);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
break;
|
|
407
|
+
|
|
408
|
+
case 'proposeSource':
|
|
409
|
+
if (!this.data.source) this.data.source = [];
|
|
410
|
+
const existingSource = this.data.source.find(s => s.name === data.name);
|
|
411
|
+
if (existingSource) {
|
|
412
|
+
Object.assign(existingSource, data);
|
|
413
|
+
} else {
|
|
414
|
+
// 完全なsourceオブジェクトを構築
|
|
415
|
+
const sourceObject = {
|
|
416
|
+
name: data.name,
|
|
417
|
+
type: data.type,
|
|
418
|
+
inventory: data.inventory
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
// cutoff_rateの追加(必須パラメータ)
|
|
422
|
+
if (data.cutoff_rate !== undefined) {
|
|
423
|
+
sourceObject.cutoff_rate = data.cutoff_rate;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// POINT線源の場合はpositionを追加
|
|
427
|
+
if (data.type === 'POINT' && data.position) {
|
|
428
|
+
sourceObject.position = data.position;
|
|
429
|
+
logger.info('POINT線源にpositionを追加', { name: data.name, position: data.position });
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// 体積線源の場合はgeometryとdivisionを追加
|
|
433
|
+
if (data.type !== 'POINT') {
|
|
434
|
+
// geometryの確実な追加
|
|
435
|
+
if (data.geometry) {
|
|
436
|
+
sourceObject.geometry = data.geometry;
|
|
437
|
+
logger.info('体積線源にgeometryを追加', { name: data.name, geometry: data.geometry });
|
|
438
|
+
} else {
|
|
439
|
+
logger.warn('体積線源にgeometryがありません', { name: data.name, type: data.type });
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// divisionの確実な追加
|
|
443
|
+
if (data.division) {
|
|
444
|
+
sourceObject.division = data.division;
|
|
445
|
+
logger.info('体積線源にdivisionを追加', { name: data.name, division: data.division });
|
|
446
|
+
} else {
|
|
447
|
+
logger.warn('体積線源にdivisionがありません', { name: data.name, type: data.type });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
logger.info('最終的なsourceObject', {
|
|
452
|
+
name: sourceObject.name,
|
|
453
|
+
type: sourceObject.type,
|
|
454
|
+
hasGeometry: !!sourceObject.geometry,
|
|
455
|
+
hasDivision: !!sourceObject.division,
|
|
456
|
+
hasPosition: !!sourceObject.position,
|
|
457
|
+
fullObject: sourceObject
|
|
458
|
+
});
|
|
459
|
+
this.data.source.push(sourceObject);
|
|
460
|
+
}
|
|
461
|
+
break;
|
|
462
|
+
|
|
463
|
+
case 'updateSource':
|
|
464
|
+
if (this.data.source) {
|
|
465
|
+
const source = this.data.source.find(s => s.name === data.name);
|
|
466
|
+
if (source) {
|
|
467
|
+
Object.keys(data).forEach(key => {
|
|
468
|
+
if (key !== 'name') {
|
|
469
|
+
if (data[key] === null) {
|
|
470
|
+
delete source[key];
|
|
471
|
+
} else {
|
|
472
|
+
source[key] = data[key];
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
break;
|
|
479
|
+
|
|
480
|
+
case 'deleteSource':
|
|
481
|
+
if (this.data.source) {
|
|
482
|
+
const sourceIndex = this.data.source.findIndex(s => s.name === data.name);
|
|
483
|
+
if (sourceIndex !== -1) {
|
|
484
|
+
this.data.source.splice(sourceIndex, 1);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
break;
|
|
488
|
+
|
|
489
|
+
case 'proposeDetector':
|
|
490
|
+
if (!this.data.detector) this.data.detector = [];
|
|
491
|
+
const existingDetector = this.data.detector.find(d => d.name === data.name);
|
|
492
|
+
if (existingDetector) {
|
|
493
|
+
Object.assign(existingDetector, data);
|
|
494
|
+
} else {
|
|
495
|
+
// 完全なdetectorオブジェクトを構築
|
|
496
|
+
const detectorObject = {
|
|
497
|
+
name: data.name,
|
|
498
|
+
origin: data.origin,
|
|
499
|
+
grid: data.grid || [],
|
|
500
|
+
show_path_trace: data.show_path_trace // 必須パラメータのためデフォルト値適用削除
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
// transformが指定されている場合は追加
|
|
504
|
+
if (data.transform) {
|
|
505
|
+
detectorObject.transform = data.transform;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
this.data.detector.push(detectorObject);
|
|
509
|
+
}
|
|
510
|
+
break;
|
|
511
|
+
|
|
512
|
+
case 'updateDetector':
|
|
513
|
+
if (this.data.detector) {
|
|
514
|
+
const detector = this.data.detector.find(d => d.name === data.name);
|
|
515
|
+
if (detector) {
|
|
516
|
+
Object.keys(data).forEach(key => {
|
|
517
|
+
if (key !== 'name') {
|
|
518
|
+
if (data[key] === null) {
|
|
519
|
+
delete detector[key];
|
|
520
|
+
} else {
|
|
521
|
+
detector[key] = data[key];
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
break;
|
|
528
|
+
|
|
529
|
+
case 'deleteDetector':
|
|
530
|
+
if (this.data.detector) {
|
|
531
|
+
const detectorIndex = this.data.detector.findIndex(d => d.name === data.name);
|
|
532
|
+
if (detectorIndex !== -1) {
|
|
533
|
+
this.data.detector.splice(detectorIndex, 1);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
break;
|
|
537
|
+
|
|
538
|
+
case 'proposeUnit':
|
|
539
|
+
await this.applyProposeUnit(data);
|
|
540
|
+
break;
|
|
541
|
+
|
|
542
|
+
case 'updateUnit':
|
|
543
|
+
await this.applyUpdateUnit(data);
|
|
544
|
+
break;
|
|
545
|
+
|
|
546
|
+
default:
|
|
547
|
+
logger.warn('未知の変更アクション', { action });
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Unit専用処理メソッド
|
|
553
|
+
async applyProposeUnit(data) {
|
|
554
|
+
try {
|
|
555
|
+
// Unit セクション存在チェック
|
|
556
|
+
if (this.data.unit) {
|
|
557
|
+
throw new DataError('unit セクションは既に存在します', 'UNIT_ALREADY_EXISTS');
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// 4つのキー完全性チェック
|
|
561
|
+
this.validateUnitOperation('proposeUnit', data);
|
|
562
|
+
|
|
563
|
+
// Unit セクション作成
|
|
564
|
+
this.data.unit = {
|
|
565
|
+
length: data.length,
|
|
566
|
+
angle: data.angle,
|
|
567
|
+
density: data.density,
|
|
568
|
+
radioactivity: data.radioactivity
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
logger.info('unit セクションを作成しました', this.data.unit);
|
|
572
|
+
|
|
573
|
+
} catch (error) {
|
|
574
|
+
logger.error('proposeUnit適用エラー', { data, error: error.message });
|
|
575
|
+
throw error;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async applyUpdateUnit(data) {
|
|
580
|
+
try {
|
|
581
|
+
// Unit セクション存在チェック
|
|
582
|
+
if (!this.data.unit) {
|
|
583
|
+
throw new DataError('unit セクションが存在しません', 'UNIT_NOT_FOUND');
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// データ完全性チェック
|
|
587
|
+
this.validateUnitOperation('updateUnit', data);
|
|
588
|
+
|
|
589
|
+
// Unit セクション更新(4つのキー全て更新)
|
|
590
|
+
this.data.unit = {
|
|
591
|
+
length: data.length,
|
|
592
|
+
angle: data.angle,
|
|
593
|
+
density: data.density,
|
|
594
|
+
radioactivity: data.radioactivity
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
logger.info('unit セクションを更新しました', this.data.unit);
|
|
598
|
+
|
|
599
|
+
} catch (error) {
|
|
600
|
+
logger.error('updateUnit適用エラー', { data, error: error.message });
|
|
601
|
+
throw error;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
validateUnitOperation(action, data) {
|
|
606
|
+
// 4つのキー完全性チェック
|
|
607
|
+
const requiredKeys = ['length', 'angle', 'density', 'radioactivity'];
|
|
608
|
+
for (const key of requiredKeys) {
|
|
609
|
+
if (!data[key]) {
|
|
610
|
+
throw new DataError(`unit ${action}に必須キー ${key} がありません`, 'UNIT_KEY_MISSING');
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// 単位値妥当性チェック
|
|
615
|
+
const validUnits = {
|
|
616
|
+
length: ['m', 'cm', 'mm'],
|
|
617
|
+
angle: ['radian', 'degree'],
|
|
618
|
+
density: ['g/cm3'],
|
|
619
|
+
radioactivity: ['Bq']
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
for (const [key, value] of Object.entries(data)) {
|
|
623
|
+
if (validUnits[key] && !validUnits[key].includes(value)) {
|
|
624
|
+
throw new DataError(
|
|
625
|
+
`${key} の値 '${value}' は無効です。有効な値: ${validUnits[key].join(', ')}`,
|
|
626
|
+
'UNIT_INVALID_VALUE'
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return true;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Unit自動検証・修復メソッド
|
|
635
|
+
async validateAndRepairUnit() {
|
|
636
|
+
try {
|
|
637
|
+
let repairActions = [];
|
|
638
|
+
|
|
639
|
+
// Unit セクション存在チェック
|
|
640
|
+
if (!this.data.unit) {
|
|
641
|
+
this.data.unit = this.createDefaultUnitSection();
|
|
642
|
+
repairActions.push('未存在unit セクションをデフォルト値で作成');
|
|
643
|
+
logger.warn('unit セクションが存在しないため、デフォルト値で作成しました');
|
|
644
|
+
} else {
|
|
645
|
+
// 不完全unit セクションの修復
|
|
646
|
+
const repairedUnit = this.repairIncompleteUnitSection(this.data.unit);
|
|
647
|
+
if (repairedUnit.repaired) {
|
|
648
|
+
this.data.unit = repairedUnit.data;
|
|
649
|
+
repairActions.push(...repairedUnit.actions);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// 修復アクションのログ
|
|
654
|
+
if (repairActions.length > 0) {
|
|
655
|
+
this.logUnitRepairActions(repairActions);
|
|
656
|
+
// 修復した場合はファイルを保存
|
|
657
|
+
await this.saveData();
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
} catch (error) {
|
|
661
|
+
logger.error('Unit自動修復に失敗しました', { error: error.message });
|
|
662
|
+
// 修復失敗時はデフォルト値で強制作成
|
|
663
|
+
this.data.unit = this.createDefaultUnitSection();
|
|
664
|
+
logger.warn('Unit修復失敗のため、デフォルト値で強制作成しました');
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
createDefaultUnitSection() {
|
|
669
|
+
const defaultUnit = {
|
|
670
|
+
length: 'cm',
|
|
671
|
+
angle: 'radian',
|
|
672
|
+
density: 'g/cm3',
|
|
673
|
+
radioactivity: 'Bq'
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
logger.info('unit デフォルトセクションを作成しました', defaultUnit);
|
|
677
|
+
return defaultUnit;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
repairIncompleteUnitSection(unitData) {
|
|
681
|
+
const defaultValues = {
|
|
682
|
+
length: 'cm',
|
|
683
|
+
angle: 'radian',
|
|
684
|
+
density: 'g/cm3',
|
|
685
|
+
radioactivity: 'Bq'
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
const validUnits = {
|
|
689
|
+
length: ['m', 'cm', 'mm'],
|
|
690
|
+
angle: ['radian', 'degree'],
|
|
691
|
+
density: ['g/cm3'],
|
|
692
|
+
radioactivity: ['Bq']
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
let repaired = false;
|
|
696
|
+
let actions = [];
|
|
697
|
+
const repairedData = { ...unitData };
|
|
698
|
+
|
|
699
|
+
// 不足キーの補完
|
|
700
|
+
for (const [key, defaultValue] of Object.entries(defaultValues)) {
|
|
701
|
+
if (!repairedData[key]) {
|
|
702
|
+
repairedData[key] = defaultValue;
|
|
703
|
+
repaired = true;
|
|
704
|
+
actions.push(`不足キー ${key} をデフォルト値 '${defaultValue}' で補完`);
|
|
705
|
+
logger.warn(`Unitキー ${key} が不足しているためデフォルト値で補完しました`, { key, defaultValue });
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// 無効値の修正
|
|
710
|
+
for (const [key, value] of Object.entries(repairedData)) {
|
|
711
|
+
if (validUnits[key] && !validUnits[key].includes(value)) {
|
|
712
|
+
const defaultValue = defaultValues[key];
|
|
713
|
+
repairedData[key] = defaultValue;
|
|
714
|
+
repaired = true;
|
|
715
|
+
actions.push(`無効値 ${key}:'${value}' をデフォルト値 '${defaultValue}' で置換`);
|
|
716
|
+
logger.warn(`Unitキー ${key} の値 '${value}' が無効のためデフォルト値で置換しました`, { key, oldValue: value, newValue: defaultValue });
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return {
|
|
721
|
+
repaired,
|
|
722
|
+
data: repairedData,
|
|
723
|
+
actions
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
logUnitRepairActions(actions) {
|
|
728
|
+
logger.info('Unitセクションの自動修復を実行しました', {
|
|
729
|
+
repairCount: actions.length,
|
|
730
|
+
repairs: actions
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
// 特別ログ: 重要な自動修復情報
|
|
734
|
+
for (const action of actions) {
|
|
735
|
+
logger.warn('Unit自動修復', { action });
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|