Menu

Architecture

Flow charts for contributors: how a command moves through the codebase, from bin/devcompass.js down to the shared engine.

On this page

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.

CVELicenseQualitySecurityOutdatedUnusedEcosystem*Predictive*

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/

utils.jstooltip.jsstats.jscontrols.jslayouts.jscore.js

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.