Architecture
Flow charts for contributors: how a command moves through the codebase, from bin/devcompass.js down to the shared engine.
System Overview
How a run of the CLI moves through the codebase, from the executable to the shared engine.
bin/devcompass.js
entry point
Registers every command. Run with no arguments at all, it skips commander and calls runAnalyze() directly.
CLI commands
src/cli/commands/*.cmd.js
commander.js wiring only — lazily requires the matching feature inside .action(), never at file top.
Feature modules
src/features/<name>/
One directory per feature: analyze, fix, cve, history, graph, ai, config, backup, clean, and more.
core/
the engine
Feature-agnostic: the Issue model, health scoring, ranking, risk classification. Doesn't know about CLI commands.
shared/
the infrastructure
Logging, error handling, caching, backups, encryption, the npm registry client, process lifecycle.
A .cmd.js file only ever imports its own feature's entrypoint — never another feature's internals directly. Where state actually lives on disk (four global SQLite databases plus per-project cache/backup files) is covered on the Configuration page.
The analyze Pipeline
Every other command either produces input for this pipeline or reads its output — it's the center of the codebase.
shared/utils/file-cache.js
Load package.json
An mtime-cached read, so nothing in one run re-reads the same file from disk twice.
features/analyze/collectors/*.js — Promise.allSettled
Run collectors concurrently
A failed collector degrades to an empty result instead of failing the whole run. Ecosystem and Predictive call GitHub's API and are skipped in silent/CI mode to keep those paths fast.
core/services/issue-collector.js — IssueCollector.getAll()
Merge & normalize into Issue[]
Dedupes findings per package (mergeByPackage — e.g. when both npm audit and a CVE lookup flag the same package) and maps everything to the canonical Issue model.
core/services/health-calculator.js — HealthCalculator.calculate()
Score the project
Starts at 10.0, subtracts severityPenalty × typePenalty per issue (CRITICAL 2.0 … LOW 0.5, security 1.2 … unused 0.3), clamps to [0, 10].
shared/utils/analysis-cache.js + renderers/
Cache & render
24h-TTL cache, versioned against the installed devcompass version. Renders default, --deep, or --json output.
core/services/snapshot-manager.js → features/history/
Save a snapshot
Unless --no-history or silent — persisted to ~/.devcompass/history.db for later history, compare, and timeline commands.
--ci / --ci-threshold (default 7.0)
CI gate (optional)
Compares healthScore against the threshold and process.exit(0 | 1) — this is what a CI pipeline checks.
* skipped when running silent or with --ci
CLI Command Pattern
Every one of the 13 registered commands follows the same dispatch shape.
You run a command
$ devcompass history stats
commander matches it
program.command('history <subcommand>')
.action() fires
src/cli/commands/history.cmd.js
Feature required lazily
require('../../features/history/history.command')
$ // src/cli/commands/<name>.cmd.js$ module.exports = function registerXCommand(program) {$ program$ .command('x <subcommand>')$ .option('--limit <number>', '...', parseIntOption, 30)$ .action(async (subcommand, options) => {$ // lazy require — keeps command registration cheap$ const xCommand = require('../../features/x/x.command');$ await xCommand({ ...options, _: ['x', subcommand] });$ });$ };Commander numeric-option gotcha
commander calls an option parser as parseArg(value, previousValue). Passing the bare parseInt breaks, because previousValue becomes the radix argument. Every command with a numeric flag defines a local wrapper instead — const parseIntOption = (value) => parseInt(value, 10) — see history.cmd.js and graph.cmd.js.
Dashboard Generation
graph and timeline don't run a server — they template out one self-contained HTML file.
index.html template
src/dashboard/index.html
features/graph/graph.exporter.js reads it as a string, not a rendered page.
Inject data
{{GRAPH_DATA}}
Replaced with window.graphData = {...} — nodes, links, and metadata as one JSON blob.
Inline clustering
{{CLUSTERING_CODE}}
graph.clustering.js's contents, module.exports stripped, dropped straight into a <script> tag.
Inline every asset
inlineAllAssets()
Every styles/*.css and scripts/*.js file inlined too — one output file, no loose parts.
The result is a single static HTML file with no external dependency beyond the D3 CDN script tag. If the template file is ever missing, generateFallbackHTML() renders a minimal D3 force-graph instead of failing outright.
Client-side script load order — src/dashboard/scripts/
layouts.js holds the 5 renderers (tree / force / radial / conflict / analytics) — it's almost always the right file for changing how the graph looks. core.js is the bootstrap: validates window.graphData, wires everything up, and defaults to the tree layout.