|
@@ -1,530 +0,0 @@
|
|
|
-# 技術実装詳細
|
|
|
|
|
-
|
|
|
|
|
-## 🏗️ アーキテクチャ変更
|
|
|
|
|
-
|
|
|
|
|
-### 現在のフロー
|
|
|
|
|
-```mermaid
|
|
|
|
|
-graph TD
|
|
|
|
|
- A[User Input] --> B[useEditorAssistant.postMessage]
|
|
|
|
|
- B --> C[Server: edit/index.ts]
|
|
|
|
|
- C --> D[OpenAI Stream]
|
|
|
|
|
- D --> E[LlmResponseStreamProcessor]
|
|
|
|
|
- E --> F[jsonrepair + parse]
|
|
|
|
|
- F --> G[SseDetectedDiff]
|
|
|
|
|
- G --> H[useEditorAssistant.processMessage]
|
|
|
|
|
- H --> I[setDetectedDiff]
|
|
|
|
|
- I --> J[yText更新]
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### 改修後のフロー
|
|
|
|
|
-```mermaid
|
|
|
|
|
-graph TD
|
|
|
|
|
- A[User Input] --> B[useEditorAssistant.postMessage]
|
|
|
|
|
- B --> C[Server: edit/index.ts]
|
|
|
|
|
- C --> D[OpenAI Stream with Search/Replace]
|
|
|
|
|
- D --> E[LlmResponseStreamProcessor]
|
|
|
|
|
- E --> F[jsonrepair + parse Search/Replace blocks]
|
|
|
|
|
- F --> G[MultiSearchReplaceProcessor]
|
|
|
|
|
- G --> H[Fuzzy Matching + Apply Diffs]
|
|
|
|
|
- H --> I[DiffApplicationResult]
|
|
|
|
|
- I --> J[SseDetectedDiff with Results]
|
|
|
|
|
- J --> K[useEditorAssistant.processMessage]
|
|
|
|
|
- K --> L[Enhanced Error Handling]
|
|
|
|
|
- L --> M[yText更新 with Validation]
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-## 📦 ファイル構成
|
|
|
|
|
-
|
|
|
|
|
-### 新規作成ファイル
|
|
|
|
|
-```
|
|
|
|
|
-apps/app/src/features/openai/server/services/editor-assistant/
|
|
|
|
|
-├── multi-search-replace-processor.ts # メイン処理エンジン
|
|
|
|
|
-├── fuzzy-matching.ts # 類似度計算ユーティリティ
|
|
|
|
|
-├── diff-application-engine.ts # 差分適用ロジック
|
|
|
|
|
-└── error-handlers.ts # エラーハンドリング
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### 更新対象ファイル
|
|
|
|
|
-```
|
|
|
|
|
-apps/app/src/features/openai/
|
|
|
|
|
-├── interfaces/editor-assistant/
|
|
|
|
|
-│ ├── llm-response-schemas.ts # Diffスキーマ更新
|
|
|
|
|
-│ └── sse-schemas.ts # SSEスキーマ更新
|
|
|
|
|
-├── server/
|
|
|
|
|
-│ ├── routes/edit/index.ts # プロンプト・処理統合
|
|
|
|
|
-│ └── services/editor-assistant/
|
|
|
|
|
-│ └── llm-response-stream-processor.ts # Search/Replace対応
|
|
|
|
|
-└── client/services/
|
|
|
|
|
- └── editor-assistant.tsx # クライアント対応
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-## 🔍 核心技術実装
|
|
|
|
|
-
|
|
|
|
|
-### 1. MultiSearchReplaceProcessor
|
|
|
|
|
-
|
|
|
|
|
-```typescript
|
|
|
|
|
-export class MultiSearchReplaceProcessor {
|
|
|
|
|
- private fuzzyThreshold: number = 0.8;
|
|
|
|
|
- private bufferLines: number = 40;
|
|
|
|
|
-
|
|
|
|
|
- constructor(config?: ProcessorConfig) {
|
|
|
|
|
- this.fuzzyThreshold = config?.fuzzyThreshold ?? 0.8;
|
|
|
|
|
- this.bufferLines = config?.bufferLines ?? 40;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- async applyDiffs(
|
|
|
|
|
- originalContent: string,
|
|
|
|
|
- diffs: LlmEditorAssistantDiff[]
|
|
|
|
|
- ): Promise<DiffApplicationResult> {
|
|
|
|
|
- // 行終端の検出
|
|
|
|
|
- const lineEnding = originalContent.includes('\r\n') ? '\r\n' : '\n';
|
|
|
|
|
- let resultLines = originalContent.split(/\r?\n/);
|
|
|
|
|
- let delta = 0;
|
|
|
|
|
- let appliedCount = 0;
|
|
|
|
|
- const failedParts: DiffError[] = [];
|
|
|
|
|
-
|
|
|
|
|
- // startLineでソート
|
|
|
|
|
- const sortedDiffs = diffs
|
|
|
|
|
- .map((diff, index) => ({ ...diff, originalIndex: index }))
|
|
|
|
|
- .sort((a, b) => (a.startLine || 0) - (b.startLine || 0));
|
|
|
|
|
-
|
|
|
|
|
- for (const diff of sortedDiffs) {
|
|
|
|
|
- const result = await this.applySingleDiff(
|
|
|
|
|
- resultLines,
|
|
|
|
|
- diff,
|
|
|
|
|
- delta
|
|
|
|
|
- );
|
|
|
|
|
-
|
|
|
|
|
- if (result.success) {
|
|
|
|
|
- resultLines = result.updatedLines;
|
|
|
|
|
- delta += result.lineDelta;
|
|
|
|
|
- appliedCount++;
|
|
|
|
|
- } else {
|
|
|
|
|
- failedParts.push(result.error);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return {
|
|
|
|
|
- success: appliedCount > 0,
|
|
|
|
|
- appliedCount,
|
|
|
|
|
- failedParts: failedParts.length > 0 ? failedParts : undefined,
|
|
|
|
|
- content: appliedCount > 0 ? resultLines.join(lineEnding) : undefined,
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private async applySingleDiff(
|
|
|
|
|
- lines: string[],
|
|
|
|
|
- diff: LlmEditorAssistantDiff,
|
|
|
|
|
- delta: number
|
|
|
|
|
- ): Promise<SingleDiffResult> {
|
|
|
|
|
- // バリデーション
|
|
|
|
|
- if (!diff.search.trim()) {
|
|
|
|
|
- return {
|
|
|
|
|
- success: false,
|
|
|
|
|
- error: {
|
|
|
|
|
- type: 'EMPTY_SEARCH',
|
|
|
|
|
- message: '検索内容が空です',
|
|
|
|
|
- details: { searchContent: diff.search, suggestions: [] }
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 検索実行
|
|
|
|
|
- const searchResult = this.findBestMatch(lines, diff.search, diff.startLine, delta);
|
|
|
|
|
-
|
|
|
|
|
- if (!searchResult.found) {
|
|
|
|
|
- return {
|
|
|
|
|
- success: false,
|
|
|
|
|
- error: this.createSearchError(diff.search, searchResult)
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 置換実行
|
|
|
|
|
- return this.applyReplacement(lines, diff, searchResult);
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### 2. Fuzzy Matching実装
|
|
|
|
|
-
|
|
|
|
|
-```typescript
|
|
|
|
|
-import { distance } from 'fastest-levenshtein';
|
|
|
|
|
-
|
|
|
|
|
-export class FuzzyMatcher {
|
|
|
|
|
- private threshold: number;
|
|
|
|
|
-
|
|
|
|
|
- constructor(threshold: number = 0.8) {
|
|
|
|
|
- this.threshold = threshold;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- calculateSimilarity(original: string, search: string): number {
|
|
|
|
|
- if (search === '') return 0;
|
|
|
|
|
-
|
|
|
|
|
- // 正規化(スマートクォート等の処理)
|
|
|
|
|
- const normalizedOriginal = this.normalizeString(original);
|
|
|
|
|
- const normalizedSearch = this.normalizeString(search);
|
|
|
|
|
-
|
|
|
|
|
- if (normalizedOriginal === normalizedSearch) return 1;
|
|
|
|
|
-
|
|
|
|
|
- // Levenshtein距離による類似度計算
|
|
|
|
|
- const dist = distance(normalizedOriginal, normalizedSearch);
|
|
|
|
|
- const maxLength = Math.max(normalizedOriginal.length, normalizedSearch.length);
|
|
|
|
|
-
|
|
|
|
|
- return 1 - (dist / maxLength);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- findBestMatch(
|
|
|
|
|
- lines: string[],
|
|
|
|
|
- searchChunk: string,
|
|
|
|
|
- startIndex: number = 0,
|
|
|
|
|
- endIndex?: number
|
|
|
|
|
- ): MatchResult {
|
|
|
|
|
- const searchLines = searchChunk.split(/\r?\n/);
|
|
|
|
|
- const searchLength = searchLines.length;
|
|
|
|
|
- const actualEndIndex = endIndex ?? lines.length;
|
|
|
|
|
-
|
|
|
|
|
- let bestScore = 0;
|
|
|
|
|
- let bestMatchIndex = -1;
|
|
|
|
|
- let bestMatchContent = '';
|
|
|
|
|
-
|
|
|
|
|
- // Middle-out検索
|
|
|
|
|
- const midPoint = Math.floor((startIndex + actualEndIndex) / 2);
|
|
|
|
|
- let leftIndex = midPoint;
|
|
|
|
|
- let rightIndex = midPoint + 1;
|
|
|
|
|
-
|
|
|
|
|
- while (leftIndex >= startIndex || rightIndex <= actualEndIndex - searchLength) {
|
|
|
|
|
- // 左側検索
|
|
|
|
|
- if (leftIndex >= startIndex) {
|
|
|
|
|
- const chunk = lines.slice(leftIndex, leftIndex + searchLength).join('\n');
|
|
|
|
|
- const similarity = this.calculateSimilarity(chunk, searchChunk);
|
|
|
|
|
-
|
|
|
|
|
- if (similarity > bestScore) {
|
|
|
|
|
- bestScore = similarity;
|
|
|
|
|
- bestMatchIndex = leftIndex;
|
|
|
|
|
- bestMatchContent = chunk;
|
|
|
|
|
- }
|
|
|
|
|
- leftIndex--;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 右側検索
|
|
|
|
|
- if (rightIndex <= actualEndIndex - searchLength) {
|
|
|
|
|
- const chunk = lines.slice(rightIndex, rightIndex + searchLength).join('\n');
|
|
|
|
|
- const similarity = this.calculateSimilarity(chunk, searchChunk);
|
|
|
|
|
-
|
|
|
|
|
- if (similarity > bestScore) {
|
|
|
|
|
- bestScore = similarity;
|
|
|
|
|
- bestMatchIndex = rightIndex;
|
|
|
|
|
- bestMatchContent = chunk;
|
|
|
|
|
- }
|
|
|
|
|
- rightIndex++;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return {
|
|
|
|
|
- found: bestScore >= this.threshold,
|
|
|
|
|
- score: bestScore,
|
|
|
|
|
- index: bestMatchIndex,
|
|
|
|
|
- content: bestMatchContent,
|
|
|
|
|
- threshold: this.threshold
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private normalizeString(str: string): string {
|
|
|
|
|
- return str
|
|
|
|
|
- .replace(/[\u2018\u2019]/g, "'") // スマートクォート
|
|
|
|
|
- .replace(/[\u201C\u201D]/g, '"') // スマートダブルクォート
|
|
|
|
|
- .replace(/\u2013/g, '-') // en dash
|
|
|
|
|
- .replace(/\u2014/g, '--') // em dash
|
|
|
|
|
- .normalize('NFC');
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### 3. エラーハンドリング強化
|
|
|
|
|
-
|
|
|
|
|
-```typescript
|
|
|
|
|
-export interface DiffError {
|
|
|
|
|
- type: 'SEARCH_NOT_FOUND' | 'SIMILARITY_TOO_LOW' | 'MULTIPLE_MATCHES' | 'EMPTY_SEARCH';
|
|
|
|
|
- message: string;
|
|
|
|
|
- details: {
|
|
|
|
|
- searchContent: string;
|
|
|
|
|
- bestMatch?: string;
|
|
|
|
|
- similarity?: number;
|
|
|
|
|
- suggestions: string[];
|
|
|
|
|
- lineRange?: string;
|
|
|
|
|
- };
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-export class ErrorHandler {
|
|
|
|
|
- static createSearchError(
|
|
|
|
|
- searchContent: string,
|
|
|
|
|
- matchResult: MatchResult,
|
|
|
|
|
- startLine?: number
|
|
|
|
|
- ): DiffError {
|
|
|
|
|
- const lineRange = startLine ? ` at line: ${startLine}` : '';
|
|
|
|
|
- const similarityPercent = Math.floor((matchResult.score || 0) * 100);
|
|
|
|
|
- const thresholdPercent = Math.floor(matchResult.threshold * 100);
|
|
|
|
|
-
|
|
|
|
|
- return {
|
|
|
|
|
- type: 'SIMILARITY_TOO_LOW',
|
|
|
|
|
- message: `No sufficiently similar match found${lineRange} (${similarityPercent}% similar, needs ${thresholdPercent}%)`,
|
|
|
|
|
- details: {
|
|
|
|
|
- searchContent,
|
|
|
|
|
- bestMatch: matchResult.content || '(no match)',
|
|
|
|
|
- similarity: matchResult.score,
|
|
|
|
|
- suggestions: [
|
|
|
|
|
- 'Use the read_file tool to get the latest content',
|
|
|
|
|
- 'Check for whitespace and indentation differences',
|
|
|
|
|
- 'Verify the search content matches exactly',
|
|
|
|
|
- `Consider lowering similarity threshold (currently ${thresholdPercent}%)`
|
|
|
|
|
- ],
|
|
|
|
|
- lineRange: startLine ? `starting at line ${startLine}` : 'start to end'
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### 4. 文字正規化システム
|
|
|
|
|
-
|
|
|
|
|
-roo-codeと同レベルの文字正規化機能を実装:
|
|
|
|
|
-
|
|
|
|
|
-```typescript
|
|
|
|
|
-// apps/app/src/features/openai/server/services/editor-assistant/text-normalization.ts
|
|
|
|
|
-export const NORMALIZATION_MAPS = {
|
|
|
|
|
- // スマートクォートの正規化
|
|
|
|
|
- SMART_QUOTES: {
|
|
|
|
|
- '\u201C': '"', // 左ダブルクォート
|
|
|
|
|
- '\u201D': '"', // 右ダブルクォート
|
|
|
|
|
- '\u2018': "'", // 左シングルクォート
|
|
|
|
|
- '\u2019': "'", // 右シングルクォート
|
|
|
|
|
- },
|
|
|
|
|
- // タイポグラフィ文字の正規化
|
|
|
|
|
- TYPOGRAPHIC: {
|
|
|
|
|
- '\u2026': '...', // 省略記号
|
|
|
|
|
- '\u2014': '-', // emダッシュ
|
|
|
|
|
- '\u2013': '-', // enダッシュ
|
|
|
|
|
- '\u00A0': ' ', // ノンブレーキングスペース
|
|
|
|
|
- },
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-export function normalizeForFuzzyMatch(text: string): string {
|
|
|
|
|
- return text
|
|
|
|
|
- .replace(/[\u201C\u201D]/g, '"')
|
|
|
|
|
- .replace(/[\u2018\u2019]/g, "'")
|
|
|
|
|
- .replace(/\u2026/g, '...')
|
|
|
|
|
- .replace(/\u2014/g, '-')
|
|
|
|
|
- .replace(/\u2013/g, '-')
|
|
|
|
|
- .replace(/\u00A0/g, ' ')
|
|
|
|
|
- .normalize('NFC'); // Unicode正規化
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### 5. 段階的バリデーションシステム
|
|
|
|
|
-
|
|
|
|
|
-roo-codeのバリデーション戦略を採用:
|
|
|
|
|
-
|
|
|
|
|
-```typescript
|
|
|
|
|
-// マーカーシーケンス検証 → 内容検証 → 適用処理
|
|
|
|
|
-export class ValidationPipeline {
|
|
|
|
|
- static validateDiffContent(diffContent: string): ValidationResult {
|
|
|
|
|
- // 1. マーカーシーケンス検証
|
|
|
|
|
- const markerResult = this.validateMarkerSequencing(diffContent);
|
|
|
|
|
- if (!markerResult.success) return markerResult;
|
|
|
|
|
-
|
|
|
|
|
- // 2. 内容検証
|
|
|
|
|
- const contentResult = this.validateContent(diffContent);
|
|
|
|
|
- if (!contentResult.success) return contentResult;
|
|
|
|
|
-
|
|
|
|
|
- // 3. 構文検証
|
|
|
|
|
- const syntaxResult = this.validateSyntax(diffContent);
|
|
|
|
|
- return syntaxResult;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private static validateMarkerSequencing(content: string): ValidationResult {
|
|
|
|
|
- // roo-codeと同じマーカー検証ロジック
|
|
|
|
|
- // <<<<<<< SEARCH → ======= → >>>>>>> REPLACE の順序チェック
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### 6. 高度なエラーハンドリング
|
|
|
|
|
-
|
|
|
|
|
-roo-codeレベルの詳細なエラーハンドリング実装:
|
|
|
|
|
-
|
|
|
|
|
-```typescript
|
|
|
|
|
-// apps/app/src/features/openai/server/services/editor-assistant/enhanced-error-handler.ts
|
|
|
|
|
-export interface DetailedDiffError {
|
|
|
|
|
- type: 'MARKER_SEQUENCE_ERROR' | 'SIMILARITY_TOO_LOW' | 'MULTIPLE_MATCHES' | 'CONTENT_ERROR';
|
|
|
|
|
- message: string;
|
|
|
|
|
- line?: number;
|
|
|
|
|
- details: {
|
|
|
|
|
- searchContent: string;
|
|
|
|
|
- bestMatch?: string;
|
|
|
|
|
- similarity?: number;
|
|
|
|
|
- suggestions: string[];
|
|
|
|
|
- correctFormat?: string;
|
|
|
|
|
- lineRange?: string;
|
|
|
|
|
- };
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-export class EnhancedErrorHandler {
|
|
|
|
|
- static createMarkerSequenceError(found: string, expected: string, line: number): DetailedDiffError {
|
|
|
|
|
- return {
|
|
|
|
|
- type: 'MARKER_SEQUENCE_ERROR',
|
|
|
|
|
- message: `マーカーシーケンスエラー: 行${line}で '${found}' が見つかりました。期待値: ${expected}`,
|
|
|
|
|
- line,
|
|
|
|
|
- details: {
|
|
|
|
|
- searchContent: found,
|
|
|
|
|
- suggestions: [
|
|
|
|
|
- 'マーカーの順序を確認: <<<<<<< SEARCH → ======= → >>>>>>> REPLACE',
|
|
|
|
|
- 'コンテンツ内の特殊マーカーをバックスラッシュ(\\)でエスケープ',
|
|
|
|
|
- '余分なセパレータや不足しているセパレータがないか確認'
|
|
|
|
|
- ],
|
|
|
|
|
- correctFormat: `<<<<<<< SEARCH\n:start_line: X\n-------\n[検索内容]\n=======\n[置換内容]\n>>>>>>> REPLACE`
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- static createSimilarityError(
|
|
|
|
|
- searchContent: string,
|
|
|
|
|
- bestMatch: string,
|
|
|
|
|
- similarity: number,
|
|
|
|
|
- threshold: number,
|
|
|
|
|
- startLine?: number
|
|
|
|
|
- ): DetailedDiffError {
|
|
|
|
|
- const lineRange = startLine ? ` (開始行: ${startLine})` : '';
|
|
|
|
|
- const similarityPercent = Math.floor(similarity * 100);
|
|
|
|
|
- const thresholdPercent = Math.floor(threshold * 100);
|
|
|
|
|
-
|
|
|
|
|
- return {
|
|
|
|
|
- type: 'SIMILARITY_TOO_LOW',
|
|
|
|
|
- message: `類似度が不十分${lineRange}: ${similarityPercent}% (必要: ${thresholdPercent}%)`,
|
|
|
|
|
- details: {
|
|
|
|
|
- searchContent,
|
|
|
|
|
- bestMatch,
|
|
|
|
|
- similarity,
|
|
|
|
|
- suggestions: [
|
|
|
|
|
- 'read_fileツールで最新のファイル内容を確認',
|
|
|
|
|
- '空白やインデントの違いを確認',
|
|
|
|
|
- '検索内容が正確に一致しているか検証',
|
|
|
|
|
- `類似度の閾値を下げることを検討 (現在: ${thresholdPercent}%)`
|
|
|
|
|
- ],
|
|
|
|
|
- lineRange: startLine ? `行${startLine}から開始` : '全体を対象'
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### 7. 設定管理とカスタマイズ
|
|
|
|
|
-
|
|
|
|
|
-```typescript
|
|
|
|
|
-// apps/app/src/features/openai/server/services/editor-assistant/config.ts
|
|
|
|
|
-export interface EditorAssistantConfig {
|
|
|
|
|
- fuzzyThreshold: number; // デフォルト: 0.8 (80%)
|
|
|
|
|
- bufferLines: number; // デフォルト: 40
|
|
|
|
|
- preserveIndentation: boolean; // デフォルト: true
|
|
|
|
|
- enableMiddleOutSearch: boolean; // デフォルト: true
|
|
|
|
|
- maxDiffBlocks: number; // デフォルト: 10
|
|
|
|
|
- stripLineNumbers: boolean; // デフォルト: true
|
|
|
|
|
- enableAggressiveMatching: boolean; // デフォルト: false
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-export const DEFAULT_CONFIG: EditorAssistantConfig = {
|
|
|
|
|
- fuzzyThreshold: 0.8, // roo-codeより緩い設定 (1.0 → 0.8)
|
|
|
|
|
- bufferLines: 40,
|
|
|
|
|
- preserveIndentation: true,
|
|
|
|
|
- enableMiddleOutSearch: true,
|
|
|
|
|
- maxDiffBlocks: 10,
|
|
|
|
|
- stripLineNumbers: true,
|
|
|
|
|
- enableAggressiveMatching: false,
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-// 環境変数による設定のオーバーライド
|
|
|
|
|
-export function loadConfig(): EditorAssistantConfig {
|
|
|
|
|
- const envConfig: Partial<EditorAssistantConfig> = {
|
|
|
|
|
- fuzzyThreshold: parseFloat(process.env.GROWI_EDITOR_ASSISTANT_FUZZY_THRESHOLD || '0.8'),
|
|
|
|
|
- bufferLines: parseInt(process.env.GROWI_EDITOR_ASSISTANT_BUFFER_LINES || '40'),
|
|
|
|
|
- maxDiffBlocks: parseInt(process.env.GROWI_EDITOR_ASSISTANT_MAX_DIFF_BLOCKS || '10'),
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- return { ...DEFAULT_CONFIG, ...envConfig };
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-## 🎛️ 設定とカスタマイズ
|
|
|
|
|
-
|
|
|
|
|
-### ProcessorConfig
|
|
|
|
|
-```typescript
|
|
|
|
|
-interface ProcessorConfig {
|
|
|
|
|
- fuzzyThreshold?: number; // デフォルト: 0.8 (80%)
|
|
|
|
|
- bufferLines?: number; // デフォルト: 40
|
|
|
|
|
- preserveIndentation?: boolean; // デフォルト: true
|
|
|
|
|
- stripLineNumbers?: boolean; // デフォルト: true
|
|
|
|
|
- enableAggressiveMatching?: boolean; // デフォルト: false
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### 環境変数での調整
|
|
|
|
|
-```typescript
|
|
|
|
|
-const config: ProcessorConfig = {
|
|
|
|
|
- fuzzyThreshold: parseFloat(process.env.EDITOR_ASSISTANT_FUZZY_THRESHOLD || '0.8'),
|
|
|
|
|
- bufferLines: parseInt(process.env.EDITOR_ASSISTANT_BUFFER_LINES || '40'),
|
|
|
|
|
- preserveIndentation: process.env.EDITOR_ASSISTANT_PRESERVE_INDENT !== 'false',
|
|
|
|
|
-};
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-## 🧪 テスト戦略
|
|
|
|
|
-
|
|
|
|
|
-### 単体テスト
|
|
|
|
|
-```typescript
|
|
|
|
|
-describe('MultiSearchReplaceProcessor', () => {
|
|
|
|
|
- it('should handle exact matches', async () => {
|
|
|
|
|
- const processor = new MultiSearchReplaceProcessor();
|
|
|
|
|
- const result = await processor.applyDiffs(originalContent, [
|
|
|
|
|
- { search: 'function test() {', replace: 'function newTest() {' }
|
|
|
|
|
- ]);
|
|
|
|
|
- expect(result.success).toBe(true);
|
|
|
|
|
- expect(result.appliedCount).toBe(1);
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- it('should handle fuzzy matches within threshold', async () => {
|
|
|
|
|
- // スペースやインデントが微妙に違う場合のテスト
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- it('should reject matches below threshold', async () => {
|
|
|
|
|
- // 類似度が低すぎる場合のエラーハンドリングテスト
|
|
|
|
|
- });
|
|
|
|
|
-});
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### 統合テスト
|
|
|
|
|
-```typescript
|
|
|
|
|
-describe('Editor Assistant Integration', () => {
|
|
|
|
|
- it('should process multiple diffs in correct order', async () => {
|
|
|
|
|
- // 複数の変更を正しい順序で処理することを確認
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- it('should handle partial failures gracefully', async () => {
|
|
|
|
|
- // 一部の変更が失敗した場合の処理を確認
|
|
|
|
|
- });
|
|
|
|
|
-});
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-## 📈 パフォーマンス考慮
|
|
|
|
|
-
|
|
|
|
|
-### メモリ最適化
|
|
|
|
|
-- 大きなファイルでの文字列処理の最適化
|
|
|
|
|
-- 不要なデータの早期解放
|
|
|
|
|
-- ストリーミング処理の継続
|
|
|
|
|
-
|
|
|
|
|
-### CPU最適化
|
|
|
|
|
-- Middle-out検索による効率化
|
|
|
|
|
-- 類似度計算の最適化
|
|
|
|
|
-- 早期終了条件の設定
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-**ファイル**: `technical-implementation-details.md`
|
|
|
|
|
-**作成日**: 2025-06-17
|
|
|
|
|
-**関連**: `editor-assistant-refactoring-plan.md`
|
|
|