DebtLens is a maintainability scanner for TypeScript and JavaScript codebases. The first supported rule pack targets React (including React Native, Expo, and Next.js apps), but the core idea applies broadly: catch duplicated logic, bloated modules, weak boundaries, TODO debt, and naming drift before it becomes permanent.
It is not an "AI code detector." It does not try to prove who wrote a line of code. Instead, it finds the patterns that tend to slip into codebases when teams move quickly with coding assistants — duplicated logic, bloated components, state sprawl, overloaded effects, thin abstractions, prop drilling, TODO debt, and naming drift.
See docs/rule-packs.md for how core rules, framework packs, and language-agnostic reporting fit together.
npx debtlens scan
npx debtlens scan src --format markdown
npx debtlens scan --min-severity medium --fail-on high
npx debtlens scan --rules duplicates,state,effects$ debtlens scan examples/react --min-severity medium
DebtLens Report
Scanned 3 files with 8 rules in 38ms.
Issues: 4 | high 2 | medium 2 | low 0 | info 0
HIGH (2)
Prop drilling [prop-drilling]
src/Dashboard.tsx:13
Dashboard forwards 7 props across 3 child components.
confidence 73%
- ReleaseHero: movie, userId, region, theme, onSelect, onSave
- ReleaseGrid: movie, userId, region, theme, onSelect, onSave, onShare
suggestion: Consider colocating the data owner closer to consumers, using a
composition slot, or extracting a focused context for stable cross-cutting values.
Duplicate logic [duplicate-logic]
src/duplicateOne.ts:1
normalizeMovieRelease is 100% structurally similar to normalizeGameRelease.
confidence 100%
- src/duplicateOne.ts:1-18 (18 lines)
- src/duplicateTwo.ts:1-18 (18 lines)
See docs/showcase-expensify-app.md for a curated run against a large production React Native codebase — one supported target, not the sole identity of the tool.
AI coding assistants make it easier to generate working code quickly. That creates a new maintainer problem: code review must catch duplicated implementations, architectural drift, unnecessary abstractions, and components that quietly absorb too many responsibilities.
That review burden is especially hard for new coders who have not yet built the instinct for what maintainability debt looks like. A beginner can ship something that works and still miss warning signs: repeated logic, overloaded effects, local state scattered everywhere, thin wrappers, or names that drift across a feature.
DebtLens gives maintainers and newer contributors a neutral, explainable report before debt becomes permanent. It is meant to teach what to look for, not just fail a build.
Built-in rules are grouped into a core pack (any TS/JS project) and a react pack
(components and hooks). Full taxonomy: docs/rule-packs.md.
| Rule | Pack | What it catches | Default severity |
|---|---|---|---|
duplicate-logic |
core | Near-duplicate functions/components using normalized AST/text similarity | Medium |
dead-abstraction |
core | Thin wrappers that add little behavior | Low |
todo-comment |
core | TODO/FIXME/HACK/temporary implementation comments | Low |
naming-drift |
core | Files with multiple competing names for the same domain concept | Info |
large-component |
react | React-style components with too many lines, hooks, or branch points | Medium |
state-sprawl |
react | Components/hooks with many local stateful hooks | Medium |
effect-complexity |
react | Long or overloaded React effect hooks | Medium |
prop-drilling |
react | Components that forward many props to children | Medium |
Synthetic fixtures under tests/benchmarks/fixtures/ exercise small (5 files), medium (30), and large (100) scan sizes.
npm run build
npm run benchmark # all fixtures + local budget check
npm run benchmark:ci # small fixture only (used in CI)Local budgets (generous; CI enforces small < 5000ms only):
| Fixture | Files | Budget |
|---|---|---|
| small | 5 | 5s |
| medium | 30 | 30s |
| large | 100 | 120s |
Per-rule timing is available via --profile in PR #62 once merged.
npm install --save-dev debtlensor run without installing:
npx debtlens scandebtlens init # write a starter debtlens.config.json (use --force to overwrite)
debtlens init --pack core # starter config using the core rule pack preset
debtlens adopt # adoption report (dry run; recommends minSeverity)
debtlens packs # list built-in rule pack presets
debtlens doctor # inspect resolved config and matched files without scanning
debtlens rules # list built-in rule ids and descriptions
debtlens explain <rule> # print rule docs, default thresholds, and false-positive guidance
debtlens suppress --rule <rule> --reason "<why>" # print a copy-paste inline suppression comment
debtlens scan [target]Options:
-i, --include <patterns> comma-separated glob patterns to include
-x, --exclude <patterns> comma-separated glob patterns to exclude
--min-severity <severity> info, low, medium, or high
--pack <pack> built-in rule pack preset
--rules <rules> comma-separated rule ids
--threshold <thresholds> comma-separated key=value threshold overrides
--max-files <count> maximum files to scan
--format <format> terminal, json, markdown, pr-comment, or sarif
-o, --output <path> write the report to a file
--fail-on <severity> exit 1 when an issue meets this severity
--fail-on-confidence <0-1> with --fail-on, require at least this confidence to fail
--baseline <path> report only issues absent from this baseline file
--diff-base <ref> report only findings introduced since this git ref
--write-baseline [path] write current issues to a baseline file and exit
--changed [ref] scan only files changed vs HEAD (or vs <ref> if given)
--staged scan only files staged in git
--respect-gitignore skip files ignored by git
--config <path> path to debtlens.config.json
--cwd <path> working directory
--package <name> scan a single npm workspace package (MVP: `packages/*` layouts)
--no-color disable terminal color
-q, --quiet terminal only: suppress per-finding detail
--profile print per-rule timing to stderr without changing findingsExamples:
# Scan the current project
debtlens scan
# Scan only app source files
debtlens scan . --include "app/**/*.ts,app/**/*.tsx,src/**/*.ts,src/**/*.tsx"
# Create a Markdown report for a pull request artifact
debtlens scan --format markdown --output debtlens-report.md
# Create a compact grouped PR comment body
debtlens scan --format pr-comment --output debtlens-pr-comment.md
# CI gate: allow low/medium debt but fail high-confidence high-severity debt
debtlens scan --min-severity medium --fail-on high --fail-on-confidence 0.8
# Tune component-size threshold
debtlens scan --threshold "large-component.maxLines=320,state-sprawl.maxStatefulHooks=8"
# Adopt on a legacy repo: record existing debt, then only report newly introduced debt
debtlens scan --write-baseline
debtlens scan --baseline debtlens-baseline.json --fail-on high
# Pull-request scan: only the files this branch changed vs main
debtlens scan --changed origin/main --fail-on high
# Pre-commit scan: only files currently staged in git
debtlens scan --staged --fail-on high
# Opt in to .gitignore filtering in addition to DebtLens exclude globs
debtlens scan --respect-gitignore
# Debug config and file matching without running detectors
debtlens doctor --pack core
debtlens doctor --include "src/**/*.ts,src/**/*.tsx" --changed
# List rule ids for config, CI, or --rules
debtlens rules
debtlens rules --format json
# Quiet terminal output: hide per-finding detail
debtlens scan --quietPreview findings and get a minSeverity recommendation before committing config or baseline files:
debtlens adopt --cwd . --rules todo-comment # dry-run report (default)
debtlens adopt --write-config --write-baseline --forceThe second command writes debtlens.config.json and debtlens-baseline.json (baseline write is skipped when zero issues are found). After adoption, use debtlens scan --baseline debtlens-baseline.json --fail-on high in CI to gate only newly introduced debt.
Baseline fingerprints are stable across line shifts, so moving existing code up or down does not resurface already-recorded debt — only genuinely new issues are reported.
When a scan reads zero files, DebtLens prints a stderr warning with likely causes such as include/exclude globs, the target path, --cwd, or an empty git file set from --changed / --staged. The warning is advisory and does not change the exit code for --fail-on.
When duplicate-logic reaches duplicate-logic.maxSnippets, DebtLens warns that duplicate comparisons were capped. JSON output includes the same advisory under summary.warnings.
Suppress intentional findings in source with an explicit, auditable reason. Suppressions apply during the scan; baseline and --diff-base filtering run afterward on the remaining issues.
Next-line — hides a finding on the line immediately below the comment:
// debtlens-disable-next-line todo-comment -- tracked in PROJ-123
// TODO: remove after migration shipsFile-level — hides all findings for that rule in the file:
// debtlens-disable-file naming-drift -- domain vocabulary is intentional hereRules:
- A non-empty reason is required after
--. Suppressions without a reason are ignored and emit a warning. - Unknown rule ids emit a warning and do not suppress.
- Only the matching rule (and line, for next-line) is suppressed; other rules on the same line still report.
Terminal output includes inline suppression counts in the filter stats line (for example, 1 inline suppressed). JSON reports expose the same count under summary.filterStats.suppressedByInline.
debtlens suppress prints a ready-to-paste directive so you don't have to remember the syntax:
debtlens suppress --rule todo-comment --reason "tracked in PROJ-123"
# // debtlens-disable-next-line todo-comment -- tracked in PROJ-123
debtlens suppress --rule naming-drift --reason "domain vocabulary is intentional" --file
# // debtlens-disable-file naming-drift -- domain vocabulary is intentionalPrefer baselines for legacy debt, config tuning for false positives, and inline suppressions for rare, documented exceptions. See docs/rules.md for guidance.
Create debtlens.config.json:
{
"$schema": "https://raw.githubusercontent.com/ColumbusLabs/DebtLens/main/schema/debtlens.config.schema.json",
"include": ["src/**/*.{ts,tsx,js,jsx}"],
"exclude": ["node_modules/**", "dist/**", "build/**", ".next/**"],
"minSeverity": "low",
"respectGitignore": false,
"rules": [
"large-component",
"state-sprawl",
"effect-complexity",
"duplicate-logic",
"dead-abstraction",
"prop-drilling",
"todo-comment",
"naming-drift"
],
"thresholds": {
"large-component.maxLines": 250,
"state-sprawl.maxStatefulHooks": 6,
"effect-complexity.maxLines": 30,
"duplicate-logic.minSimilarity": 0.86
},
"propDrilling": {
"ignoreComponents": ["DesignSystemCard", "DesignSystemModal"]
}
}The stable JSON Schema URL is https://raw.githubusercontent.com/ColumbusLabs/DebtLens/main/schema/debtlens.config.schema.json. debtlens init writes this URL into new config files so editors can provide validation and autocomplete.
Built-in presets select a rule set without hand-picking every rule id. See docs/rule-packs.md.
| Pack | Rules |
|---|---|
core |
duplicate-logic, dead-abstraction, todo-comment, naming-drift |
react |
core + large-component, state-sprawl, effect-complexity, prop-drilling |
react-native |
same as react |
next |
same as react |
{
"$schema": "https://raw.githubusercontent.com/ColumbusLabs/DebtLens/main/schema/debtlens.config.schema.json",
"pack": "core",
"include": ["src/**/*.{ts,tsx,js,jsx}"]
}Explicit rules in config override the pack. Use debtlens packs to list presets.
Tune noisy rules without disabling them. ruleSeverities replaces the severity a rule
reports (changing summary counts and --fail-on behavior), and ruleConfidenceFloors
hides findings from a rule below a minimum confidence:
{
"ruleSeverities": {
"naming-drift": "info"
},
"ruleConfidenceFloors": {
"prop-drilling": 0.8
}
}Unknown rule ids in either map emit a warning with a did-you-mean suggestion. Issues
hidden by a confidence floor are counted under summary.filterStats.filteredByConfidenceFloor.
Both maps accept plugin rule ids when plugins are configured.
naming-drift ships with a built-in media/release vocabulary. Add your own domain concepts with vocabulary (concept id → competing terms). Your groups are merged with the built-ins, and a group with the same id overrides the built-in one.
{
"vocabulary": {
"commerce-entity": ["product", "sku", "item", "listing"],
"auth-user": ["user", "account", "member", "profile"]
}
}Ship custom rules as local ESM modules without forking the CLI. List them in config with the plugin API version, then select them like built-in rules:
{
"$schema": "https://raw.githubusercontent.com/ColumbusLabs/DebtLens/main/schema/debtlens.config.schema.json",
"pluginApiVersion": 1,
"plugins": ["./debtlens-rules/no-console.mjs"],
"include": ["src/**/*.{ts,tsx,js,jsx}"]
}Plugin authors import types from the published debtlens/plugin entry point:
import type { Detector, DetectorContext } from "debtlens/plugin";Besides rules, a plugin's default export may include thresholds (defaults read by
context.getThreshold, merged after built-ins so user config and --threshold still
override them) and vocabulary (naming-drift concept groups, overridden by user config
groups with the same id):
export default {
rules: [noConsoleDetector],
thresholds: { "no-console.maxCalls": 0 },
vocabulary: { logging: ["log", "logger", "console", "debug", "trace"] },
};See the reference plugin in examples/plugin/ and the full
contract in docs/plugin-api-rfc.md. Plugin paths must stay
within the config file's directory tree, rule ids must not collide with built-ins, and
CI pipelines scanning untrusted repos can set DEBTLENS_DISABLE_PLUGINS=1 to skip
plugin loading entirely (see SECURITY.md).
Terminal output is designed for local development. JSON is designed for integrations. Markdown is designed for release notes and maintainer handoffs. pr-comment is compact Markdown grouped by file for GitHub pull request comments. SARIF (2.1.0) is designed for GitHub code scanning and other security/quality dashboards.
debtlens scan --format json
debtlens scan --format markdown --output reports/debtlens.md
debtlens scan --format pr-comment --output debtlens-pr-comment.md
debtlens scan --format sarif --output debtlens.sarifRun DebtLens on pull requests and surface findings as code-scanning annotations:
name: DebtLens
on: pull_request
permissions:
contents: read
security-events: write # required to upload SARIF
jobs:
debtlens:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # needed for --changed to diff against the base branch
- uses: ColumbusLabs/debtlens@v0
with:
changed: origin/${{ github.base_ref }}
format: sarif
output: debtlens.sarif
thresholds: large-component.maxLines=300
quiet: true
fail-on: high
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: debtlens.sarifInputs: target, min-severity, rules, fail-on, fail-on-confidence, format, output, changed, respect-gitignore, baseline, config, write-baseline, thresholds, max-files, working-directory, quiet, step-summary, comment. Each maps to the matching scan flag. write-baseline and baseline are mutually exclusive. With fail-on, a qualifying issue fails the job (gating the merge); if: always() still uploads the SARIF so annotations appear even on a failing run.
Set step-summary: true to append a compact Markdown rollup to the job's GitHub Actions step summary (useful alongside SARIF or terminal output):
- uses: ColumbusLabs/debtlens@v0
with:
changed: origin/${{ github.base_ref }}
format: sarif
output: debtlens.sarif
step-summary: true
quiet: true
fail-on: highSet comment: true to upsert a stable pull request comment (requires pull-requests: write):
permissions:
contents: read
pull-requests: write
- uses: ColumbusLabs/debtlens@v0
with:
changed: origin/${{ github.base_ref }}
comment: true
fail-on: highTo post a grouped PR comment manually instead, write the pr-comment output and post it with actions/github-script:
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: ColumbusLabs/debtlens@v0
with:
changed: origin/${{ github.base_ref }}
format: pr-comment
output: debtlens-pr-comment.md
fail-on: high
- uses: actions/github-script@v7
if: always() && github.event_name == 'pull_request'
with:
script: |
const fs = require('node:fs');
const body = fs.readFileSync('debtlens-pr-comment.md', 'utf8');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});Want to help make DebtLens better? Start with the
first-PR guide, the
rule pack taxonomy, and
CONTRIBUTING.md. The v0.3 contributor roadmap batch is complete;
see docs/good-first-issues.md for a historical index of
shipped tasks. Propose new work in
Discussions, via the rule request
template, or the plugin API RFC.
Contribution paths: core TS/JS rules, React pack rules, framework packs (Next.js, RN, Node), scanner/CI (baselines, monorepos, inline suppressions), and reporters.
npm install
npm run typecheck
npm test # node:test suite (run via tsx)
npm run test:all # typecheck + tests
npm run build
npm run dev
node dist/cli/index.js scan examples/react --min-severity infoDebtLens is in the v0.3 release line. Recent capabilities include debtlens adopt
and debtlens doctor, rule packs, inline suppressions with required reasons,
confidence-aware --fail-on, monorepo --package scanning, GitHub Action step summaries
and PR comment upsert, and --diff-base branch comparisons.
The architecture stays intentionally simple: a language-agnostic scan and reporting
layer with pluggable rule packs on top. React is the first serious pack; React Native,
Next.js, and broader TS/JS rules expand from there. See ROADMAP.md and
docs/rule-packs.md.
MIT