/** * Guidance Compiler * * Parses root CLAUDE.md and optional CLAUDE.local.md into a compiled policy bundle: * 1. A small always-loaded constitution (first 30-60 lines of invariants) * 2. A set of task-scoped rule shards tagged by intent, risk, domain, repo path, tool class * 3. A machine-readable manifest with rule IDs, triggers, and verifiers * * @module @claude-flow/guidance/compiler */ import { createHash } from 'node:crypto'; // ============================================================================ // Parser Patterns // ============================================================================ /** Matches a rule declaration: "R001:" or "RULE-001:" or "[R001]" or "- [R001]" */ const RULE_ID_PATTERN = /^(?:#{1,4}\s+)?(?:[-*]\s+)?\[?([A-Z]+-?\d{3,4})\]?[:\s]/; /** Matches risk class annotations: "(critical)", "[high-risk]", etc. */ const RISK_PATTERN = /\(?(critical|high|medium|low|info)(?:-risk)?\)?/i; /** Matches domain tags: @security, @testing, etc. */ const DOMAIN_TAG_PATTERN = /@(security|testing|performance|architecture|debugging|deployment|general)/gi; /** Matches tool class tags: [edit], [bash], etc. */ const TOOL_TAG_PATTERN = /\[(edit|bash|read|write|mcp|task|all)\]/gi; /** Matches intent tags: #bug-fix, #feature, etc. */ const INTENT_TAG_PATTERN = /#(bug-fix|feature|refactor|security|performance|testing|docs|deployment|architecture|debug|general)/gi; /** Matches repo scope: scope:src/**, scope:tests/**, etc. */ const SCOPE_PATTERN = /scope:([\w\/\*\.\-]+)/gi; /** Matches verifier annotations: verify:tests-pass, verify:lint-clean */ const VERIFIER_PATTERN = /verify:([\w\-]+)/i; /** Matches priority override: priority:N */ const PRIORITY_PATTERN = /priority:(\d+)/i; /** Section markers for constitution identification */ const CONSTITUTION_MARKERS = [ /^#+\s*(safety|security|invariant|constitution|critical|non[- ]?negotiable|always)/i, /^#+\s*(must|never|always|required|mandatory)/i, ]; /** Section markers for shard boundaries */ const SHARD_MARKERS = [ /^#+\s/, // Any heading /^---+\s*$/, // Horizontal rule /^\*\*\*+\s*$/, // Bold horizontal rule ]; const DEFAULT_CONFIG = { maxConstitutionLines: 60, defaultRiskClass: 'medium', defaultPriority: 50, autoGenerateIds: true, }; // ============================================================================ // Guidance Compiler // ============================================================================ export class GuidanceCompiler { config; nextAutoId = 1; constructor(config = {}) { this.config = { ...DEFAULT_CONFIG, ...config }; } /** * Compile guidance files into a policy bundle */ compile(rootContent, localContent) { // Parse both files into raw rules const rootRules = this.parseGuidanceFile(rootContent, 'root'); const localRules = localContent ? this.parseGuidanceFile(localContent, 'local') : []; // Merge rules (local overrides root for same ID) const allRules = this.mergeRules(rootRules, localRules); // Split into constitution and shards const constitutionRules = allRules.filter(r => r.isConstitution); const shardRules = allRules.filter(r => !r.isConstitution); // Build constitution const constitution = this.buildConstitution(constitutionRules); // Build shards const shards = this.buildShards(shardRules); // Build manifest const manifest = this.buildManifest(allRules, rootContent, localContent); return { constitution, shards, manifest }; } /** * Parse a guidance markdown file into rules */ parseGuidanceFile(content, source) { const rules = []; const lines = content.split('\n'); let currentSection = ''; let currentBlock = []; let inConstitutionSection = false; let blockStartLine = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Detect section boundaries if (SHARD_MARKERS.some(m => m.test(line))) { // Flush current block if (currentBlock.length > 0) { const blockRules = this.extractRulesFromBlock(currentBlock.join('\n'), source, inConstitutionSection, currentSection); rules.push(...blockRules); currentBlock = []; } // Check if this is a constitution section inConstitutionSection = CONSTITUTION_MARKERS.some(m => m.test(line)); currentSection = line.replace(/^#+\s*/, '').trim(); blockStartLine = i; } currentBlock.push(line); } // Flush last block if (currentBlock.length > 0) { const blockRules = this.extractRulesFromBlock(currentBlock.join('\n'), source, inConstitutionSection, currentSection); rules.push(...blockRules); } return rules; } /** * Extract rules from a content block */ extractRulesFromBlock(block, source, isConstitution, section) { const rules = []; const lines = block.split('\n'); // Try to extract explicit rules (with IDs) let ruleBuffer = []; let currentRuleId = null; for (const line of lines) { const idMatch = line.match(RULE_ID_PATTERN); if (idMatch) { // Flush previous rule if (currentRuleId && ruleBuffer.length > 0) { rules.push(this.parseRule(currentRuleId, ruleBuffer.join('\n'), source, isConstitution)); ruleBuffer = []; } currentRuleId = idMatch[1]; ruleBuffer.push(line.replace(RULE_ID_PATTERN, '').trim()); } else if (currentRuleId) { ruleBuffer.push(line); } } // Flush last rule if (currentRuleId && ruleBuffer.length > 0) { rules.push(this.parseRule(currentRuleId, ruleBuffer.join('\n'), source, isConstitution)); } // If no explicit rules found, try to extract implicit rules from bullet points if (rules.length === 0) { const implicitRules = this.extractImplicitRules(block, source, isConstitution, section); rules.push(...implicitRules); } return rules; } /** * Extract implicit rules from bullet points and paragraphs */ extractImplicitRules(block, source, isConstitution, section) { const rules = []; const lines = block.split('\n'); for (const line of lines) { const trimmed = line.trim(); // Skip empty lines, headings, and non-actionable content if (!trimmed || /^#+\s/.test(trimmed) || /^---/.test(trimmed)) continue; // Match actionable bullet points const bulletMatch = trimmed.match(/^[-*]\s+(.+)/); if (!bulletMatch) continue; const text = bulletMatch[1].trim(); // Only create rules for actionable statements if (!this.isActionableRule(text)) continue; // Auto-generate ID if enabled if (this.config.autoGenerateIds) { const id = `AUTO-${String(this.nextAutoId++).padStart(3, '0')}`; rules.push(this.parseRule(id, text, source, isConstitution)); } } return rules; } /** * Check if text represents an actionable rule */ isActionableRule(text) { const actionPatterns = [ /\b(must|never|always|should|require|forbid|ensure|validate|check|verify)\b/i, /\b(do not|don't|cannot|can't|avoid|prevent|block|deny|reject)\b/i, /\b(use|prefer|apply|follow|implement|enforce|maintain|keep|run|include|write|mock|respect)\b/i, ]; return actionPatterns.some(p => p.test(text)); } /** * Parse a single rule from its text content */ parseRule(id, text, source, isConstitution) { const now = Date.now(); // Extract risk class const riskMatch = text.match(RISK_PATTERN); const riskClass = riskMatch?.[1]?.toLowerCase() ?? this.config.defaultRiskClass; // Extract tool classes const toolClasses = []; let toolMatch; const toolRegex = new RegExp(TOOL_TAG_PATTERN.source, 'gi'); while ((toolMatch = toolRegex.exec(text)) !== null) { toolClasses.push(toolMatch[1].toLowerCase()); } if (toolClasses.length === 0) toolClasses.push('all'); // Extract intents const intents = []; let intentMatch; const intentRegex = new RegExp(INTENT_TAG_PATTERN.source, 'gi'); while ((intentMatch = intentRegex.exec(text)) !== null) { intents.push(intentMatch[1].toLowerCase()); } if (intents.length === 0) { intents.push(...this.inferIntents(text)); } // Extract domains const domains = []; let domainMatch; const domainRegex = new RegExp(DOMAIN_TAG_PATTERN.source, 'gi'); while ((domainMatch = domainRegex.exec(text)) !== null) { domains.push(domainMatch[1].toLowerCase()); } if (domains.length === 0) { domains.push(...this.inferDomains(text)); } // Extract repo scopes const repoScopes = []; let scopeMatch; const scopeRegex = new RegExp(SCOPE_PATTERN.source, 'gi'); while ((scopeMatch = scopeRegex.exec(text)) !== null) { repoScopes.push(scopeMatch[1]); } if (repoScopes.length === 0) repoScopes.push('**/*'); // Extract verifier const verifierMatch = text.match(VERIFIER_PATTERN); const verifier = verifierMatch?.[1]; // Extract priority const priorityMatch = text.match(PRIORITY_PATTERN); const priority = priorityMatch ? parseInt(priorityMatch[1], 10) : this.config.defaultPriority; // Clean rule text (remove annotations) const cleanText = text .replace(RISK_PATTERN, '') .replace(TOOL_TAG_PATTERN, '') .replace(INTENT_TAG_PATTERN, '') .replace(DOMAIN_TAG_PATTERN, '') .replace(SCOPE_PATTERN, '') .replace(VERIFIER_PATTERN, '') .replace(PRIORITY_PATTERN, '') .replace(/\s+/g, ' ') .trim(); return { id, text: cleanText, riskClass, toolClasses, intents, repoScopes, domains, priority: isConstitution ? priority + 100 : priority, source, isConstitution, verifier, createdAt: now, updatedAt: now, }; } /** * Infer intents from rule text */ inferIntents(text) { const intents = []; const lower = text.toLowerCase(); if (/secur|auth|secret|password|token|cve|vuln|encrypt/i.test(lower)) intents.push('security'); if (/test|spec|mock|coverage|assert|tdd/i.test(lower)) intents.push('testing'); if (/perf|optim|fast|slow|cache|memory|speed/i.test(lower)) intents.push('performance'); if (/refactor|clean|restructur|simplif/i.test(lower)) intents.push('refactor'); if (/bug|fix|error|broken|fail|debug/i.test(lower)) intents.push('bug-fix'); if (/architect|design|pattern|structure|boundary/i.test(lower)) intents.push('architecture'); if (/deploy|release|publish|ci|cd/i.test(lower)) intents.push('deployment'); if (/doc|readme|comment|jsdoc/i.test(lower)) intents.push('docs'); return intents.length > 0 ? intents : ['general']; } /** * Infer domains from rule text */ inferDomains(text) { const domains = []; const lower = text.toLowerCase(); if (/secur|auth|secret|password|token|cve|vuln/i.test(lower)) domains.push('security'); if (/test|spec|mock|coverage|assert/i.test(lower)) domains.push('testing'); if (/perf|optim|fast|slow|cache|speed/i.test(lower)) domains.push('performance'); if (/architect|design|ddd|domain|boundary/i.test(lower)) domains.push('architecture'); if (/bug|fix|error|debug/i.test(lower)) domains.push('debugging'); return domains.length > 0 ? domains : ['general']; } /** * Merge root and local rules, local overrides root for same ID */ mergeRules(rootRules, localRules) { const ruleMap = new Map(); for (const rule of rootRules) { ruleMap.set(rule.id, rule); } for (const rule of localRules) { if (ruleMap.has(rule.id)) { // Local overrides root, but mark as updated const existing = ruleMap.get(rule.id); ruleMap.set(rule.id, { ...rule, priority: Math.max(rule.priority, existing.priority), updatedAt: Date.now(), }); } else { ruleMap.set(rule.id, rule); } } return Array.from(ruleMap.values()).sort((a, b) => b.priority - a.priority); } /** * Build the constitution from constitution-class rules */ buildConstitution(rules) { // Sort by priority descending const sorted = [...rules].sort((a, b) => b.priority - a.priority); // Build compact text const lines = [ '# Constitution - Always Active Rules', '', ]; let currentDomain = ''; for (const rule of sorted) { const domain = rule.domains[0] || 'general'; if (domain !== currentDomain) { currentDomain = domain; lines.push(`## ${domain.charAt(0).toUpperCase() + domain.slice(1)}`); } lines.push(`- [${rule.id}] ${rule.text}`); } // Trim to max lines const text = lines.slice(0, this.config.maxConstitutionLines).join('\n'); return { rules: sorted, text, hash: this.hashContent(text), }; } /** * Build shards from non-constitution rules */ buildShards(rules) { return rules.map(rule => ({ rule, compactText: this.buildCompactShardText(rule), })); } /** * Build compact text for a shard */ buildCompactShardText(rule) { const tags = [ rule.riskClass, ...rule.domains, ...rule.intents, ...rule.toolClasses.filter(t => t !== 'all'), ].map(t => `@${t}`).join(' '); return `[${rule.id}] ${rule.text} ${tags}`.trim(); } /** * Build the manifest */ buildManifest(allRules, rootContent, localContent) { const sourceHashes = { root: this.hashContent(rootContent), }; if (localContent) { sourceHashes.local = this.hashContent(localContent); } return { rules: allRules.map(r => ({ id: r.id, triggers: [...r.intents, ...r.domains, ...r.toolClasses], verifier: r.verifier ?? null, riskClass: r.riskClass, priority: r.priority, source: r.source, })), compiledAt: Date.now(), sourceHashes, totalRules: allRules.length, constitutionRules: allRules.filter(r => r.isConstitution).length, shardRules: allRules.filter(r => !r.isConstitution).length, }; } /** * Hash content for change detection */ hashContent(content) { return createHash('sha256').update(content).digest('hex').slice(0, 16); } } /** * Create a compiler instance */ export function createCompiler(config) { return new GuidanceCompiler(config); } //# sourceMappingURL=compiler.js.map