chore: commit src/hooks/OrchestrationDataModel.ts

This commit is contained in:
sumeyaaaa 2026-02-21 13:57:48 +03:00
parent 5ce9e584ae
commit b8f44b1c44

View file

@ -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<void> {
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<string[]> {
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<boolean> {
const ignoredIntents = await this.readIntentIgnore()
return ignoredIntents.includes(intentId)
}
}