UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

291 lines 11 kB
/** * Deterministic Codemod Engine (ADR-143) * * Truly $0, no-LLM Tier-1 code transforms. Uses the TypeScript compiler API to * locate exact AST nodes, then applies formatting-preserving text-range edits to * the original source string (we never re-print the whole file, so comments and * formatting survive). * * Only intents that can be transformed *deterministically and safely* live here. * Intents that need inference or judgement (add-types, add-error-handling, * async-await) are intentionally NOT codemods — they route to a model. See * ADR-143 for the rationale. * * @module ruvector/codemods/engine */ import ts from 'typescript'; import { buildReassignmentResolver } from './scope-analysis.js'; /** Intents this engine can apply deterministically with $0 cost. */ export const DETERMINISTIC_CODEMOD_INTENTS = [ 'var-to-const', 'remove-console', 'add-logging', ]; /** Intents recognised by the router but NOT safe as deterministic codemods. */ export const MODEL_ROUTED_INTENTS = [ 'add-types', 'add-error-handling', 'async-await', ]; export function isDeterministicCodemod(intent) { return DETERMINISTIC_CODEMOD_INTENTS.includes(intent); } // ============================================================================ // Parsing & edit application // ============================================================================ function scriptKind(language) { switch (language) { case 'typescript': return ts.ScriptKind.TS; case 'tsx': return ts.ScriptKind.TSX; case 'jsx': return ts.ScriptKind.JSX; default: return ts.ScriptKind.JS; } } function parse(code, language) { return ts.createSourceFile(`codemod-input.${language}`, code, ts.ScriptTarget.Latest, /* setParentNodes */ true, scriptKind(language)); } function parseDiagnosticCount(sf) { // parseDiagnostics is internal but stable; guard so a missing field never throws. return sf.parseDiagnostics?.length ?? 0; } /** Apply edits to the original source, right-to-left so offsets stay valid. */ function applyEdits(code, edits) { const sorted = [...edits].sort((a, b) => b.start - a.start); let out = code; for (const e of sorted) { out = out.slice(0, e.start) + e.replacement + out.slice(e.end); } return out; } function lineStartOffset(code, pos) { let i = pos; while (i > 0 && code[i - 1] !== '\n') i--; return i; } function leadingIndent(code, lineStart) { let i = lineStart; while (i < code.length && (code[i] === ' ' || code[i] === '\t')) i++; return code.slice(lineStart, i); } // ============================================================================ // var-to-const (scope-aware reassignment analysis — see scope-analysis.ts) // ============================================================================ function isVarList(list) { return !(list.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)); } function collectBindingNames(name, out) { if (ts.isIdentifier(name)) { out.add(name.text); return; } for (const el of name.elements) { if (ts.isBindingElement(el)) collectBindingNames(el.name, out); } } function varToConstEdits(code, sf) { const resolver = buildReassignmentResolver(sf); const edits = []; const visit = (node) => { if (ts.isVariableDeclarationList(node) && isVarList(node)) { const names = new Set(); for (const decl of node.declarations) collectBindingNames(decl.name, names); // `const` only when no declared binding is reassigned *within its own scope*. const anyReassigned = [...names].some((n) => resolver.isReassigned(n, node)); const keyword = anyReassigned ? 'let' : 'const'; // The `var` keyword is the first token of the declaration list. const start = node.getStart(sf); // Only rewrite when the literal text really is `var` (guards against odd trivia). if (code.slice(start, start + 3) === 'var') { edits.push({ start, end: start + 3, replacement: keyword }); } } ts.forEachChild(node, visit); }; visit(sf); return edits; } // ============================================================================ // remove-console // ============================================================================ function rootIdentifier(expr) { let cur = expr; while (ts.isPropertyAccessExpression(cur) || ts.isElementAccessExpression(cur)) { cur = cur.expression; } return ts.isIdentifier(cur) ? cur : undefined; } function isConsoleCallStatement(stmt) { if (!ts.isExpressionStatement(stmt)) return false; let expr = stmt.expression; // unwrap `void console.log(x)` and awaited/comma forms defensively if (ts.isCallExpression(expr)) { const root = rootIdentifier(expr.expression); return !!root && root.text === 'console'; } return false; } function removeConsoleEdits(code, sf) { const edits = []; const visit = (node) => { if (isConsoleCallStatement(node)) { const stmt = node; const start = stmt.getStart(sf); const end = stmt.getEnd(); const ls = lineStartOffset(code, start); const beforeIsBlank = code.slice(ls, start).trim() === ''; // Is the rest of the line after the statement only whitespace? let lineEnd = end; while (lineEnd < code.length && code[lineEnd] !== '\n') lineEnd++; const afterIsBlank = code.slice(end, lineEnd).trim() === ''; if (beforeIsBlank && afterIsBlank) { // Statement owns its line(s): drop the whole line incl. trailing newline. const dropEnd = lineEnd < code.length ? lineEnd + 1 : lineEnd; edits.push({ start: ls, end: dropEnd, replacement: '' }); } else { // Inline with other code: remove just the statement, tidy one trailing space. let e = end; if (code[e] === ' ') e++; edits.push({ start, end: e, replacement: '' }); } } ts.forEachChild(node, visit); }; visit(sf); return edits; } function isFunctionLikeWithBlock(node) { return ((ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node) || ts.isConstructorDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node)) && !!node.body && ts.isBlock(node.body)); } function functionName(node) { if (ts.isConstructorDeclaration(node)) return 'constructor'; if ((ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isMethodDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node)) && node.name && (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name))) { return node.name.text; } // function expression / arrow assigned to a variable or property const parent = node.parent; if (parent && ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) return parent.name.text; if (parent && ts.isPropertyAssignment(parent) && ts.isIdentifier(parent.name)) return parent.name.text; return 'anonymous'; } function alreadyLogsEntry(body, name) { const first = body.statements[0]; if (!first || !isConsoleCallStatement(first)) return false; const text = first.getText(); return text.includes(`'${name} called'`) || text.includes(`"${name} called"`); } function addLoggingEdits(code, sf) { const edits = []; const visit = (node) => { if (isFunctionLikeWithBlock(node)) { const body = node.body; const name = functionName(node); if (!alreadyLogsEntry(body, name)) { const braceOffset = body.getStart(sf); // position of '{' const headerLineStart = lineStartOffset(code, node.getStart(sf)); const indent = leadingIndent(code, headerLineStart); const bodyIndent = indent + ' '; const insertion = `\n${bodyIndent}console.log(${JSON.stringify(`${name} called`)});`; edits.push({ start: braceOffset + 1, end: braceOffset + 1, replacement: insertion }); } } ts.forEachChild(node, visit); }; visit(sf); return edits; } // ============================================================================ // Public API // ============================================================================ const TRANSFORMS = { 'var-to-const': varToConstEdits, 'remove-console': removeConsoleEdits, 'add-logging': addLoggingEdits, }; /** * Apply a deterministic codemod to a source string. * * Returns the transformed source plus metadata. Never throws on malformed input * — it reports `success: false` with a reason instead. Guarantees the output * does not introduce new parse errors (otherwise it returns the input unchanged). */ export function applyCodemod(intent, code, opts = {}) { const language = opts.language ?? 'typescript'; if (!isDeterministicCodemod(intent)) { return { intent: intent, success: false, changed: false, output: code, edits: 0, language, reason: `"${intent}" is not a deterministic codemod — route it to a model (Tier 2/3).`, }; } let sf; try { sf = parse(code, language); } catch (err) { return { intent, success: false, changed: false, output: code, edits: 0, language, reason: `parse failed: ${err.message}`, }; } const beforeDiagnostics = parseDiagnosticCount(sf); const edits = TRANSFORMS[intent](code, sf); if (edits.length === 0) { return { intent, success: true, changed: false, output: code, edits: 0, language }; } const output = applyEdits(code, edits); // Safety net: never hand back source that parses worse than the input. const afterDiagnostics = parseDiagnosticCount(parse(output, language)); if (afterDiagnostics > beforeDiagnostics) { return { intent, success: false, changed: false, output: code, edits: 0, language, reason: 'transform would introduce parse errors — aborted (input returned unchanged).', }; } return { intent, success: true, changed: true, output, edits: edits.length, language }; } //# sourceMappingURL=engine.js.map