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
2 changes: 2 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ DebtLens intentionally starts with JSON config rather than JavaScript config. St

## Future plugin model

See [`plugin-api-rfc.md`](./plugin-api-rfc.md) for the proposed third-party rule API (RFC — not implemented yet).

Potential design:

```ts
Expand Down
149 changes: 149 additions & 0 deletions docs/plugin-api-rfc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Plugin API RFC

Status: **Draft** — design only; no loader shipped yet. Target: v0.4 ([`ROADMAP.md`](../ROADMAP.md)).

## Problem

Teams want custom DebtLens rules without forking the CLI. Today all detectors are hardcoded in [`src/detectors/index.ts`](../src/detectors/index.ts) and config is JSON-only ([`SECURITY.md`](../SECURITY.md)).

## Goals

- Third-party rules use the **same `Detector` interface** as built-ins.
- Plugin loading is **explicit** in config — no implicit discovery.
- **Versioned** API surface with fail-fast mismatch errors.
- **Local ESM modules only** by default; no remote fetch at load time.

## Non-goals (v1 of plugins)

- npm package auto-discovery
- JavaScript/TypeScript config files that execute on load
- Sandboxed WASM rules

## Detector contract

Plugins export detectors matching the built-in interface ([`src/core/types.ts`](../src/core/types.ts)):

```ts
export interface Detector {
id: string;
name: string;
description: string;
defaultSeverity: Severity;
tags: string[];
detect: (context: DetectorContext) => Promise<DebtIssue[]> | DebtIssue[];
}
```

`DetectorContext` provides:

- `project` — ts-morph `Project`
- `files` — parsed `SourceFileInfo[]`
- `options` — merged `ScanOptions`
- `getThreshold(key, fallback)` — numeric threshold helper
- `addWarning(message)` — non-fatal scan warnings

Issues must include `message`, `severity`, `confidence`, `file`, `location`, `evidence`, and `suggestion` per [`CONTRIBUTING.md`](../CONTRIBUTING.md).

## Config shape (proposed)

```json
{
"$schema": "https://raw.githubusercontent.com/ColumbusLabs/DebtLens/main/schema/debtlens.config.schema.json",
"pluginApiVersion": 1,
"plugins": ["./debtlens-rules/custom-rule.mjs"],
"rules": ["duplicate-logic", "custom-boundary-rule"]
}
```

| Field | Meaning |
| --- | --- |
| `pluginApiVersion` | Must match the DebtLens runtime plugin API version |
| `plugins` | Array of paths to ESM modules, resolved relative to the config file directory |

Each plugin module default-exports either:

- a single `Detector`, or
- `{ rules: Detector[], vocabulary?: Record<string, string[]> }`

## Loading model

1. Parse JSON config (no `require()` of user JS).
2. If `plugins` present, for each path:
- Resolve to absolute path under config directory
- `import()` the module (Node ESM)
- Validate exported detectors (required fields, unique ids)
3. Merge plugin detectors with built-in registry; explicit `rules` array selects which run.
4. On `pluginApiVersion` mismatch → throw with clear upgrade message.

Built-in detectors always register first; plugin ids must not collide with built-in ids unless intentionally replacing (replacement deferred — collision is an error in v1).

## Versioning

- **`pluginApiVersion`** in config matches **`DEBTLENS_PLUGIN_API_VERSION`** in the CLI (integer, bumped on breaking detector-context changes).
- Patch/minor DebtLens releases keep the same plugin API version when `DetectorContext` is unchanged.
- Changelog documents API version bumps.

## Security

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).

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} */
export 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) {
if (file.content.includes("console.log")) {
issues.push({
id: `dl_nc_${file.relativePath}`,
ruleId: "no-console",
ruleName: "No console",
severity: "low",
confidence: 0.85,
message: "console.log found in source.",
file: file.relativePath,
location: { startLine: 1 },
tags: ["hygiene"],
suggestion: "Remove debug logging or route through a logger.",
});
}
}
return issues;
},
};

export default { rules: [noConsoleDetector] };
```

(Pseudocode — types would ship from a future `debtlens/plugin` entry point.)

## 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)

## Open questions

- Should plugins export threshold defaults?
- Allow vocabulary packs from plugins?
- Per-plugin enable flags vs flat `rules` list?

Track decisions in issue [#26](https://github.com/ColumbusLabs/DebtLens/issues/26).