diff --git a/CHANGELOG.md b/CHANGELOG.md index 788a61d..be41274 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,11 +15,16 @@ All notable changes to DebtLens are documented here. This project adheres to - Opt-in `feature-flags` pack with `stale-feature-flag` detector. - `--concurrency` and `--cache-dir` scan controls for large-repo performance. - `--ownership-report` scorecards with count and payoff leaderboards. -- HTML reports now include import-graph SVG and directory debt treemap views. +- HTML reports now include import-graph SVG and directory debt treemap views + ([#264](https://github.com/ColumbusLabs/DebtLens/issues/264)). - Core rules: `long-parameter-list`, `god-file`, and `cognitive-complexity` for function/module - design smells beyond single-axis size checks. + design smells beyond single-axis size checks + ([#260](https://github.com/ColumbusLabs/DebtLens/issues/260), + [#261](https://github.com/ColumbusLabs/DebtLens/issues/261), + [#262](https://github.com/ColumbusLabs/DebtLens/issues/262)). - Config `budgets` block and `debtlens scan --budget-report` for per-area debt SLO gating. -- `debtlens scan --format badge` emits a self-contained SVG badge plus shields.io endpoint JSON. +- `debtlens scan --format badge` emits a self-contained SVG badge plus shields.io endpoint JSON + ([#265](https://github.com/ColumbusLabs/DebtLens/issues/265)). - Scan results now warn when matched files exceed `maxFiles`; terminal output prints the advisory and JSON reports include it in `summary.warnings` while keeping `summary.filesScanned` as the actual scanned count. diff --git a/README.md b/README.md index 996d38a..2344cda 100644 --- a/README.md +++ b/README.md @@ -799,25 +799,22 @@ jobs: run: echo "DebtLens total=${{ steps.debtlens.outputs.total-issues }} top=${{ steps.debtlens.outputs.top-rule }}" ``` -A Shields endpoint badge can be generated from the artifact by publishing a tiny JSON file derived from `summary.totalIssues`: +Generate a README status badge directly from a scan: -```json -{ - "schemaVersion": 1, - "label": "DebtLens", - "message": "12 issues", - "color": "orange" -} +```bash +debtlens scan . --format badge --output debtlens-badge.svg ``` -For example: +That writes a self-contained SVG and a sibling `debtlens-badge.json` shields.io endpoint +payload. Color thresholds are configurable via the `badge` config block (`greenMax` / +`yellowMax`). Publish the JSON from any static endpoint: -```bash -jq '{schemaVersion: 1, label: "DebtLens", message: (.summary.totalIssues|tostring + " issues"), color: (if .summary.totalIssues == 0 then "brightgreen" elif .summary.bySeverity.high > 0 then "red" else "orange" end)}' debtlens-report.json > debtlens-badge.json +```markdown +![DebtLens](https://img.shields.io/endpoint?url=https://example.com/debtlens-badge.json) ``` -For a stricter "0 new high debt" badge after `--baseline` or `--diff-base`, derive the -message from the remaining high-severity issues in the filtered report: +You can still derive a custom badge from a JSON report artifact with `jq` when you need a +stricter "0 new high debt" message after `--baseline` or `--diff-base`: ```bash jq '{ @@ -828,12 +825,6 @@ jq '{ }' debtlens-report.json > debtlens-high-badge.json ``` -Publish that JSON file from any static endpoint and use the Shields endpoint badge: - -```markdown -![DebtLens](https://img.shields.io/endpoint?url=https://example.com/debtlens-high-badge.json) -``` - Other CI templates: [GitLab](./docs/ci-gitlab.md), [Bitbucket](./docs/ci-bitbucket.md), and [Azure Pipelines](./docs/ci-azure.md). For local hooks, see [pre-commit hooks](./docs/pre-commit.md). For monorepo rollout, see [per-package baselines](./docs/monorepo-baselines.md). diff --git a/docs/report-gallery.md b/docs/report-gallery.md index f59f204..b915ada 100644 --- a/docs/report-gallery.md +++ b/docs/report-gallery.md @@ -10,10 +10,19 @@ reviewers already work. | Markdown | `debtlens scan examples/react --format markdown --output debtlens-report.md` | PR descriptions, release notes, and maintainer handoffs. | | PR comment | `debtlens scan examples/react --format pr-comment --output debtlens-pr-comment.md` | Compact GitHub review comments with prioritized fix targets and optional caps. | | SARIF | `debtlens scan examples/react --format sarif --sarif-category examples-react --output debtlens.sarif` | GitHub code scanning and compatible quality dashboards. | -| HTML | `debtlens scan examples/react --format html --output debtlens-report.html` | Shareable human-readable artifact. | +| HTML | `debtlens scan examples/react --format html --output debtlens-report.html` | Shareable human-readable artifact with findings, import-graph SVG, and debt treemap. | +| Badge | `debtlens scan examples/react --format badge --output debtlens-badge.svg` | README / status badge SVG; also writes shields.io endpoint JSON beside the SVG. | | JUnit | `debtlens scan examples/react --format junit --junit-fail-on high --output debtlens-junit.xml` | CI systems that surface test-style failures while keeping lower severities visible. | | GitLab Code Quality | `debtlens scan examples/react --format gitlab-codequality --output gl-code-quality-report.json` | Native GitLab Merge Request Code Quality widgets. | +HTML reports are self-contained (inline SVG, no CDN assets). When the scan builds an +import graph for TypeScript/JavaScript files, the HTML includes an import-graph view with +cycle edges highlighted, plus a directory debt treemap sized by finding density. + +Badge output writes a self-contained SVG. When `--output` ends in `.svg`, DebtLens also +writes a sibling `*.json` shields.io endpoint payload. Configure color thresholds with a +`badge` block in `debtlens.config.json` (`greenMax` / `yellowMax`). + The GitHub Action runs one canonical JSON scan and renders requested reports from that result so counts, filters, baselines, suppressions, and source links stay aligned. When `hotspots` is enabled, reports can include optional git-derived hotspot diff --git a/docs/rule-packs.md b/docs/rule-packs.md index e49efed..e21ffa1 100644 --- a/docs/rule-packs.md +++ b/docs/rule-packs.md @@ -42,6 +42,9 @@ For a user-facing selection table, see [`pack-chooser.md`](./pack-chooser.md). | `swallowed-error` | **core** | Catch blocks that only log without rethrowing or returning | Medium | | `floating-promise` | **core** | Unawaited promise-returning calls and effect fire-and-forget | Medium | | `commented-out-code` | **core** | Contiguous comment lines that look like dead code | Low | +| `long-parameter-list` | **core** | Functions with too many parameters or boolean flag parameters | Medium | +| `god-file` | **core** | Kitchen-sink modules that exceed multiple sprawl thresholds together | Medium | +| `cognitive-complexity` | **core** | Sonar-style cognitive complexity that penalizes nesting more than flat branching | Medium | | `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 | @@ -111,6 +114,9 @@ These apply to any TypeScript or JavaScript codebase: - **`swallowed-error`** — catch blocks that only log without rethrowing or returning a handled result. - **`floating-promise`** — promise-returning calls that are not awaited, returned, void-marked, or error-handled. - **`commented-out-code`** — contiguous comment blocks that look like dead code. +- **`long-parameter-list`** — functions with too many parameters or multiple boolean flags. +- **`god-file`** — kitchen-sink modules that cross several independent sprawl axes at once. +- **`cognitive-complexity`** — nested control-flow difficulty complementary to `complex-control-flow`. Future core rules may expand these signals into language-specific packs or richer project graph analysis. diff --git a/docs/rules.md b/docs/rules.md index a1827b7..16c6e06 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -1102,17 +1102,26 @@ When this is a false positive: ## `cognitive-complexity` -Scores functions with a Sonar-style cognitive complexity model that penalizes nesting more than flat branching. +Scores functions with a Sonar-style cognitive complexity model that penalizes nesting more than flat branching. Prefer this when readability/nesting is the concern; keep `complex-control-flow` when raw branch/cyclomatic pressure is the concern. Default thresholds: - `cognitive-complexity.max`: `15` +Scoring model (summary): + +- +1 for each control-flow structure (`if`, loops, `catch`, ternary, etc.) +- +1 additional per nesting level when that structure is nested +- sequences of boolean operators add incremental cost +- a flat `switch` does not accumulate nesting the way nested `if` chains do + When this is a false positive: - generated parsers or dispatch tables with intentionally flat `switch` blocks - compare with `complex-control-flow` when only cyclomatic count is high +Confidence scales with how far the score exceeds `cognitive-complexity.max`. + ## `python-error-handling` Flags Python `try/except` handlers that are empty (`pass` only), bare `except:`, or broad `except Exception:` blocks that only log without meaningful handling. diff --git a/src/cli/completions.ts b/src/cli/completions.ts index 1803d85..7232489 100644 --- a/src/cli/completions.ts +++ b/src/cli/completions.ts @@ -26,7 +26,7 @@ const baselineFlags = [ "--parallel", "--batch-size", ]; -const formats = ["terminal", "json", "markdown", "pr-comment", "sarif", "html", "junit", "gitlab-codequality"]; +const formats = ["terminal", "json", "markdown", "pr-comment", "sarif", "html", "junit", "gitlab-codequality", "badge"]; const baselineFormats = ["terminal", "json"]; const compareFormats = ["terminal", "markdown", "json"]; const scanFlags = [...SCAN_ARG_FLAGS, "--from-eslint", "--debounce"]; diff --git a/tests/cli/completions.test.ts b/tests/cli/completions.test.ts index a6052a6..d1b6012 100644 --- a/tests/cli/completions.test.ts +++ b/tests/cli/completions.test.ts @@ -27,6 +27,7 @@ describe("debtlens completions", () => { assert.match(result.stdout, /\$\{COMP_WORDS\[1\]\}" == "compare" && "\$prev" == "--format"/); assert.match(result.stdout, /terminal markdown json/); assert.match(result.stdout, /gitlab-codequality/); + assert.match(result.stdout, /badge/); assert.match(result.stdout, /--dry-run/); assert.match(result.stdout, /--audit-suppressions/); assert.match(result.stdout, /--hotspots/); @@ -78,7 +79,7 @@ describe("debtlens completions", () => { assert.match(fish.stdout, /-l codeowners/); assert.match(fish.stdout, /-l junit-fail-on -a "info low medium high"/); assert.match(fish.stdout, /-l gate -a "advisory new-code strict-new-code legacy-baseline"/); - assert.match(fish.stdout, /not __fish_seen_subcommand_from baseline compare" -l format -a "terminal json markdown pr-comment sarif html junit gitlab-codequality"/); + assert.match(fish.stdout, /not __fish_seen_subcommand_from baseline compare" -l format -a "terminal json markdown pr-comment sarif html junit gitlab-codequality badge"/); assert.match(fish.stdout, /__fish_seen_subcommand_from compare" -l format -a "terminal markdown json"/); assert.match(fish.stdout, /__fish_seen_subcommand_from baseline" -l format -a "terminal json"/); }); diff --git a/tests/docs/ciProviderDocs.test.ts b/tests/docs/ciProviderDocs.test.ts index b223116..9484861 100644 --- a/tests/docs/ciProviderDocs.test.ts +++ b/tests/docs/ciProviderDocs.test.ts @@ -46,6 +46,17 @@ describe("provider CI docs", () => { assert.match(reportGalleryDocs, /CODEOWNERS-based owner\s+summaries/); }); + it("documents HTML graph/treemap and badge report surfaces", () => { + assert.match(reportGalleryDocs, /--format html --output debtlens-report\.html/); + assert.match(reportGalleryDocs, /import-graph SVG/); + assert.match(reportGalleryDocs, /debt treemap/); + assert.match(reportGalleryDocs, /self-contained \(inline SVG, no CDN assets\)/); + assert.match(reportGalleryDocs, /--format badge --output debtlens-badge\.svg/); + assert.match(reportGalleryDocs, /shields\.io endpoint/); + assert.match(readme, /--format badge --output debtlens-badge\.svg/); + assert.match(readme, /debtlens-badge\.json/); + }); + it("documents GitLab Code Quality baseline and new-code gates", () => { assert.match(gitLabDocs, /--format gitlab-codequality --output gl-code-quality-report\.json/); assert.match(gitLabDocs, /reports:\n\s+codequality: gl-code-quality-report\.json/); diff --git a/tests/reporters/graphReporter.test.ts b/tests/reporters/graphReporter.test.ts index ea0e57d..332113e 100644 --- a/tests/reporters/graphReporter.test.ts +++ b/tests/reporters/graphReporter.test.ts @@ -19,6 +19,20 @@ describe("graph reporter", () => { assert.match(treemap, / { + const graph: ImportGraph = { + nodes: ["a.ts", "b.ts", "c.ts"], + edges: [ + { from: "a.ts", to: "b.ts", inCycle: true }, + { from: "b.ts", to: "c.ts", inCycle: false }, + ], + cycles: [["a.ts", "b.ts"]], + }; + const svg = renderImportGraphSvg(graph); + assert.match(svg, /stroke="#e05d44"/); + assert.match(svg, /stroke="#8c959f"/); + }); + it("escapes graph and treemap labels", () => { const graph: ImportGraph = { nodes: ["src/&file.ts", "b.ts"], diff --git a/tests/reporters/htmlReporter.test.ts b/tests/reporters/htmlReporter.test.ts index 914ac4b..bcc44ed 100644 --- a/tests/reporters/htmlReporter.test.ts +++ b/tests/reporters/htmlReporter.test.ts @@ -28,6 +28,39 @@ describe("html reporter", () => { assert.match(html, /Top payoff targets/); assert.match(html, /1 low-severity finding/); assert.match(html, /Debt Heatmap/); + assert.match(html, /Debt treemap/); + assert.match(html, /aria-label="Debt treemap"/); + assert.doesNotMatch(html, /https?:\/\//); + }); + + it("renders import graph with cycle edges when summary.importGraph is present", () => { + const result = makeResult([{ + id: "1", + fingerprint: "1", + ruleId: "import-cycle", + ruleName: "Import cycle", + severity: "medium", + confidence: 0.8, + message: "Cycle detected", + file: "a.ts", + location: { startLine: 1 }, + tags: [], + }]); + result.summary.importGraph = { + nodes: ["a.ts", "b.ts"], + edges: [ + { from: "a.ts", to: "b.ts", inCycle: true }, + { from: "b.ts", to: "a.ts", inCycle: true }, + ], + cycles: [["a.ts", "b.ts"]], + }; + + const html = renderHtml(result); + + assert.match(html, /Import graph/); + assert.match(html, /aria-label="Import graph"/); + assert.match(html, /stroke="#e05d44"/); + assert.doesNotMatch(html, /https?:\/\//); }); it("renders an empty state", () => {