singularity-forge/src/resources/extensions/sf/knowledge-injector.js

334 lines
9 KiB
JavaScript

/**
* Knowledge Injector — automatically injects relevant learnings into dispatch prompts.
*
* Purpose: During milestone planning, query KNOWLEDGE.md for relevant learnings and
* inject them into execute-task, plan-slice, and other dispatch prompts. This makes
* accumulated knowledge actionable in future runs instead of inert.
*
* Consumer: auto-prompts.js when loading prompts for dispatch.
*
* Implementation:
* 1. Parse KNOWLEDGE.md judgment-log entries
* 2. Extract key concepts (tags, domains, failure modes)
* 3. Use semantic similarity scoring to match against current task context
* 4. Inject high-confidence (>0.8) knowledge into prompt variables
* 5. Track which knowledge was used (feedback loop)
*/
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { logWarning } from "./workflow-logger.js";
/**
* Parse KNOWLEDGE.md and extract judgment-log entries.
*
* Format expected:
* ```
* ### Judgment Entry: <title>
* - **Evidence:** <source>
* - **Confidence:** 0.95
* - **Domain:** <domain>
* - **Recommendation:** <action>
* ```
*/
function parseKnowledgeEntries(knowledgeContent) {
const entries = [];
const entryPattern = /### Judgment Entry:\s*(.+?)\n([\s\S]*?)(?=###\s|$)/g;
let match;
while ((match = entryPattern.exec(knowledgeContent)) !== null) {
const title = match[1].trim();
const body = match[2];
// Extract fields
const evidenceMatch = body.match(
/[-*]\s+\*?\*?Evidence:\*?\*?\s*(.+?)(?:\n|$)/,
);
const confidenceMatch = body.match(
/[-*]\s+\*?\*?Confidence:\*?\*?\s*([\d.]+)/,
);
const domainMatch = body.match(
/[-*]\s+\*?\*?Domain:\*?\*?\s*(.+?)(?:\n|$)/,
);
const recommendationMatch = body.match(
/[-*]\s+\*?\*?Recommendation:\*?\*?\s*(.+?)(?:\n|$)/,
);
entries.push({
title,
evidence: evidenceMatch ? evidenceMatch[1].trim() : "",
confidence: confidenceMatch ? parseFloat(confidenceMatch[1]) : 0.5,
domain: domainMatch ? domainMatch[1].trim() : "general",
recommendation: recommendationMatch ? recommendationMatch[1].trim() : "",
body: body.trim(),
});
}
return entries;
}
/**
* Extract key concepts (domain tags, failure modes, constraints) from knowledge entry.
*
* Used for semantic similarity matching.
*/
function extractConcepts(entry) {
const concepts = new Set();
// Add domain
if (entry.domain) concepts.add(entry.domain);
// Extract key phrases
const phrasePatterns = [
/avoid\s+(\w+)/gi,
/use\s+(\w+)/gi,
/requires?\s+(\w+)/gi,
/prevents?\s+(\w+)/gi,
/bug.*?(\w+)/gi,
/error.*?(\w+)/gi,
];
for (const pattern of phrasePatterns) {
let match;
while ((match = pattern.exec(entry.body)) !== null) {
concepts.add(match[1].toLowerCase());
}
}
// Add title keywords
const titleKeywords = entry.title.split(/\s+/).filter((w) => w.length > 3);
titleKeywords.forEach((w) => concepts.add(w.toLowerCase()));
return Array.from(concepts);
}
/**
* Semantic similarity scoring (simple keyword-based for now).
*
* Purpose: Match knowledge entries to current task context.
* Returns: 0.0-1.0 score
*/
function semanticSimilarity(knowledgeConcepts, contextKeywords) {
if (!contextKeywords || contextKeywords.length === 0) return 0;
const contextSet = new Set(contextKeywords.map((k) => k.toLowerCase()));
const matches = knowledgeConcepts.filter((c) => contextSet.has(c));
// Score: proportion of knowledge concepts that appear in context
return matches.length / Math.max(knowledgeConcepts.length, 1);
}
/**
* Find relevant knowledge for a given task context.
*
* Purpose: Given task domain/keywords, return matching knowledge entries.
*
* Parameters:
* - knowledgeEntries: parsed KNOWLEDGE.md entries
* - contextKeywords: task domain, task type, technology stack keywords
* - minConfidence: filter entries below this confidence threshold (default 0.6)
* - minSimilarity: filter entries below this similarity score (default 0.5)
*
* Returns: sorted array of relevant entries with scores
*/
export function findRelevantKnowledge(
knowledgeEntries,
contextKeywords,
minConfidence = 0.6,
minSimilarity = 0.5,
) {
const relevant = [];
for (const entry of knowledgeEntries) {
// Filter by confidence
if (entry.confidence < minConfidence) continue;
// Score similarity
const concepts = extractConcepts(entry);
const similarity = semanticSimilarity(concepts, contextKeywords);
if (similarity >= minSimilarity) {
relevant.push({
entry,
similarity,
score: entry.confidence * 0.7 + similarity * 0.3, // Weighted score
});
}
}
// Sort by combined score
return relevant.sort((a, b) => b.score - a.score);
}
/**
* Format knowledge for injection into prompts.
*
* Purpose: Convert knowledge entries to readable injection text for prompts.
*/
function formatKnowledgeForInjection(relevantKnowledge) {
if (!relevantKnowledge || relevantKnowledge.length === 0) {
return "(no relevant knowledge)";
}
const lines = ["## Relevant Prior Learning"];
for (const item of relevantKnowledge.slice(0, 5)) {
const { entry, score } = item;
const confidence = (entry.confidence * 100).toFixed(0);
const relevance = (score * 100).toFixed(0);
lines.push(
`\n### ${entry.title} [confidence: ${confidence}%, relevance: ${relevance}%]`,
);
lines.push(`**Domain:** ${entry.domain}`);
lines.push(`**Evidence:** ${entry.evidence}`);
lines.push(`**Recommendation:** ${entry.recommendation}`);
lines.push(`\n${entry.body}`);
}
return lines.join("\n");
}
/**
* Detect contradictory knowledge entries.
*
* Purpose: Flag when knowledge advises conflicting actions (e.g., "use Python 3.12"
* vs. "avoid Python 3.12") so triage agents can resolve ambiguity.
*/
export function detectContradictions(knowledgeEntries) {
const contradictions = [];
const recommendations = new Map();
for (const entry of knowledgeEntries) {
const rec = entry.recommendation.toLowerCase();
if (!recommendations.has(rec)) {
recommendations.set(rec, []);
}
recommendations.get(rec).push(entry);
}
// Find conflicting patterns (e.g., "use X" vs "avoid X")
for (const [rec, entries] of recommendations.entries()) {
// Check for explicit conflicts
if (rec.includes("avoid") || rec.includes("don't")) {
const contradictingRec = rec.replace(/avoid|don't\s+/i, "use ");
if (recommendations.has(contradictingRec)) {
contradictions.push({
type: "direct_conflict",
entries,
conflictingEntries: recommendations.get(contradictingRec),
});
}
}
}
return contradictions;
}
/**
* Load and parse KNOWLEDGE.md from project.
*/
function loadKnowledgeFile(basePath) {
const candidates = [
join(basePath, ".sf", "KNOWLEDGE.md"),
join(basePath, "KNOWLEDGE.md"),
];
for (const p of candidates) {
if (existsSync(p)) {
try {
return readFileSync(p, "utf-8");
} catch {}
}
}
return null;
}
/**
* Main API: Inject knowledge into prompt variables.
*
* Purpose: This is called by auto-prompts.js when loading prompts, to add
* {{knowledgeInjection}} variables automatically.
*
* Parameters:
* - basePath: project root
* - taskContext: { domain, keywords, taskType, technology } — context for matching
* - options: { minConfidence, minSimilarity, maxEntries }
*
* Returns: formatted string suitable for prompt variable substitution
*/
export function injectKnowledgeIntPrompt(
basePath,
taskContext = {},
options = {},
) {
const knowledgeContent = loadKnowledgeFile(basePath);
if (!knowledgeContent) {
return "(knowledge base unavailable)";
}
const entries = parseKnowledgeEntries(knowledgeContent);
if (entries.length === 0) {
return "(no knowledge entries found)";
}
// Extract context keywords
const contextKeywords = [
taskContext.domain,
taskContext.taskType,
...(taskContext.keywords || []),
...(taskContext.technology || []),
].filter(Boolean);
// Find relevant knowledge
const minConfidence = options.minConfidence ?? 0.7;
const minSimilarity = options.minSimilarity ?? 0.5;
const relevant = findRelevantKnowledge(
entries,
contextKeywords,
minConfidence,
minSimilarity,
);
// Check for contradictions (log warning if found)
const contradictions = detectContradictions(entries);
if (contradictions.length > 0) {
logWarning(
"knowledge-injector",
`${contradictions.length} contradictory knowledge entries detected`,
);
}
// Format and return
return formatKnowledgeForInjection(relevant);
}
/**
* Track knowledge usage for feedback loop.
*
* Purpose: Record which knowledge was actually used in a dispatch so we can
* later measure effectiveness and refine knowledge compounding.
*/
export function trackKnowledgeUsage(_basePath, taskId, injectedKnowledge) {
// This would write to a usage log in .sf/knowledge-usage.jsonl
// Implementation deferred to feedback-loop integration
return {
taskId,
injectedCount: injectedKnowledge.length,
timestamp: new Date().toISOString(),
};
}
export default {
injectKnowledgeIntPrompt,
findRelevantKnowledge,
detectContradictions,
parseKnowledgeEntries,
extractConcepts,
semanticSimilarity,
formatKnowledgeForInjection,
loadKnowledgeFile,
trackKnowledgeUsage,
};