Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions docs/roadmap/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -612,9 +612,10 @@ Rewrite the CFG algorithm as a node-level visitor that builds basic blocks and e
- ✅ `queries.js` CLI wrappers → `queries-cli.js` (15 functions)
- ✅ Shared `result-formatter.js` (`outputResult` for JSON/NDJSON dispatch)
- ✅ Shared `test-filter.js` (`isTestFile` predicate)
- 🔲 Extract CLI wrappers from remaining modules (audit, batch, check, cochange, communities, complexity, cfg, dataflow, flow, manifesto, owners, structure, triage, branch-compare)
- 🔲 Introduce `CommandRunner` shared lifecycle
- 🔲 Per-command `src/commands/` directory structure
- ✅ Extract CLI wrappers from remaining modules (audit, batch, check, cochange, communities, complexity, cfg, dataflow, flow, manifesto, owners, structure, triage, branch-compare, sequence)
- ✅ Per-command `src/commands/` directory structure (16 command files)
- ✅ Move shared utilities to `src/infrastructure/` (result-formatter.js, test-filter.js)
- 🔲 Introduce `CommandRunner` shared lifecycle (command files vary too much for a single pattern today — revisit once commands stabilize)

Eliminate the `*Data()` / `*()` dual-function pattern replicated across 19 modules. Every analysis module (queries, audit, batch, check, cochange, communities, complexity, cfg, dataflow, ast, flow, manifesto, owners, structure, triage, branch-compare, viewer) currently implements both data extraction AND CLI formatting.

Expand Down
3 changes: 1 addition & 2 deletions src/ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@ import { buildExtensionSet } from './ast-analysis/shared.js';
import { walkWithVisitors } from './ast-analysis/visitor.js';
import { createAstStoreVisitor } from './ast-analysis/visitors/ast-store-visitor.js';
import { openReadonlyOrFail } from './db.js';
import { outputResult } from './infrastructure/result-formatter.js';
import { debug } from './logger.js';
import { paginateResult } from './paginate.js';

import { outputResult } from './result-formatter.js';

// ─── Constants ────────────────────────────────────────────────────────

export const AST_NODE_KINDS = ['call', 'new', 'string', 'regex', 'throw', 'await'];
Expand Down
89 changes: 2 additions & 87 deletions src/audit.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@
import path from 'node:path';
import { loadConfig } from './config.js';
import { openReadonlyOrFail } from './db.js';
import { isTestFile } from './infrastructure/test-filter.js';
import { RULE_DEFS } from './manifesto.js';
import { explainData, kindIcon } from './queries.js';
import { outputResult } from './result-formatter.js';
import { isTestFile } from './test-filter.js';
import { explainData } from './queries.js';

// ─── Threshold resolution ───────────────────────────────────────────

Expand Down Expand Up @@ -336,87 +335,3 @@ function defaultHealth() {
thresholdBreaches: [],
};
}

// ─── CLI formatter ──────────────────────────────────────────────────

export function audit(target, customDbPath, opts = {}) {
const data = auditData(target, customDbPath, opts);

if (outputResult(data, null, opts)) return;

if (data.functions.length === 0) {
console.log(`No ${data.kind === 'file' ? 'file' : 'function/symbol'} matching "${target}"`);
return;
}

console.log(`\n# Audit: ${target} (${data.kind})`);
console.log(` ${data.functions.length} function(s) analyzed\n`);

for (const fn of data.functions) {
const lineRange = fn.endLine ? `${fn.line}-${fn.endLine}` : `${fn.line}`;
const roleTag = fn.role ? ` [${fn.role}]` : '';
console.log(`## ${kindIcon(fn.kind)} ${fn.name} (${fn.kind})${roleTag}`);
console.log(` ${fn.file}:${lineRange}${fn.lineCount ? ` (${fn.lineCount} lines)` : ''}`);
if (fn.summary) console.log(` ${fn.summary}`);
if (fn.signature) {
if (fn.signature.params != null) console.log(` Parameters: (${fn.signature.params})`);
if (fn.signature.returnType) console.log(` Returns: ${fn.signature.returnType}`);
}

// Health metrics
if (fn.health.cognitive != null) {
console.log(`\n Health:`);
console.log(
` Cognitive: ${fn.health.cognitive} Cyclomatic: ${fn.health.cyclomatic} Nesting: ${fn.health.maxNesting}`,
);
console.log(` MI: ${fn.health.maintainabilityIndex}`);
if (fn.health.halstead.volume) {
console.log(
` Halstead: vol=${fn.health.halstead.volume} diff=${fn.health.halstead.difficulty} effort=${fn.health.halstead.effort} bugs=${fn.health.halstead.bugs}`,
);
}
if (fn.health.loc) {
console.log(
` LOC: ${fn.health.loc} SLOC: ${fn.health.sloc} Comments: ${fn.health.commentLines}`,
);
}
}

// Threshold breaches
if (fn.health.thresholdBreaches.length > 0) {
console.log(`\n Threshold Breaches:`);
for (const b of fn.health.thresholdBreaches) {
const icon = b.level === 'fail' ? 'FAIL' : 'WARN';
console.log(` [${icon}] ${b.metric}: ${b.value} >= ${b.threshold}`);
}
}

// Impact
console.log(`\n Impact: ${fn.impact.totalDependents} transitive dependent(s)`);
for (const [level, nodes] of Object.entries(fn.impact.levels)) {
console.log(` Level ${level}: ${nodes.map((n) => n.name).join(', ')}`);
}

// Call edges
if (fn.callees.length > 0) {
console.log(`\n Calls (${fn.callees.length}):`);
for (const c of fn.callees) {
console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
}
}
if (fn.callers.length > 0) {
console.log(`\n Called by (${fn.callers.length}):`);
for (const c of fn.callers) {
console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
}
}
if (fn.relatedTests.length > 0) {
console.log(`\n Tests (${fn.relatedTests.length}):`);
for (const t of fn.relatedTests) {
console.log(` ${t.file}`);
}
}

console.log();
}
}
25 changes: 0 additions & 25 deletions src/batch.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,6 @@ export function batchData(command, targets, customDbPath, opts = {}) {
return { command, total: targets.length, succeeded, failed, results };
}

/**
* CLI wrapper — calls batchData and prints JSON to stdout.
*/
export function batch(command, targets, customDbPath, opts = {}) {
const data = batchData(command, targets, customDbPath, opts);
console.log(JSON.stringify(data, null, 2));
}

/**
* Expand comma-separated positional args into individual entries.
* `['a,b', 'c']` → `['a', 'b', 'c']`.
Expand Down Expand Up @@ -161,20 +153,3 @@ export function multiBatchData(items, customDbPath, sharedOpts = {}) {

return { mode: 'multi', total: items.length, succeeded, failed, results };
}

/**
* CLI wrapper for batch-query — detects multi-command mode (objects with .command)
* or falls back to single-command batchData (default: 'where').
*/
export function batchQuery(targets, customDbPath, opts = {}) {
const { command: defaultCommand = 'where', ...rest } = opts;
const isMulti = targets.length > 0 && typeof targets[0] === 'object' && targets[0].command;

let data;
if (isMulti) {
data = multiBatchData(targets, customDbPath, rest);
} else {
data = batchData(defaultCommand, targets, customDbPath, rest);
}
console.log(JSON.stringify(data, null, 2));
}
2 changes: 1 addition & 1 deletion src/boundaries.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isTestFile } from './infrastructure/test-filter.js';
import { debug } from './logger.js';
import { isTestFile } from './test-filter.js';

// ─── Glob-to-Regex ───────────────────────────────────────────────────

Expand Down
97 changes: 1 addition & 96 deletions src/branch-compare.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ import os from 'node:os';
import path from 'node:path';
import Database from 'better-sqlite3';
import { buildGraph } from './builder.js';
import { isTestFile } from './infrastructure/test-filter.js';
import { kindIcon } from './queries.js';
import { outputResult } from './result-formatter.js';
import { isTestFile } from './test-filter.js';

// ─── Git Helpers ────────────────────────────────────────────────────────

Expand Down Expand Up @@ -477,97 +476,3 @@ export function branchCompareMermaid(data) {

return lines.join('\n');
}

// ─── Text Formatting ────────────────────────────────────────────────────

function formatText(data) {
if (data.error) return `Error: ${data.error}`;

const lines = [];
const shortBase = data.baseSha.slice(0, 7);
const shortTarget = data.targetSha.slice(0, 7);

lines.push(`branch-compare: ${data.baseRef}..${data.targetRef}`);
lines.push(` Base: ${data.baseRef} (${shortBase})`);
lines.push(` Target: ${data.targetRef} (${shortTarget})`);
lines.push(` Files changed: ${data.changedFiles.length}`);

if (data.added.length > 0) {
lines.push('');
lines.push(` + Added (${data.added.length} symbol${data.added.length !== 1 ? 's' : ''}):`);
for (const sym of data.added) {
lines.push(` [${kindIcon(sym.kind)}] ${sym.name} -- ${sym.file}:${sym.line}`);
}
}

if (data.removed.length > 0) {
lines.push('');
lines.push(
` - Removed (${data.removed.length} symbol${data.removed.length !== 1 ? 's' : ''}):`,
);
for (const sym of data.removed) {
lines.push(` [${kindIcon(sym.kind)}] ${sym.name} -- ${sym.file}:${sym.line}`);
if (sym.impact && sym.impact.length > 0) {
lines.push(
` ^ ${sym.impact.length} transitive caller${sym.impact.length !== 1 ? 's' : ''} affected`,
);
}
}
}

if (data.changed.length > 0) {
lines.push('');
lines.push(
` ~ Changed (${data.changed.length} symbol${data.changed.length !== 1 ? 's' : ''}):`,
);
for (const sym of data.changed) {
const parts = [];
if (sym.changes.lineCount !== 0) {
parts.push(`lines: ${sym.base.lineCount} -> ${sym.target.lineCount}`);
}
if (sym.changes.fanIn !== 0) {
parts.push(`fan_in: ${sym.base.fanIn} -> ${sym.target.fanIn}`);
}
if (sym.changes.fanOut !== 0) {
parts.push(`fan_out: ${sym.base.fanOut} -> ${sym.target.fanOut}`);
}
const detail = parts.length > 0 ? ` (${parts.join(', ')})` : '';
lines.push(
` [${kindIcon(sym.kind)}] ${sym.name} -- ${sym.file}:${sym.base.line}${detail}`,
);
if (sym.impact && sym.impact.length > 0) {
lines.push(
` ^ ${sym.impact.length} transitive caller${sym.impact.length !== 1 ? 's' : ''} affected`,
);
}
}
}

const s = data.summary;
lines.push('');
lines.push(
` Summary: +${s.added} added, -${s.removed} removed, ~${s.changed} changed` +
` -> ${s.totalImpacted} caller${s.totalImpacted !== 1 ? 's' : ''} impacted` +
(s.filesAffected > 0
? ` across ${s.filesAffected} file${s.filesAffected !== 1 ? 's' : ''}`
: ''),
);

return lines.join('\n');
}

// ─── CLI Display Function ───────────────────────────────────────────────

export async function branchCompare(baseRef, targetRef, opts = {}) {
const data = await branchCompareData(baseRef, targetRef, opts);

if (opts.format === 'json') opts = { ...opts, json: true };
if (outputResult(data, null, opts)) return;

if (opts.format === 'mermaid') {
console.log(branchCompareMermaid(data));
return;
}

console.log(formatText(data));
}
59 changes: 1 addition & 58 deletions src/cfg.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,10 @@ import {
import { walkWithVisitors } from './ast-analysis/visitor.js';
import { createCfgVisitor } from './ast-analysis/visitors/cfg-visitor.js';
import { openReadonlyOrFail } from './db.js';
import { isTestFile } from './infrastructure/test-filter.js';
import { info } from './logger.js';
import { paginateResult } from './paginate.js';

import { outputResult } from './result-formatter.js';
import { isTestFile } from './test-filter.js';

// Re-export for backward compatibility
export { CFG_RULES };
export { _makeCfgRules as makeCfgRules };
Expand Down Expand Up @@ -472,58 +470,3 @@ function edgeStyle(kind) {
if (kind === 'continue') return ', color=blue, style=dashed';
return '';
}

// ─── CLI Printer ────────────────────────────────────────────────────────

/**
* CLI display for cfg command.
*/
export function cfg(name, customDbPath, opts = {}) {
const data = cfgData(name, customDbPath, opts);

if (outputResult(data, 'results', opts)) return;

if (data.warning) {
console.log(`\u26A0 ${data.warning}`);
return;
}
if (data.results.length === 0) {
console.log(`No symbols matching "${name}".`);
return;
}

const format = opts.format || 'text';
if (format === 'dot') {
console.log(cfgToDOT(data));
return;
}
if (format === 'mermaid') {
console.log(cfgToMermaid(data));
return;
}

// Text format
for (const r of data.results) {
console.log(`\n${r.kind} ${r.name} (${r.file}:${r.line})`);
console.log('\u2500'.repeat(60));
console.log(` Blocks: ${r.summary.blockCount} Edges: ${r.summary.edgeCount}`);

if (r.blocks.length > 0) {
console.log('\n Blocks:');
for (const b of r.blocks) {
const loc = b.startLine
? ` L${b.startLine}${b.endLine && b.endLine !== b.startLine ? `-${b.endLine}` : ''}`
: '';
const label = b.label ? ` (${b.label})` : '';
console.log(` [${b.index}] ${b.type}${label}${loc}`);
}
}

if (r.edges.length > 0) {
console.log('\n Edges:');
for (const e of r.edges) {
console.log(` B${e.source} \u2192 B${e.target} [${e.kind}]`);
}
}
}
}
Loading
Loading