devcontext
Version:
DevContext is a cutting-edge Model Context Protocol (MCP) server designed to provide developers with continuous, project-centric context awareness.
731 lines (629 loc) • 24.2 kB
JavaScript
/**
* GlobalPatternRepository.js
*
* Manages global patterns that are available across all sessions.
* Provides functionality to store, retrieve, and manage patterns in the global repository.
*/
import { executeQuery } from "../db.js";
import { v4 as uuidv4 } from "uuid";
/**
* @typedef {Object} PatternDefinition
* @property {string} pattern_type - Type of the pattern
* @property {string} [name] - Human-readable name for the pattern
* @property {string} [description] - Description of what the pattern represents
* @property {string} representation - Textual or structured representation of the pattern
* @property {string|Object} [detection_rules] - Rules used to detect this pattern
* @property {string} [language] - Programming language this pattern applies to (e.g., 'javascript', 'python', or 'any' for language-agnostic patterns)
*/
/**
* @typedef {Object} Pattern
* @property {string} pattern_id - Unique identifier for the pattern
* @property {string} pattern_type - Type of the pattern
* @property {string} name - Human-readable name for the pattern
* @property {string} description - Description of what the pattern represents
* @property {string} representation - Textual or structured representation of the pattern
* @property {string} detection_rules - JSON string of rules used to detect this pattern
* @property {string} language - Programming language this pattern applies to (e.g., 'javascript', 'python', or 'any' for language-agnostic patterns)
* @property {number} frequency - How often this pattern has been observed
* @property {number} utility_score - How useful this pattern is rated
* @property {number} confidence_score - Confidence in this pattern's correctness
* @property {number} reinforcement_count - How many times this pattern has been reinforced
* @property {boolean} is_global - Whether this pattern is global across sessions
* @property {string} created_at - When this pattern was created
* @property {string} updated_at - When this pattern was last updated
*/
/**
* Stores a pattern in the global pattern repository
*
* @param {PatternDefinition} patternDefinition - Definition of the pattern to store
* @param {number} [confidenceScore=0.5] - Confidence score for this pattern (0-1)
* @returns {Promise<string>} The ID of the newly stored global pattern
*/
export async function storeGlobalPattern(
patternDefinition,
confidenceScore = 0.5
) {
try {
// 1. Generate a unique ID for the pattern
const pattern_id = uuidv4();
// 2. Extract and prepare pattern data with defaults
const {
pattern_type,
name = `Global_Pattern_${pattern_id.substring(0, 8)}`,
description = "Globally recognized pattern",
representation,
detection_rules = "{}",
language = "any",
} = patternDefinition;
// 3. Ensure representation and detection_rules are in string format for storage
const representationStr =
typeof representation === "object"
? JSON.stringify(representation)
: representation;
const detectionRulesStr =
typeof detection_rules === "object"
? JSON.stringify(detection_rules)
: detection_rules;
// 4. Set default scores and counters for a global pattern
const frequency = 0; // New global patterns start with zero frequency
const utility_score = 0.5; // Medium utility by default
const reinforcement_count = 1; // Initial reinforcement
const is_global = true; // Mark as global pattern
const created_at = new Date().toISOString();
const updated_at = created_at;
// 5. Insert the pattern into the database
const query = `
INSERT INTO project_patterns (
pattern_id,
pattern_type,
name,
description,
representation,
detection_rules,
language,
frequency,
utility_score,
confidence_score,
reinforcement_count,
is_global,
created_at,
updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
const params = [
pattern_id,
pattern_type,
name,
description,
representationStr,
detectionRulesStr,
language,
frequency,
utility_score,
confidenceScore,
reinforcement_count,
is_global,
created_at,
updated_at,
];
await executeQuery(query, params);
console.log(
`Added new global pattern "${name}" (${pattern_id}) to repository`
);
// 6. Return the generated pattern ID
return pattern_id;
} catch (error) {
console.error("Error adding global pattern to repository:", error);
throw new Error(`Failed to add global pattern: ${error.message}`);
}
}
/**
* Retrieves global patterns from the repository with optional filtering
*
* @param {Object} filterOptions - Options to filter the global patterns
* @param {string} [filterOptions.type] - Filter by pattern type
* @param {number} [filterOptions.minConfidence] - Filter by minimum confidence score
* @param {number} [filterOptions.limit] - Maximum number of patterns to return
* @param {string} [filterOptions.language] - Filter by programming language
* @returns {Promise<Pattern[]>} Array of global patterns matching the filters
*/
export async function retrieveGlobalPatterns(filterOptions = {}) {
try {
const { type, minConfidence, limit, language } = filterOptions;
// Build the query
let query = "SELECT * FROM project_patterns WHERE is_global = TRUE";
const params = [];
// Apply additional filters if provided
if (type) {
query += " AND pattern_type = ?";
params.push(type);
}
if (minConfidence !== undefined && !isNaN(minConfidence)) {
query += " AND confidence_score >= ?";
params.push(minConfidence);
}
// Apply language filter if provided
if (language) {
query += " AND (language = ? OR language = ? OR language IS NULL)";
params.push(language, "any"); // Include language-specific, universal, and legacy NULL patterns
}
// Order by confidence score and utility score
query += " ORDER BY confidence_score DESC, utility_score DESC";
// Apply limit if provided
if (limit !== undefined && !isNaN(limit) && limit > 0) {
query += " LIMIT ?";
params.push(limit);
}
// Execute the query
const patterns = await executeQuery(query, params);
// Check if patterns has a rows property and it's an array
const rows =
patterns && patterns.rows && Array.isArray(patterns.rows)
? patterns.rows
: Array.isArray(patterns)
? patterns
: [];
// If no valid results, return empty array
if (rows.length === 0) {
console.warn("No valid global patterns found");
return [];
}
// Parse detection_rules JSON for each pattern
return rows.map((pattern) => ({
...pattern,
detection_rules: JSON.parse(pattern.detection_rules || "{}"),
is_global: Boolean(pattern.is_global), // Ensure is_global is a boolean
}));
} catch (error) {
console.error("Error retrieving global patterns:", error);
throw new Error(`Failed to retrieve global patterns: ${error.message}`);
}
}
/**
* Promotes an existing pattern to global status
*
* @param {string} patternId - ID of the pattern to promote to global
* @param {number} [newConfidence] - New confidence score to assign (optional)
* @returns {Promise<boolean>} True if the pattern was successfully promoted, false otherwise
*/
export async function promotePatternToGlobal(patternId, newConfidence) {
try {
// Build the update query
let query = "UPDATE project_patterns SET is_global = TRUE";
const params = [];
// Update confidence score if provided
if (newConfidence !== undefined && !isNaN(newConfidence)) {
query += ", confidence_score = ?";
params.push(newConfidence);
}
// Update the timestamp
const updated_at = new Date().toISOString();
query += ", updated_at = ?";
params.push(updated_at);
// Add WHERE clause
query += " WHERE pattern_id = ?";
params.push(patternId);
// Execute the query
const result = await executeQuery(query, params);
// Check if a row was affected
const success = result.affectedRows > 0;
if (success) {
console.log(
`Pattern ${patternId} successfully promoted to global status`
);
if (newConfidence !== undefined) {
console.log(`Updated confidence score to ${newConfidence}`);
}
} else {
console.warn(`No pattern with ID ${patternId} found to promote`);
}
return success;
} catch (error) {
console.error(`Error promoting pattern ${patternId} to global:`, error);
throw new Error(`Failed to promote pattern: ${error.message}`);
}
}
/**
* Records a pattern observation and updates its metrics
*
* @param {string} patternId - ID of the pattern to reinforce
* @param {'usage'|'confirmation'|'rejection'} observationType - Type of the observation
* @param {any} [contextData] - Additional context data for the observation
* @returns {Promise<void>}
*/
export async function reinforcePattern(
patternId,
observationType,
contextData = {}
) {
try {
// Create observation ID
const observation_id = uuidv4();
const timestamp = new Date().toISOString();
// Convert contextData to JSON string
const observation_data = JSON.stringify(contextData || {});
// Define adjustment values
const confidenceAdjustments = {
usage: 0.03, // Small increase for usage
confirmation: 0.05, // Moderate increase for explicit confirmation
rejection: -0.08, // Larger decrease for rejection
};
const utilityAdjustments = {
usage: 0.04, // Moderate increase for usage (indicates utility)
confirmation: 0.03, // Small increase for confirmation
rejection: -0.02, // Small decrease for rejection
};
// Begin transaction
await executeQuery("BEGIN TRANSACTION");
try {
// 1. Insert observation record
const insertObservationQuery = `
INSERT INTO pattern_observations (
observation_id,
pattern_id,
observation_type,
observation_data,
timestamp
) VALUES (?, ?, ?, ?, ?)
`;
await executeQuery(insertObservationQuery, [
observation_id,
patternId,
observationType,
observation_data,
timestamp,
]);
// 2. Get current pattern data
const getPatternQuery =
"SELECT confidence_score, utility_score, reinforcement_count FROM project_patterns WHERE pattern_id = ?";
const patternResult = await executeQuery(getPatternQuery, [patternId]);
if (patternResult.length === 0) {
throw new Error(`Pattern with ID ${patternId} not found`);
}
const pattern = patternResult[0];
// 3. Calculate new scores
let newConfidenceScore =
pattern.confidence_score +
(confidenceAdjustments[observationType] || 0);
let newUtilityScore =
pattern.utility_score + (utilityAdjustments[observationType] || 0);
// Ensure scores stay within bounds
newConfidenceScore = Math.max(0, Math.min(1, newConfidenceScore));
newUtilityScore = Math.max(0, Math.min(1, newUtilityScore));
// 4. Update pattern metrics
const updatePatternQuery = `
UPDATE project_patterns SET
reinforcement_count = reinforcement_count + 1,
confidence_score = ?,
utility_score = ?,
updated_at = ?
`;
// Add last_detected_at update if observation is a usage
const updateLastDetected =
observationType === "usage" ? ", last_detected_at = ?" : "";
const updatePatternParams = [
newConfidenceScore,
newUtilityScore,
timestamp,
];
// Add timestamp parameter if updating last_detected_at
if (observationType === "usage") {
updatePatternParams.push(timestamp);
}
// Complete the query with WHERE clause
const finalUpdateQuery =
updatePatternQuery + updateLastDetected + " WHERE pattern_id = ?";
updatePatternParams.push(patternId);
await executeQuery(finalUpdateQuery, updatePatternParams);
// Commit transaction
await executeQuery("COMMIT");
console.log(
`Pattern ${patternId} reinforced with '${observationType}' observation`
);
} catch (error) {
// Rollback transaction in case of error
await executeQuery("ROLLBACK");
throw error;
}
} catch (error) {
console.error(`Error reinforcing pattern ${patternId}:`, error);
throw new Error(`Failed to reinforce pattern: ${error.message}`);
}
}
/**
* Calculate similarity between two patterns
*
* @param {Pattern} pattern1 - First pattern to compare
* @param {Pattern} pattern2 - Second pattern to compare
* @returns {number} Similarity score between 0 and 1
*/
export function calculatePatternSimilarity(pattern1, pattern2) {
// Initialize similarity scores for different components
let representationSimilarity = 0;
let rulesSimilarity = 0;
let typeSimilarity = 0;
let languageSimilarity = 1.0; // Default to full match for language
// 1. Base type similarity on pattern_type match
typeSimilarity = pattern1.pattern_type === pattern2.pattern_type ? 1.0 : 0.3;
// 2. Check language similarity
if (pattern1.language && pattern2.language) {
// If both patterns have specific languages defined
if (pattern1.language === "any" || pattern2.language === "any") {
// If either is language-agnostic, still a good match but slightly penalized
languageSimilarity = 0.9;
} else if (pattern1.language !== pattern2.language) {
// Different specific languages - significant penalty
languageSimilarity = 0.2; // Significantly different patterns
}
} else if (pattern1.language || pattern2.language) {
// One has a language, the other doesn't (might be a legacy pattern or NULL)
// This is a reasonable match but less confident
languageSimilarity = 0.7;
}
// 3. Compare representations using Jaccard similarity
representationSimilarity = calculateJaccardSimilarity(
extractTokensFromField(pattern1.representation),
extractTokensFromField(pattern2.representation)
);
// 4. Compare detection_rules using Jaccard similarity
rulesSimilarity = calculateJaccardSimilarity(
extractTokensFromField(pattern1.detection_rules),
extractTokensFromField(pattern2.detection_rules)
);
// 5. Combine the similarities with weights
// Representation is the most important, followed by rules, then language and type
const combinedSimilarity =
representationSimilarity * 0.5 +
rulesSimilarity * 0.3 +
languageSimilarity * 0.15 +
typeSimilarity * 0.05;
// Ensure the result is within [0,1]
return Math.max(0, Math.min(1, combinedSimilarity));
}
/**
* Extract tokens from a pattern field which could be a JSON string or regular text
*
* @param {string} field - The field to extract tokens from
* @returns {string[]} Array of normalized tokens
*/
function extractTokensFromField(field) {
if (!field) return [];
let content = field;
// If the field is a JSON string, try to parse it to get its content
if (
typeof field === "string" &&
(field.startsWith("{") || field.startsWith("["))
) {
try {
const parsed = JSON.parse(field);
// Convert the parsed object back to a string for tokenization
content = JSON.stringify(parsed, null, 0).toLowerCase();
} catch (e) {
// If parsing fails, use the original string
content = field.toLowerCase();
}
} else if (typeof field === "object") {
// If it's already an object, stringify it
content = JSON.stringify(field, null, 0).toLowerCase();
} else {
// For plain strings, just use as is
content = String(field).toLowerCase();
}
// Simple tokenization: split by non-alphanumeric chars and filter empty tokens
// In a real implementation, we would use TextTokenizerLogic.tokenize and stem
return content
.split(/[^a-z0-9_]+/)
.filter((token) => token.length > 1)
.map((token) => token.trim());
}
/**
* Calculate Jaccard similarity index between two sets of tokens
*
* @param {string[]} tokens1 - First set of tokens
* @param {string[]} tokens2 - Second set of tokens
* @returns {number} Jaccard similarity index (0-1)
*/
function calculateJaccardSimilarity(tokens1, tokens2) {
if (!tokens1.length && !tokens2.length) return 1.0; // Both empty means identical
if (!tokens1.length || !tokens2.length) return 0.0; // One empty means no similarity
// Create sets from the token arrays to eliminate duplicates
const set1 = new Set(tokens1);
const set2 = new Set(tokens2);
// Calculate intersection size
let intersectionSize = 0;
for (const token of set1) {
if (set2.has(token)) {
intersectionSize++;
}
}
// Calculate union size
const unionSize = set1.size + set2.size - intersectionSize;
// Jaccard similarity = size of intersection / size of union
return intersectionSize / unionSize;
}
/**
* Consolidates session patterns by promoting or merging them with global patterns
*
* @param {Object} options - Options for consolidation
* @param {number} [options.minReinforcementCount=3] - Minimum reinforcement count for promotion
* @param {number} [options.minConfidence=0.6] - Minimum confidence score for promotion
* @param {number} [options.similarityThreshold=0.8] - Threshold for pattern similarity to consider merging
* @returns {Promise<{promoted: number, merged: number}>} Count of promoted and merged patterns
*/
export async function consolidateSessionPatterns(options = {}) {
try {
// Set default options
const {
minReinforcementCount = 3,
minConfidence = 0.6,
similarityThreshold = 0.8,
} = options;
console.log(
`Starting pattern consolidation process (minReinforcementCount=${minReinforcementCount}, minConfidence=${minConfidence})`
);
// Track counts
let promotedCount = 0;
let mergedCount = 0;
// 1. Find non-global patterns that meet the criteria
const query = `
SELECT * FROM project_patterns
WHERE is_global = FALSE
AND reinforcement_count >= ?
AND confidence_score >= ?
`;
const sessionPatterns = await executeQuery(query, [
minReinforcementCount,
minConfidence,
]);
console.log(
`Found ${sessionPatterns.length} session patterns that qualify for promotion or merging`
);
if (sessionPatterns.length === 0) {
return { promoted: 0, merged: 0 };
}
// 2. Get existing global patterns for potential merging
const globalPatterns = await retrieveGlobalPatterns();
// 3. Process each qualifying session pattern
for (const sessionPattern of sessionPatterns) {
const patternId = sessionPattern.pattern_id;
// Try to find a similar global pattern for merging
let shouldPromote = true;
let similarGlobalPattern = null;
for (const globalPattern of globalPatterns) {
const similarity = calculatePatternSimilarity(
sessionPattern,
globalPattern
);
if (similarity >= similarityThreshold) {
shouldPromote = false;
similarGlobalPattern = globalPattern;
break;
}
}
if (shouldPromote) {
// Promote the pattern to global status
console.log(`Promoting session pattern ${patternId} to global status`);
const promoted = await promotePatternToGlobal(
patternId,
sessionPattern.confidence_score
);
if (promoted) {
promotedCount++;
console.log(`Successfully promoted pattern ${patternId}`);
}
} else if (similarGlobalPattern) {
// Placeholder for merging logic
console.log(
`Merge attempt for pattern ${patternId} with ${similarGlobalPattern.pattern_id}`
);
// In a real implementation, the merging would:
// 1. Update the global pattern with some attributes from the session pattern
// 2. Maybe increase confidence/utility/reinforcement_count of the global pattern
// 3. Delete the session pattern or mark it as merged
// For now, just log and count
mergedCount++;
}
}
console.log(
`Pattern consolidation complete. Promoted: ${promotedCount}, Merged: ${mergedCount}`
);
return {
promoted: promotedCount,
merged: mergedCount,
};
} catch (error) {
console.error("Error consolidating session patterns:", error);
throw new Error(`Failed to consolidate session patterns: ${error.message}`);
}
}
/**
* Schedules a periodic background process for pattern consolidation
*
* @param {number} [intervalMinutes=60] - The interval in minutes to run the consolidation
* @returns {number} The interval ID that can be used to clear the interval if needed
*/
export function scheduleConsolidation(intervalMinutes = 60) {
// Convert intervalMinutes to milliseconds
const intervalMs = intervalMinutes * 60 * 1000;
console.log(
`Scheduling pattern consolidation to run every ${intervalMinutes} minutes`
);
// Set up the interval
const intervalId = setInterval(async () => {
console.log(
`Running scheduled pattern consolidation (interval: ${intervalMinutes} minutes)`
);
try {
// Call consolidateSessionPatterns with sensible defaults
const result = await consolidateSessionPatterns({
minReinforcementCount: 5,
minConfidence: 0.7,
});
console.log(
`Pattern consolidation completed: ${result.promoted} patterns promoted, ${result.merged} patterns merged`
);
} catch (error) {
console.error(`Error during scheduled pattern consolidation:`, error);
}
}, intervalMs);
return intervalId;
}
/**
* Retrieves usage statistics for a specific pattern
*
* @param {string} patternId - The ID of the pattern to get statistics for
* @returns {Promise<{usageCount: number, successRate: number, avgConfidence: number}>} Usage statistics
*/
export async function getPatternUsageStats(patternId) {
try {
// Get observation counts from pattern_observations table
const observationsQuery = `
SELECT
COUNT(*) as total_observations,
SUM(CASE WHEN observation_type IN ('usage', 'confirmation') THEN 1 ELSE 0 END) as successful_uses,
SUM(CASE WHEN observation_type = 'rejection' THEN 1 ELSE 0 END) as failed_uses
FROM pattern_observations
WHERE pattern_id = ?
`;
const observationsResult = await executeQuery(observationsQuery, [
patternId,
]);
if (!observationsResult || observationsResult.length === 0) {
return {
usageCount: 0,
successRate: 0,
avgConfidence: 0,
};
}
const stats = observationsResult[0];
const usageCount = stats.total_observations || 0;
// Calculate success rate: (successful uses) / (successful uses + failed uses)
// Avoid division by zero if there are no success/failure observations
const successPlusFailed =
(stats.successful_uses || 0) + (stats.failed_uses || 0);
const successRate =
successPlusFailed > 0
? (stats.successful_uses || 0) / successPlusFailed
: 0;
// Get current confidence score from project_patterns table
const patternQuery = `
SELECT confidence_score
FROM project_patterns
WHERE pattern_id = ?
`;
const patternResult = await executeQuery(patternQuery, [patternId]);
// If pattern not found, return zero confidence
const avgConfidence =
patternResult && patternResult.length > 0
? patternResult[0].confidence_score
: 0;
return {
usageCount,
successRate,
avgConfidence,
};
} catch (error) {
console.error(`Error getting pattern usage stats for ${patternId}:`, error);
throw new Error(`Failed to get pattern usage statistics: ${error.message}`);
}
}