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
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,35 @@
All notable changes to DebtLens are documented here. This project adheres to
[Semantic Versioning](https://semver.org/).

## [Unreleased]

### Added

- **`debtlens explain <rule>`** command printing rule docs, default thresholds, and
false-positive guidance from `docs/rules.md` ([#145](https://github.com/ColumbusLabs/DebtLens/issues/145)).
- **Did-you-mean suggestions** for unknown rule ids in `--rules`, config `rules`, inline
suppression directives, and `debtlens explain` ([#151](https://github.com/ColumbusLabs/DebtLens/issues/151)).
- **`failOn` config field** to set the CI exit-code severity policy in
`debtlens.config.json`; the `--fail-on` CLI flag overrides it
([#106](https://github.com/ColumbusLabs/DebtLens/issues/106)).
- **`pluginApiVersion` and `plugins` config fields** with fail-fast runtime validation
against the supported plugin API version
([#69](https://github.com/ColumbusLabs/DebtLens/issues/69)). The plugin API version is
an integer bumped only on breaking `Detector`/`DetectorContext` changes; bumps are
documented here and in `docs/plugin-api-rfc.md`.
- **Plugin loader** for local ESM rule plugins per the plugin API RFC: detectors are
validated against the built-in `Detector` contract, rule id collisions fail fast, and
paths cannot escape the config directory
([#68](https://github.com/ColumbusLabs/DebtLens/issues/68)).
- **`DEBTLENS_DISABLE_PLUGINS=1`** environment escape hatch for CI pipelines scanning
untrusted repositories; built-in rules still run
([#71](https://github.com/ColumbusLabs/DebtLens/issues/71)).
- **Reference plugin** in `examples/plugin/` (no-console rule) with CI integration
coverage ([#72](https://github.com/ColumbusLabs/DebtLens/issues/72)).
- **`debtlens/plugin` entry point** exporting `Detector`, `DetectorContext`, `DebtIssue`,
`Severity`, and `DEBTLENS_PLUGIN_API_VERSION` for plugin authors
([#70](https://github.com/ColumbusLabs/DebtLens/issues/70)).

## [0.3.0] - 2026-06-09

### Added
Expand Down
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ 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 scan [target]
```

Expand Down Expand Up @@ -305,6 +306,32 @@ Explicit `rules` in config override the pack. Use `debtlens packs` to list prese
}
```

### Plugins

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:

```json
{
"$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:

```ts
import type { Detector, DetectorContext } from "debtlens/plugin";
```

See the reference plugin in [`examples/plugin/`](./examples/plugin/) and the full
contract in [`docs/plugin-api-rfc.md`](./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`](./SECURITY.md)).

## Output formats

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.
Expand Down
12 changes: 12 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,15 @@ Security goals:
- Avoid executing scanned code.
- Avoid loading arbitrary project config as executable JavaScript.
- Prefer JSON config until a safe plugin model exists.

## Plugins

Config-listed plugins (`plugins` in `debtlens.config.json`) are local ESM modules that
execute with the CLI's privileges. Treat them like any other code in the repository:
only enable them in trusted pipelines. Plugin paths must stay within the config file's
directory tree, and no code is loaded from config values other than the explicit
`plugins` list (see [`docs/plugin-api-rfc.md`](./docs/plugin-api-rfc.md)).

CI environments scanning untrusted repositories can set `DEBTLENS_DISABLE_PLUGINS=1`
to skip plugin loading entirely; built-in rules still run and a single note is written
to stderr when configured plugins are skipped.
154 changes: 154 additions & 0 deletions docs/next-phase-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Next Phase Plan: Plugin API + CLI Quick Wins

Status: **Implemented** — all eight issues landed on this branch as five sequential
commits matching the PR breakdown below.
Date: 2026-06-10

## Context

v0.3 ("Maintainer workflow integrations") is shipped. Per [`ROADMAP.md`](../ROADMAP.md),
the headline item for v0.4 is the **plugin API for third-party rules**, which already has
an accepted design in [`docs/plugin-api-rfc.md`](./plugin-api-rfc.md) but no implementation:
`plugins` and `pluginApiVersion` do not exist yet in the config schema, and all detectors
are hardcoded in [`src/detectors/index.ts`](../src/detectors/index.ts).

This plan selects **eight open issues** in two tracks: the plugin API vertical (five
issues that together ship the RFC end to end) and three small, independent CLI/config
quick wins that improve day-to-day DX while the larger work lands.

## Selected issues

### Track A — Plugin API (roadmap v0.4 centerpiece)

| Order | Issue | Title | Difficulty |
| --- | --- | --- | --- |
| A1 | [#69](https://github.com/ColumbusLabs/DebtLens/issues/69) | Add `pluginApiVersion` to config schema and runtime validation | Small |
| A2 | [#68](https://github.com/ColumbusLabs/DebtLens/issues/68) | Implement plugin loader prototype | Large |
| A3 | [#71](https://github.com/ColumbusLabs/DebtLens/issues/71) | Add `DEBTLENS_DISABLE_PLUGINS` CI escape hatch | Small |
| A4 | [#72](https://github.com/ColumbusLabs/DebtLens/issues/72) | Add example plugin repo fixture and integration test | Medium |
| A5 | [#70](https://github.com/ColumbusLabs/DebtLens/issues/70) | Export `debtlens/plugin` TypeScript types entry point | Medium |

**Why this cluster:** these five issues are mutually dependent slices of one feature, all
labeled `type: rfc`, and the RFC resolves every design question in advance (config shape,
loading model, versioning, security constraints). Shipping them together moves the roadmap's
single biggest v0.4 commitment from "designed" to "done" and unblocks the follow-on plugin
issues (#73 plugin thresholds, #74 plugin vocabulary, #165 policy packs).

### Track B — CLI/config quick wins (independent, low risk)

| Order | Issue | Title | Difficulty |
| --- | --- | --- | --- |
| B1 | [#151](https://github.com/ColumbusLabs/DebtLens/issues/151) | Suggest did-you-mean for unknown rule ids | Small |
| B2 | [#145](https://github.com/ColumbusLabs/DebtLens/issues/145) | Add `debtlens explain` command for rule documentation | Small |
| B3 | [#106](https://github.com/ColumbusLabs/DebtLens/issues/106) | Add `failOn` severity to config file | Small |

**Why these:** all three are `good first issue`-class, touch isolated code paths, and
two of them (#151, #145) become more valuable once plugins exist — did-you-mean and
`explain` should operate over the merged (built-in + plugin) registry, so building them
in the same phase keeps the registry abstraction honest.

**Deliberately deferred:** performance work (#138–#142) until the plugin loader settles the
detector registry shape; Python pack (#92–#96) since it depends on the language-pack
interface spike; new core rules (#85–#91) which are independent and parallelizable by
other contributors.

## Execution plan

### A1 — `pluginApiVersion` (#69)

- Add `pluginApiVersion` (integer) and `plugins` (string array) to the JSON schema in
[`src/config/schema.ts`](../src/config/schema.ts) and to `DebtLensConfig` in
[`src/config/loadConfig.ts`](../src/config/loadConfig.ts).
- Export `DEBTLENS_PLUGIN_API_VERSION = 1` constant from a new `src/plugins/version.ts`.
- Validate at config load: mismatch throws with an upgrade message naming both versions.
- Update the schema drift test (`tests/config/schema.test.ts`) and CHANGELOG with the bump policy.

### A2 — Plugin loader (#68)

- New `src/plugins/loadPlugins.ts`:
- Resolve each `plugins[]` path relative to the config file directory; reject paths
escaping the repo root (per RFC security section).
- Dynamic `import()` of ESM modules; accept default export of a single `Detector` or
`{ rules: Detector[], vocabulary? }`.
- Validate detector shape (id, name, description, defaultSeverity, tags, detect) and
error on id collisions with built-ins or other plugins.
- Merge plugin detectors into the registry consumed by `scan()` in
[`src/core/scan.ts`](../src/core/scan.ts); explicit `rules` selection works unchanged.
- Tests: happy path, invalid export shape, id collision, path traversal rejection
(`tests/core/scan.test.ts`, new `tests/plugins/loadPlugins.test.ts`).

### A3 — `DEBTLENS_DISABLE_PLUGINS` (#71)

- In the loader entry point: when env var is `1`, skip loading and emit one stderr note
if `plugins` is configured. Built-in rules unaffected.
- Document in `SECURITY.md` and the RFC (the RFC already reserves this flag).
- CLI test asserting scan succeeds with plugins configured but disabled.

### A4 — Example plugin + integration test (#72)

- Add `examples/plugin/no-console.mjs` (the RFC's minimal example) plus a sample
`debtlens.config.json` enabling it.
- Integration test scans a small fixture and asserts the plugin finding appears in JSON
output; wire into the default `npm test` run.
- Link the example from the RFC and README.

### A5 — `debtlens/plugin` types entry (#70)

- Add a `./plugin` subpath export in `package.json` exposing `Detector`,
`DetectorContext`, `DebtIssue`, and `Severity` from [`src/core/types.ts`](../src/core/types.ts).
- Convert the example plugin (or add a sibling) to a type-checked `.ts` variant that
imports only from the published entry; verify with `npm run typecheck`.
- Document in the RFC and README.

### B1 — Did-you-mean for rule ids (#151)

- Small Levenshtein helper in `src/utils/` (no new dependency).
- Apply where `--rules`, config `rules`, and suppression directives reject unknown ids
(CLI parse in [`src/cli/index.ts`](../src/cli/index.ts),
[`src/core/suppressions.ts`](../src/core/suppressions.ts)).
- Match against the merged registry ids so plugin rules are suggested too.
- Acceptance: `todo-comments` suggests `todo-comment` (`tests/cli/`).

### B2 — `debtlens explain` (#145)

- New `explain <rule-id>` command in [`src/cli/index.ts`](../src/cli/index.ts) rendering
the matching section of [`docs/rules.md`](./rules.md) plus default severity, tags, and
thresholds from the detector registry.
- Unknown rule id exits non-zero and reuses the B1 did-you-mean helper.
- Tests in `tests/cli/`.

### B3 — `failOn` in config (#106)

- Add `failOn` to the config schema and `mergeConfig` precedence
([`src/config/mergeConfig.ts`](../src/config/mergeConfig.ts)): CLI `--fail-on` overrides
config value, matching the existing `failOnConfidence` pattern.
- Tests: config-only `failOn` gates exit code (`tests/cli/scan.test.ts`); schema drift
test updated (`tests/config/schema.test.ts`).

## Sequencing and PR breakdown

```
PR 1: B1 + B2 (did-you-mean helper, explain command — explain reuses the helper)
PR 2: B3 (failOn config — isolated)
PR 3: A1 (schema + version constant — small, reviewable alone)
PR 4: A2 + A3 (loader + escape hatch — the escape hatch is part of the loader's entry)
PR 5: A4 + A5 (example plugin + types entry — the typed example validates the entry point)
```

Tracks A and B are independent; PRs 1–3 can land in any order. PR 4 depends on PR 3,
and PR 5 depends on PR 4.

## Validation

- `npm test` (full suite) on every PR; targeted suites per issue's stated test command.
- `npm run typecheck` for PR 5.
- Schema drift tests guard config changes in PRs 2–3.
- The calibration fixtures (`tests/fixtures/quality/`) guard against detector behavior
drift — no detector logic changes in this phase, so counts must stay identical.

## Definition of done

- All eight issues closed with tests matching their stated acceptance criteria.
- Plugin RFC status updated from Draft to Shipped, with follow-ons (#73, #74, #165) noted.
- CHANGELOG entries for the plugin API (with `pluginApiVersion` bump policy), `explain`,
did-you-mean, and config `failOn`.
18 changes: 9 additions & 9 deletions docs/plugin-api-rfc.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Plugin API RFC

Status: **Draft** — design only; no loader shipped yet. Target: v0.4 ([`ROADMAP.md`](../ROADMAP.md)).
Status: **Shipped (v1)** — the loader, `pluginApiVersion` validation, and the `DEBTLENS_DISABLE_PLUGINS` escape hatch are implemented. Follow-ons: plugin threshold defaults ([#73](https://github.com/ColumbusLabs/DebtLens/issues/73)) and vocabulary merging ([#74](https://github.com/ColumbusLabs/DebtLens/issues/74)).

## Problem

Expand Down Expand Up @@ -90,15 +90,15 @@ Align with [`SECURITY.md`](../SECURITY.md):
- **No network** during plugin load.
- **No arbitrary code** from config values — only explicit `plugins` paths.
- Paths must stay within the repository (reject `..` traversal outside repo root).
- CI environments may set `DEBTLENS_DISABLE_PLUGINS=1` to skip loading (future flag).
- CI environments may set `DEBTLENS_DISABLE_PLUGINS=1` to skip loading entirely; built-in rules still run and a single stderr note is emitted when configured plugins are skipped.

Untrusted repos: treat plugins like any local code — only enable in trusted pipelines.

## Minimal example plugin

```js
// debtlens-rules/no-console.mjs
/** @type {import("debtlens").Detector} */
/** @type {import("debtlens/plugin").Detector} */
export const noConsoleDetector = {
id: "no-console",
name: "No console",
Expand Down Expand Up @@ -130,15 +130,15 @@ export const noConsoleDetector = {
export default { rules: [noConsoleDetector] };
```

(Pseudocode — types would ship from a future `debtlens/plugin` entry point.)
A runnable version of this plugin lives in [`examples/plugin/`](../examples/plugin/), and types ship from the published `debtlens/plugin` entry point (`import type { Detector } from "debtlens/plugin"`).

## Implementation phases

1. **RFC merged** (this document) — no runtime change
2. **Loader prototype** — `import()` + validation behind feature flag
3. **Schema + docs** — `plugins` / `pluginApiVersion` in JSON schema
4. **Example repo** — sample plugin + integration test
5. **Stable export** — document supported API in [`docs/architecture.md`](./architecture.md)
1. **RFC merged** (this document) — no runtime change
2. **Loader prototype** — `import()` + validation ✅ ([#68](https://github.com/ColumbusLabs/DebtLens/issues/68))
3. **Schema + docs** — `plugins` / `pluginApiVersion` in JSON schema ✅ ([#69](https://github.com/ColumbusLabs/DebtLens/issues/69))
4. **Example plugin** — [`examples/plugin/`](../examples/plugin/) + integration test ✅ ([#72](https://github.com/ColumbusLabs/DebtLens/issues/72))
5. **Stable export** — `debtlens/plugin` types entry point ✅ ([#70](https://github.com/ColumbusLabs/DebtLens/issues/70))

## Open questions

Expand Down
6 changes: 6 additions & 0 deletions examples/plugin/debtlens.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/ColumbusLabs/DebtLens/main/schema/debtlens.config.schema.json",
"pluginApiVersion": 1,
"plugins": ["./no-console.mjs"],
"include": ["src/**/*.{ts,tsx,js,jsx}"]
}
37 changes: 37 additions & 0 deletions examples/plugin/no-console.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Reference DebtLens plugin: flags console.log calls in production source.
// Run it from this directory: npx debtlens scan src --rules no-console
// See docs/plugin-api-rfc.md for the plugin contract.

/** @type {import("debtlens/plugin").Detector} */
const noConsoleDetector = {
id: "no-console",
name: "No console",
description: "Flags console.log in production source.",
defaultSeverity: "low",
tags: ["hygiene"],
detect(context) {
const issues = [];
for (const file of context.files) {
const lines = file.content.split(/\r?\n/);
for (let index = 0; index < lines.length; index += 1) {
if (!lines[index].includes("console.log")) continue;
issues.push({
id: `dl_nc_${file.relativePath}:${index + 1}`,
ruleId: "no-console",
ruleName: "No console",
severity: "low",
confidence: 0.85,
message: "console.log found in source.",
file: file.relativePath,
location: { startLine: index + 1 },
evidence: [lines[index].trim()],
suggestion: "Remove debug logging or route through a logger.",
tags: ["hygiene"],
});
}
}
return issues;
},
};

export default { rules: [noConsoleDetector] };
7 changes: 7 additions & 0 deletions examples/plugin/src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function startApp(): void {
console.log("booting app");
}

export function shutdownApp(): void {
// Intentionally quiet: no debug logging here.
}
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@
"bin": {
"debtlens": "dist/cli/index.js"
},
"exports": {
"./plugin": {
"types": "./dist/plugin.d.ts",
"import": "./dist/plugin.js"
},
"./package.json": "./package.json"
},
"files": [
"dist",
"schema",
Expand Down
Loading