334 lines
9 KiB
JavaScript
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,
|
|
};
|