From b8f44b1c44947ed1535deded261296e713e9310d Mon Sep 17 00:00:00 2001 From: sumeyaaaa Date: Sat, 21 Feb 2026 13:57:48 +0300 Subject: [PATCH] chore: commit src/hooks/OrchestrationDataModel.ts --- src/hooks/OrchestrationDataModel.ts | 246 +++++++++++++++++++++++++++- 1 file changed, 241 insertions(+), 5 deletions(-) diff --git a/src/hooks/OrchestrationDataModel.ts b/src/hooks/OrchestrationDataModel.ts index 5b13550914..ed1bc3f506 100644 --- a/src/hooks/OrchestrationDataModel.ts +++ b/src/hooks/OrchestrationDataModel.ts @@ -30,6 +30,20 @@ export interface AgentTraceRange { content_hash: string } +/** + * Mutation classification types (Phase 3 requirement) + */ +export type MutationClass = "AST_REFACTOR" | "INTENT_EVOLUTION" + +/** + * Mutation classification result + */ +export interface MutationClassification { + mutation_class: MutationClass + confidence: "high" | "medium" | "low" + reason?: string +} + export interface AgentTraceConversation { url: string contributor: { @@ -51,6 +65,8 @@ export interface AgentTraceFile { export interface AgentTraceEntry { id: string timestamp: string + tool_name?: string // Phase 3: Track which tool made the change + mutation_class?: MutationClass // Phase 3: AST_REFACTOR or INTENT_EVOLUTION vcs: { revision_id: string } @@ -125,6 +141,25 @@ This file contains persistent knowledge shared across parallel sessions (Archite ` await fs.writeFile(agentPath, header, "utf-8") } + + // Initialize .intentignore if it doesn't exist (Phase 2 requirement) + const intentIgnorePath = path.join(this.orchestrationDir, ".intentignore") + try { + await fs.access(intentIgnorePath) + } catch { + // File doesn't exist, create it with header + const header = `# Intent Ignore File +# List intent IDs that should be protected from modifications +# One intent ID per line +# Lines starting with # are comments +# +# Example: +# INT-005 # Legacy system - deprecated +# INT-010 # Production critical - manual changes only + +` + await fs.writeFile(intentIgnorePath, header, "utf-8") + } } catch (error) { console.error("Failed to initialize orchestration directory:", error) throw error @@ -201,7 +236,10 @@ This file contains persistent knowledge shared across parallel sessions (Archite try { const content = await fs.readFile(tracePath, "utf-8") - const lines = content.trim().split("\n").filter((line) => line.trim() && !line.startsWith("#")) + const lines = content + .trim() + .split("\n") + .filter((line) => line.trim() && !line.startsWith("#")) const entries: AgentTraceEntry[] = [] @@ -213,9 +251,7 @@ This file contains persistent knowledge shared across parallel sessions (Archite // Check if any file's conversation references this intent const referencesIntent = entry.files.some((file) => file.conversations.some((conv) => - conv.related.some( - (rel) => rel.type === "intent" && rel.value === intentId, - ), + conv.related.some((rel) => rel.type === "intent" && rel.value === intentId), ), ) @@ -250,11 +286,211 @@ This file contains persistent knowledge shared across parallel sessions (Archite return crypto.createHash("sha256").update(content).digest("hex") } + /** + * Classify mutation type: AST_REFACTOR vs INTENT_EVOLUTION (Phase 3 requirement) + * + * Heuristics: + * - AST_REFACTOR: Same semantic meaning, structural changes (renames, formatting, reorganization) + * - INTENT_EVOLUTION: New functionality, feature additions, behavior changes + * + * This is a simplified heuristic - in production, this could use AST diffing or ML models + */ + classifyMutation(oldContent: string | null, newContent: string, filePath: string): MutationClassification { + // If file didn't exist before, it's always INTENT_EVOLUTION + if (!oldContent || oldContent === "") { + return { + mutation_class: "INTENT_EVOLUTION", + confidence: "high", + reason: "New file creation", + } + } + + // Normalize whitespace for comparison + const oldNormalized = oldContent.replace(/\s+/g, " ").trim() + const newNormalized = newContent.replace(/\s+/g, " ").trim() + + // If content is identical (after normalization), it's not a mutation + if (oldNormalized === newNormalized) { + return { + mutation_class: "AST_REFACTOR", + confidence: "high", + reason: "No semantic changes detected", + } + } + + // Calculate similarity ratio + const similarity = this.calculateSimilarity(oldNormalized, newNormalized) + + // Heuristic: If >80% similar, likely a refactor + if (similarity > 0.8) { + // Check if it's mostly structural changes + const oldLines = oldContent.split("\n") + const newLines = newContent.split("\n") + const lineCountDiff = Math.abs(oldLines.length - newLines.length) / Math.max(oldLines.length, 1) + + // If line count changed significantly, might be evolution + if (lineCountDiff > 0.3) { + return { + mutation_class: "INTENT_EVOLUTION", + confidence: "medium", + reason: `Significant line count change (${Math.round(lineCountDiff * 100)}%) despite high similarity`, + } + } + + return { + mutation_class: "AST_REFACTOR", + confidence: similarity > 0.9 ? "high" : "medium", + reason: `High similarity (${Math.round(similarity * 100)}%) suggests refactoring`, + } + } + + // Check for new function/class definitions (strong indicator of evolution) + const newFunctionPattern = /(?:function|class|const|let|var)\s+\w+\s*[=:\(]/g + const oldMatches = (oldContent.match(newFunctionPattern) || []).length + const newMatches = (newContent.match(newFunctionPattern) || []).length + + if (newMatches > oldMatches) { + return { + mutation_class: "INTENT_EVOLUTION", + confidence: "high", + reason: `New code definitions detected (${newMatches - oldMatches} new)`, + } + } + + // Check for significant content addition + const contentGrowth = (newContent.length - oldContent.length) / Math.max(oldContent.length, 1) + if (contentGrowth > 0.5) { + return { + mutation_class: "INTENT_EVOLUTION", + confidence: "medium", + reason: `Significant content growth (${Math.round(contentGrowth * 100)}%)`, + } + } + + // Default: if similarity is low, likely evolution + return { + mutation_class: similarity < 0.5 ? "INTENT_EVOLUTION" : "AST_REFACTOR", + confidence: "low", + reason: `Similarity: ${Math.round(similarity * 100)}%`, + } + } + + /** + * Calculate similarity ratio between two strings using Levenshtein distance + */ + private calculateSimilarity(str1: string, str2: string): number { + const maxLen = Math.max(str1.length, str2.length) + if (maxLen === 0) return 1.0 + + const distance = this.levenshteinDistance(str1, str2) + return 1 - distance / maxLen + } + + /** + * Calculate Levenshtein distance between two strings + */ + private levenshteinDistance(str1: string, str2: string): number { + const m = str1.length + const n = str2.length + const dp: number[][] = [] + + for (let i = 0; i <= m; i++) { + dp[i] = [i] + } + for (let j = 0; j <= n; j++) { + dp[0][j] = j + } + + for (let i = 1; i <= m; i++) { + for (let j = 1; j <= n; j++) { + if (str1[i - 1] === str2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + } else { + dp[i][j] = Math.min( + dp[i - 1][j] + 1, // deletion + dp[i][j - 1] + 1, // insertion + dp[i - 1][j - 1] + 1, // substitution + ) + } + } + } + + return dp[m][n] + } + + /** + * Append lesson learned to AGENT.md (Phase 4 requirement) + */ + async appendLesson(lesson: string, context?: { tool?: string; error?: string; file?: string }): Promise { + const agentPath = path.join(this.orchestrationDir, "AGENT.md") + try { + let content = await fs.readFile(agentPath, "utf-8") + + // Ensure Lessons Learned section exists + if (!content.includes("## Lessons Learned")) { + content += "\n\n## Lessons Learned\n\n" + } + + // Append new lesson with timestamp + const timestamp = new Date().toISOString() + const contextInfo = context + ? `\n**Context:** ${context.tool ? `Tool: ${context.tool}` : ""}${context.file ? ` | File: ${context.file}` : ""}${context.error ? ` | Error: ${context.error}` : ""}\n` + : "" + const lessonEntry = `### ${timestamp}\n${contextInfo}${lesson}\n\n---\n\n` + + // Find the Lessons Learned section and append + const lessonsIndex = content.indexOf("## Lessons Learned") + if (lessonsIndex !== -1) { + const afterHeader = content.indexOf("\n", lessonsIndex) + 1 + content = content.slice(0, afterHeader) + lessonEntry + content.slice(afterHeader) + } else { + content += `\n## Lessons Learned\n\n${lessonEntry}` + } + + await fs.writeFile(agentPath, content, "utf-8") + } catch (error) { + console.error("Failed to append lesson to AGENT.md:", error) + // Don't throw - lesson recording shouldn't break execution + } + } + /** * Get orchestration directory path */ getOrchestrationDir(): string { return this.orchestrationDir } -} + /** + * Read .intentignore file and return list of ignored intent IDs (Phase 2 requirement) + */ + async readIntentIgnore(): Promise { + const intentIgnorePath = path.join(this.orchestrationDir, ".intentignore") + try { + const content = await fs.readFile(intentIgnorePath, "utf-8") + const lines = content.split("\n") + const ignoredIntents: string[] = [] + + for (const line of lines) { + // Remove comments and trim + const cleanLine = line.split("#")[0].trim() + if (cleanLine && !cleanLine.startsWith("#")) { + ignoredIntents.push(cleanLine) + } + } + + return ignoredIntents + } catch (error) { + // File doesn't exist or can't be read - return empty array + return [] + } + } + + /** + * Check if an intent ID is in the ignore list + */ + async isIntentIgnored(intentId: string): Promise { + const ignoredIntents = await this.readIntentIgnore() + return ignoredIntents.includes(intentId) + } +}