tech-debt-visualizer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,26 @@
1
+ Technical Debt Visualizer
2
+ Copyright (c) 2025. All rights reserved.
3
+
4
+ This program is free software: you can redistribute it and/or modify it under
5
+ the terms of the GNU General Public License as published by the Free Software
6
+ Foundation, either version 3 of the License, or (at your option) any later
7
+ version.
8
+
9
+ This program is distributed in the hope that it will be useful, but WITHOUT ANY
10
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
11
+ PARTICULAR PURPOSE. See the GNU General Public License for more details.
12
+
13
+ You should have received a copy of the GNU General Public License along with
14
+ this program. If not, see <https://www.gnu.org/licenses/>.
15
+
16
+ ---
17
+
18
+ DUAL LICENSING
19
+
20
+ This project is offered under the GNU GPL v3 (above) for open-source use. If you
21
+ need to use this software in a proprietary or commercial context without the
22
+ obligations of the GPL (e.g., without disclosing source code or licensing
23
+ derivative works under the GPL), a separate commercial license is available
24
+ from the copyright holder. Contact the copyright holder for terms.
25
+
26
+ Full text of GPL-3.0: https://www.gnu.org/licenses/gpl-3.0.html
package/README.md ADDED
@@ -0,0 +1,166 @@
1
+ # Technical Debt Visualizer
2
+
3
+ Analyze a repo and get a **cleanliness score**, **debt breakdown**, and optional **AI explanations and refactor suggestions**—in the terminal or as an interactive HTML report.
4
+
5
+ ![Node 18+](https://img.shields.io/badge/node-%3E%3D18-brightgreen?style=flat-square)
6
+ ![Languages](https://img.shields.io/badge/languages-JS%20%7C%20TS%20%7C%20Python-green?style=flat-square)
7
+ ![License](https://img.shields.io/badge/license-GPL--3.0-blue?style=flat-square)
8
+
9
+ ---
10
+
11
+ ## Quick start
12
+
13
+ ```bash
14
+ git clone <this-repo>
15
+ cd tech-debt-visualizer
16
+ npm install && npm run build
17
+ node dist/cli.js analyze . --format cli
18
+ ```
19
+
20
+ To try the **HTML dashboard**:
21
+ `node dist/cli.js analyze . --format html -o report.html` then open `report.html`.
22
+
23
+ To use **AI insights** (explanations + optional code refactors): set `GEMINI_API_KEY` or `OPENAI_API_KEY` and run the same commands without `--no-llm`.
24
+
25
+ ---
26
+
27
+ ## How it works
28
+
29
+ The tool runs a fixed pipeline so you know exactly what you’re getting.
30
+
31
+ 1. **Discover files**
32
+ Walks the repo (respecting common ignores like `node_modules`, `.git`, `dist`) and collects source files by extension: `.js`, `.ts`, `.jsx`, `.tsx`, `.py`, etc.
33
+
34
+ 2. **Run language analyzers**
35
+ Pluggable analyzers (today: JavaScript/TypeScript and Python) parse each file with **tree-sitter**, then:
36
+ - Count **cyclomatic complexity** (if/else, loops, switch, ternaries, `&&`/`||`).
37
+ - Count effective lines and check for **module-level docs** (JSDoc, docstrings).
38
+ - Emit **debt items** (e.g. “High cyclomatic complexity”, “Missing documentation”, “Large file”) with severity and confidence.
39
+
40
+ 3. **Enrich with git**
41
+ Uses `git log` (e.g. last 90 days) to compute per-file **churn** and **commit count**. Combines that with complexity into a **hotspot score**: files that change often and are complex are treated as higher risk. A simple **debt trend** over recent commits is also derived (heuristic, not full historical analysis).
42
+
43
+ 4. **Score and tier**
44
+ A single **debt score** (0–100) is computed from severity and confidence of all debt items. That score is mapped to a **Cleanliness tier** (1–5), e.g. “Thoughtful Prompter (3/5)” or “Pure Coder (5/5)”, shown at the top of the CLI and report.
45
+
46
+ 5. **Optional LLM pass**
47
+ If an API key is set and you don’t use `--no-llm`, the tool:
48
+ - Asks the LLM to assess **per-file cleanliness** for the top ~15 hotspot files (and optionally suggest a **concrete code refactor** in a code block).
49
+ - Asks the LLM to explain **each debt item** (why it matters, what to do) and optionally suggest a **simplified/refactored code snippet**.
50
+ - Asks the LLM for one **overall codebase assessment** (a short paragraph).
51
+
52
+ Responses are parsed: prose goes into insights/assessments; any markdown code block is stored as **suggested refactor** and shown in CLI and HTML.
53
+
54
+ 6. **Output**
55
+ Results are printed in the terminal (CLI) and/or written as **HTML** (treemap, trend chart, drill-down), **JSON**, or **Markdown** for CI or tooling.
56
+
57
+ So: **static metrics + git → score & tier → optional LLM explanations and code suggestions → your chosen output format.**
58
+
59
+ ---
60
+
61
+ ## Install & run
62
+
63
+ | How you run it | Command |
64
+ |----------------|--------|
65
+ | **From this repo** | `node dist/cli.js analyze [path]` (after `npm run build`) |
66
+ | **Global (after publish)** | `npm install -g tech-debt-visualizer` then `tech-debt analyze [path]` |
67
+ | **No install (after publish)** | `npx tech-debt-visualizer analyze [path]` |
68
+
69
+ Requires **Node 18+**.
70
+
71
+ ---
72
+
73
+ ## Options
74
+
75
+ | Option | Meaning |
76
+ |--------|--------|
77
+ | `-f, --format` | `cli` (default), `html`, `json`, or `markdown` |
78
+ | `-o, --output` | Output path (e.g. `report.html` for HTML) |
79
+ | `--no-llm` | Skip all LLM calls (no API key needed) |
80
+ | `--ci` | Terse output; exit code 1 if debt score &gt; 60 |
81
+
82
+ Examples:
83
+
84
+ ```bash
85
+ node dist/cli.js analyze . -f html -o report.html
86
+ node dist/cli.js analyze ./src -f json -o debt.json
87
+ node dist/cli.js analyze . --ci
88
+ ```
89
+
90
+ ---
91
+
92
+ ## LLM (optional)
93
+
94
+ The tool can call an LLM to get **explanations** and **concrete code refactor suggestions**. You only need one provider; the first one with a key wins.
95
+
96
+ | Provider | Env var(s) | Optional env |
97
+ |----------|------------|---------------|
98
+ | **OpenRouter** | `OPENROUTER_API_KEY` | `OPENROUTER_MODEL` (default: `google/gemini-2.0-flash-001`) |
99
+ | **Gemini** | `GEMINI_API_KEY` or `GOOGLE_GENAI_API_KEY` | `GEMINI_MODEL` (default: `gemini-1.5-flash`) |
100
+ | **OpenAI** | `OPENAI_API_KEY` or `ANTHROPIC_API_KEY` | `OPENAI_BASE_URL`, `OPENAI_MODEL` (default: `gpt-4o-mini`) |
101
+
102
+ With a key set, the CLI and HTML report will include:
103
+
104
+ - **Overall assessment** — One short paragraph on the codebase.
105
+ - **Per-file** — For hotspot files: short assessment and, when the LLM suggests one, a **suggested refactor** code block.
106
+ - **Per debt item** — Why it matters, what to do, and when applicable a **suggested refactor** code block (parsed from the LLM response).
107
+
108
+ Use `--no-llm` to run with no API calls.
109
+
110
+ ---
111
+
112
+ ## What we measure
113
+
114
+ - **Cyclomatic complexity** — Decision points in the AST (if/for/while/switch/ternary/&&/||). Higher values suggest harder testing and refactors.
115
+ - **Documentation** — Presence of module-level JSDoc or docstrings.
116
+ - **Hotspots** — Files with high git churn *and* high complexity (riskiest to change).
117
+ - **Debt trend** — Simple heuristic over recent commits (files touched); not a full historical debt metric.
118
+ - **Cleanliness tier** — Score 0–100 mapped to a 1–5 label (e.g. “Pure Coder (5/5)” for low debt).
119
+
120
+ ---
121
+
122
+ ## Contributing
123
+
124
+ We’d love **issues** and **contributions**.
125
+ Please read **[CONTRIBUTING.md](CONTRIBUTING.md)** before sending a pull request.
126
+
127
+ - **Bugs or confusing behavior?** Open an issue and describe what you ran and what you expected.
128
+ - **Feature ideas?** Open an issue with a short use case; we’re happy to discuss.
129
+ - **Want to add a language?** Implement the `IAnalyzer` interface in `src/analyzers/` (see `javascript.ts` / `python.ts`), register it in `src/analyzers/index.ts`, and add the right file extensions in `src/discover.ts`. PRs welcome.
130
+ - **Code or docs?** Open a PR. You must agree to our **[Contributor License Agreement (CLA)](CLA.md)** before we can merge; the PR template includes a checkbox for this.
131
+
132
+ ---
133
+
134
+ ## Repo layout
135
+
136
+ ```
137
+ src/
138
+ cli.ts # Entrypoint, progress, output
139
+ engine.ts # Runs discovery → analyzers → git → optional LLM
140
+ discover.ts # File discovery by extension
141
+ git-analyzer.ts # Churn, hotspots, trend from git log
142
+ llm.ts # LLM calls (OpenAI/OpenRouter/Gemini), prompts, code-block parsing
143
+ cleanliness-score.ts # Score → tier (1–5)
144
+ types.ts # DebtItem, FileMetrics, AnalysisRun, IAnalyzer
145
+ analyzers/ # Per-language: tree-sitter parse, complexity, debt items
146
+ reports/ # HTML, JSON, Markdown
147
+ ```
148
+
149
+ ---
150
+
151
+ ## Publishing (maintainers)
152
+
153
+ To publish this package to npm: see **[docs/PUBLISHING.md](docs/PUBLISHING.md)** for step-by-step instructions (login, build, publish, scoped name if needed).
154
+
155
+ ## License
156
+
157
+ **Dual licensing.**
158
+
159
+ - **GPL-3.0 (open source):** You may use, modify, and distribute this software under the [GNU General Public License v3](https://www.gnu.org/licenses/gpl-3.0.html). You must comply with the GPL (e.g., disclose source for derivatives, same license).
160
+ - **Commercial license:** If you need to use this in proprietary or commercial software without GPL obligations, a separate commercial license is available from the copyright holder. Contact the copyright holder for terms.
161
+
162
+ See [LICENSE](LICENSE) for the GPL-3.0 text and dual-licensing note.
163
+
164
+ ---
165
+
166
+ *This project is an independent initiative and is not affiliated with, endorsed by, or operated by any formed legal entity.*
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Base utilities for AST-based analyzers: complexity from tree-sitter trees,
3
+ * and shared file discovery.
4
+ */
5
+ import Parser from "tree-sitter";
6
+ import type { DebtCategory, DebtItem, Severity } from "../types.js";
7
+ /** Recursively count complexity from tree (cyclomatic: decision points only). */
8
+ export declare function countComplexity(node: Parser.SyntaxNode): number;
9
+ /** Count lines in source (excluding empty and comment-only). */
10
+ export declare function effectiveLines(source: string): number;
11
+ export declare function createDebtItem(file: string, category: DebtCategory, title: string, description: string, opts?: {
12
+ line?: number;
13
+ endLine?: number;
14
+ severity?: Severity;
15
+ confidence?: number;
16
+ metrics?: Record<string, number | string>;
17
+ }): DebtItem;
18
+ export declare function inferSeverity(complexity: number): Severity;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Base utilities for AST-based analyzers: complexity from tree-sitter trees,
3
+ * and shared file discovery.
4
+ */
5
+ /** Tree-sitter node types that add cyclomatic complexity (decision points only). */
6
+ const COMPLEXITY_NODE_TYPES = new Set([
7
+ "if_statement",
8
+ "else_clause",
9
+ "elif_clause",
10
+ "for_statement",
11
+ "while_statement",
12
+ "switch_statement",
13
+ "case_clause",
14
+ "catch_clause",
15
+ "except_clause",
16
+ "conditional_expression",
17
+ ]);
18
+ /** Recursively count complexity from tree (cyclomatic: decision points only). */
19
+ export function countComplexity(node) {
20
+ let n = 0;
21
+ const type = node.type;
22
+ if (COMPLEXITY_NODE_TYPES.has(type))
23
+ n++;
24
+ if (type === "binary_expression") {
25
+ const text = node.text;
26
+ if (text.includes("&&") || text.includes("||"))
27
+ n++;
28
+ }
29
+ for (let i = 0; i < node.childCount; i++) {
30
+ const child = node.child(i);
31
+ if (child)
32
+ n += countComplexity(child);
33
+ }
34
+ return n;
35
+ }
36
+ /** Count lines in source (excluding empty and comment-only). */
37
+ export function effectiveLines(source) {
38
+ return source
39
+ .split("\n")
40
+ .filter((line) => {
41
+ const t = line.trim();
42
+ return t.length > 0 && !t.startsWith("//") && !t.startsWith("#") && !t.startsWith("/*") && !t.startsWith("*");
43
+ }).length;
44
+ }
45
+ export function createDebtItem(file, category, title, description, opts = {}) {
46
+ const id = `${file}:${opts.line ?? 0}:${category}:${title.slice(0, 30)}`.replace(/\s/g, "_");
47
+ return {
48
+ id,
49
+ file,
50
+ line: opts.line,
51
+ endLine: opts.endLine,
52
+ category,
53
+ severity: opts.severity ?? "medium",
54
+ title,
55
+ description,
56
+ confidence: opts.confidence ?? 0.8,
57
+ metrics: opts.metrics,
58
+ };
59
+ }
60
+ export function inferSeverity(complexity) {
61
+ if (complexity >= 20)
62
+ return "critical";
63
+ if (complexity >= 10)
64
+ return "high";
65
+ if (complexity >= 5)
66
+ return "medium";
67
+ return "low";
68
+ }
@@ -0,0 +1,4 @@
1
+ import type { IAnalyzer } from "../types.js";
2
+ export declare const analyzers: IAnalyzer[];
3
+ export { javascriptAnalyzer } from "./javascript.js";
4
+ export { pythonAnalyzer } from "./python.js";
@@ -0,0 +1,5 @@
1
+ import { javascriptAnalyzer } from "./javascript.js";
2
+ import { pythonAnalyzer } from "./python.js";
3
+ export const analyzers = [javascriptAnalyzer, pythonAnalyzer];
4
+ export { javascriptAnalyzer } from "./javascript.js";
5
+ export { pythonAnalyzer } from "./python.js";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * JavaScript/TypeScript analyzer using tree-sitter.
3
+ * Metrics: cyclomatic complexity, line count, basic duplication heuristic, documentation.
4
+ */
5
+ import type { AnalyzerResult } from "../types.js";
6
+ export declare const javascriptAnalyzer: {
7
+ name: string;
8
+ languages: string[];
9
+ canAnalyze(filePath: string): boolean;
10
+ analyze(files: Map<string, string>, _options?: {
11
+ repoPath?: string;
12
+ }): Promise<AnalyzerResult>;
13
+ };
@@ -0,0 +1,89 @@
1
+ /**
2
+ * JavaScript/TypeScript analyzer using tree-sitter.
3
+ * Metrics: cyclomatic complexity, line count, basic duplication heuristic, documentation.
4
+ */
5
+ import Parser from "tree-sitter";
6
+ import JavaScript from "tree-sitter-javascript";
7
+ import TypeScript from "tree-sitter-typescript";
8
+ import { countComplexity, createDebtItem, effectiveLines, inferSeverity, } from "./base.js";
9
+ const LANG_JS = "javascript";
10
+ const LANG_TS = "typescript";
11
+ const JS_EXT = /\.(?:js|jsx|mjs|cjs)$/i;
12
+ const TS_EXT = /\.(?:ts|tsx)$/i;
13
+ function getParser(filePath) {
14
+ const P = new Parser();
15
+ const TS = TypeScript;
16
+ if (filePath.endsWith(".tsx") && TS.tsx) {
17
+ P.setLanguage(TS.tsx);
18
+ }
19
+ else if (TS_EXT.test(filePath) && TS.typescript) {
20
+ P.setLanguage(TS.typescript);
21
+ }
22
+ else {
23
+ const J = JavaScript;
24
+ P.setLanguage((J.default ?? JavaScript));
25
+ }
26
+ return P;
27
+ }
28
+ function languageForPath(filePath) {
29
+ return TS_EXT.test(filePath) ? LANG_TS : LANG_JS;
30
+ }
31
+ export const javascriptAnalyzer = {
32
+ name: "javascript",
33
+ languages: [LANG_JS, LANG_TS],
34
+ canAnalyze(filePath) {
35
+ return JS_EXT.test(filePath) || TS_EXT.test(filePath);
36
+ },
37
+ async analyze(files, _options) {
38
+ const metrics = [];
39
+ const debtItems = [];
40
+ const errors = [];
41
+ for (const [path, content] of files) {
42
+ const lang = languageForPath(path);
43
+ const parser = getParser(path);
44
+ try {
45
+ const tree = parser.parse(content);
46
+ const root = tree.rootNode;
47
+ const complexity = root ? countComplexity(root) : 0;
48
+ const lineCount = effectiveLines(content);
49
+ const hasDocumentation = /^\s*(\/\*\*[\s\S]*?\*\/|\/\/\s*@file|\/\*\*)/m.test(content);
50
+ const fileMetric = {
51
+ file: path,
52
+ language: lang,
53
+ cyclomaticComplexity: complexity,
54
+ cognitiveComplexity: complexity,
55
+ lineCount,
56
+ hasDocumentation,
57
+ };
58
+ metrics.push(fileMetric);
59
+ if (complexity >= 5) {
60
+ debtItems.push(createDebtItem(path, "complexity", `High cyclomatic complexity (${complexity})`, `This file has ${complexity} decision points, which makes it harder to test and maintain.`, {
61
+ severity: inferSeverity(complexity),
62
+ confidence: 0.85,
63
+ metrics: { cyclomaticComplexity: complexity },
64
+ }));
65
+ }
66
+ if (lineCount > 300) {
67
+ debtItems.push(createDebtItem(path, "complexity", "Large file", `File has ${lineCount} effective lines. Consider splitting into smaller modules.`, {
68
+ severity: lineCount > 500 ? "high" : "medium",
69
+ confidence: 0.75,
70
+ metrics: { lineCount },
71
+ }));
72
+ }
73
+ if (!hasDocumentation && lineCount > 50) {
74
+ debtItems.push(createDebtItem(path, "documentation", "Missing module-level documentation", "No JSDoc or file-level comment found. Document purpose and usage for maintainability.", { confidence: 0.7 }));
75
+ }
76
+ }
77
+ catch (e) {
78
+ errors.push({ file: path, message: e instanceof Error ? e.message : String(e) });
79
+ }
80
+ }
81
+ return {
82
+ language: "javascript",
83
+ files: [...files.keys()],
84
+ metrics,
85
+ debtItems,
86
+ errors,
87
+ };
88
+ },
89
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Python analyzer using tree-sitter.
3
+ * Metrics: cyclomatic complexity, line count, docstring presence.
4
+ */
5
+ import type { AnalyzerResult } from "../types.js";
6
+ export declare const pythonAnalyzer: {
7
+ name: string;
8
+ languages: string[];
9
+ canAnalyze(filePath: string): boolean;
10
+ analyze(files: Map<string, string>, _options?: {
11
+ repoPath?: string;
12
+ }): Promise<AnalyzerResult>;
13
+ };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Python analyzer using tree-sitter.
3
+ * Metrics: cyclomatic complexity, line count, docstring presence.
4
+ */
5
+ import Parser from "tree-sitter";
6
+ import Python from "tree-sitter-python";
7
+ import { countComplexity, createDebtItem, effectiveLines, inferSeverity, } from "./base.js";
8
+ const PY_EXT = /\.py$/i;
9
+ function getParser() {
10
+ const P = new Parser();
11
+ P.setLanguage(Python);
12
+ return P;
13
+ }
14
+ function hasModuleDocstring(content) {
15
+ const trimmed = content.trimStart();
16
+ return (trimmed.startsWith('"""') ||
17
+ trimmed.startsWith("'''") ||
18
+ /^\s*#.*\n(\s*#.*\n)*/.test(content));
19
+ }
20
+ export const pythonAnalyzer = {
21
+ name: "python",
22
+ languages: ["python"],
23
+ canAnalyze(filePath) {
24
+ return PY_EXT.test(filePath);
25
+ },
26
+ async analyze(files, _options) {
27
+ const metrics = [];
28
+ const debtItems = [];
29
+ const errors = [];
30
+ const parser = getParser();
31
+ for (const [path, content] of files) {
32
+ try {
33
+ const tree = parser.parse(content);
34
+ const root = tree.rootNode;
35
+ const complexity = root ? countComplexity(root) : 0;
36
+ const lineCount = effectiveLines(content);
37
+ const hasDocumentation = hasModuleDocstring(content);
38
+ const fileMetric = {
39
+ file: path,
40
+ language: "python",
41
+ cyclomaticComplexity: complexity,
42
+ cognitiveComplexity: complexity,
43
+ lineCount,
44
+ hasDocumentation,
45
+ };
46
+ metrics.push(fileMetric);
47
+ if (complexity >= 5) {
48
+ debtItems.push(createDebtItem(path, "complexity", `High cyclomatic complexity (${complexity})`, `This module has ${complexity} decision points. Consider simplifying conditionals or extracting functions.`, {
49
+ severity: inferSeverity(complexity),
50
+ confidence: 0.85,
51
+ metrics: { cyclomaticComplexity: complexity },
52
+ }));
53
+ }
54
+ if (lineCount > 250) {
55
+ debtItems.push(createDebtItem(path, "complexity", "Large module", `File has ${lineCount} effective lines. Consider splitting into smaller modules or packages.`, {
56
+ severity: lineCount > 400 ? "high" : "medium",
57
+ confidence: 0.75,
58
+ metrics: { lineCount },
59
+ }));
60
+ }
61
+ if (!hasDocumentation && lineCount > 30) {
62
+ debtItems.push(createDebtItem(path, "documentation", "Missing module docstring", "No module-level docstring found. Add a docstring describing the module's purpose.", { confidence: 0.7 }));
63
+ }
64
+ }
65
+ catch (e) {
66
+ errors.push({ file: path, message: e instanceof Error ? e.message : String(e) });
67
+ }
68
+ }
69
+ return {
70
+ language: "python",
71
+ files: [...files.keys()],
72
+ metrics,
73
+ debtItems,
74
+ errors,
75
+ };
76
+ },
77
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Technical Debt Cleanliness Score: map debt score (0-100) to one of five tiers.
3
+ * Lower debt score = higher tier (cleaner code).
4
+ */
5
+ export interface CleanlinessTier {
6
+ tier: 1 | 2 | 3 | 4 | 5;
7
+ label: string;
8
+ description: string;
9
+ }
10
+ /** Debt score 0-100 (higher = worse). Returns tier 1-5 (1 = worst, 5 = best). */
11
+ export declare function getCleanlinessTier(debtScore: number): CleanlinessTier;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Technical Debt Cleanliness Score: map debt score (0-100) to one of five tiers.
3
+ * Lower debt score = higher tier (cleaner code).
4
+ */
5
+ const TIERS = [
6
+ { tier: 1, label: "Prompt Roulette Champion", description: "100% vibes, 0% understanding" },
7
+ { tier: 2, label: "Vibe Coding Hero", description: '"Just make it work" smashes accept button' },
8
+ { tier: 3, label: "Thoughtful Prompter", description: "AI helps, human decides" },
9
+ { tier: 4, label: "Power Tool User", description: "AI is the nail gun" },
10
+ { tier: 5, label: "Pure Coder", description: "Hand-crafted artisanal code, no AI needed" },
11
+ ];
12
+ /** Debt score 0-100 (higher = worse). Returns tier 1-5 (1 = worst, 5 = best). */
13
+ export function getCleanlinessTier(debtScore) {
14
+ const clamped = Math.max(0, Math.min(100, Math.round(debtScore)));
15
+ if (clamped <= 20)
16
+ return TIERS[4]; // 5/5
17
+ if (clamped <= 40)
18
+ return TIERS[3]; // 4/5
19
+ if (clamped <= 60)
20
+ return TIERS[2]; // 3/5
21
+ if (clamped <= 80)
22
+ return TIERS[1]; // 2/5
23
+ return TIERS[0]; // 1/5
24
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI entry: colorful terminal output, progress bars, actionable insights.
4
+ */
5
+ export {};