tasq/node_modules/@claude-flow/guidance/dist/compiler.js

419 lines
16 KiB
JavaScript

/**
* 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