fallow-code-scan 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/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # Code Scan
2
+
3
+ Local web dashboard for Fallow code analysis reports.
4
+
5
+ ## Usage
6
+
7
+ Run it from the root of the project you want to analyze:
8
+
9
+ ```sh
10
+ npx fallow-code-scan
11
+ ```
12
+
13
+ Open the printed local URL, then press **Refresh** to run a scan.
14
+
15
+ By default Code Scan listens on `127.0.0.1:5179`. Override it with environment
16
+ variables when needed:
17
+
18
+ ```sh
19
+ HOST=0.0.0.0 PORT=5180 npx fallow-code-scan
20
+ ```
21
+
22
+ ## What It Shows
23
+
24
+ - Blocking Findings from Fallow checks
25
+ - Refactoring Suggestions from maintainability analysis
26
+ - Code Health metrics and lowest maintainability files
27
+ - Copyable text reports for sharing findings
28
+
29
+ ## Requirements
30
+
31
+ - Node.js 18 or newer
32
+ - A JavaScript or TypeScript project supported by Fallow
33
+
34
+ Fallow is installed as a package dependency, so users do not need to install it
35
+ separately.
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "fallow-code-scan",
3
+ "version": "0.1.0",
4
+ "description": "Local web dashboard for Fallow code analysis reports.",
5
+ "main": "src/server.js",
6
+ "bin": {
7
+ "code-scan": "src/start.js",
8
+ "fallow-code-scan": "src/start.js"
9
+ },
10
+ "files": [
11
+ "public",
12
+ "src",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "dev": "node src/start.js",
17
+ "test": "node --test test/*.test.js",
18
+ "pack:dry": "npm pack --dry-run"
19
+ },
20
+ "keywords": [
21
+ "fallow",
22
+ "code-analysis",
23
+ "dashboard",
24
+ "maintainability",
25
+ "dead-code"
26
+ ],
27
+ "license": "MIT",
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "dependencies": {
35
+ "fallow": "2.91.0",
36
+ "lucide": "1.21.0"
37
+ }
38
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ async function writeText(text) {
3
+ if (copyWithTextarea(text)) return;
4
+ if (navigator.clipboard && navigator.clipboard.writeText) {
5
+ await navigator.clipboard.writeText(text);
6
+ return;
7
+ }
8
+ throw new Error("Copy is not available in this browser.");
9
+ }
10
+
11
+ function copyWithTextarea(text) {
12
+ const textarea = document.createElement("textarea");
13
+ textarea.value = text;
14
+ textarea.setAttribute("readonly", "");
15
+ textarea.className = "copy-buffer";
16
+ document.body.appendChild(textarea);
17
+ textarea.focus();
18
+ textarea.select();
19
+ textarea.setSelectionRange(0, textarea.value.length);
20
+
21
+ try {
22
+ return document.execCommand("copy");
23
+ } finally {
24
+ textarea.remove();
25
+ }
26
+ }
27
+
28
+ window.codeScanCopy = {
29
+ writeText
30
+ };
31
+ }
@@ -0,0 +1,54 @@
1
+ {
2
+ function renderSection(section, escapeHtml) {
3
+ const node = document.createElement("details");
4
+ node.className = "finding-group-static";
5
+ node.open = true;
6
+ node.innerHTML = `
7
+ <summary>
8
+ <i class="finding-chevron chevron-open" data-lucide="chevron-down" aria-hidden="true"></i>
9
+ <i class="finding-chevron chevron-closed" data-lucide="chevron-right" aria-hidden="true"></i>
10
+ <i class="finding-type-icon" data-lucide="${typeIcon(section.id)}" aria-hidden="true"></i>
11
+ <h3>${escapeHtml(section.label)}</h3>
12
+ </summary>
13
+ <ul>${recordsHtml(section.records, escapeHtml)}</ul>
14
+ `;
15
+ return node;
16
+ }
17
+
18
+ function recordsHtml(records, escapeHtml) {
19
+ const html = records.map((record) => `
20
+ <li class="finding-record">
21
+ <div>
22
+ <span class="item-title">${escapeHtml(record.title)}</span>
23
+ <span class="item-detail">${escapeHtml(record.detail)}</span>
24
+ </div>
25
+ </li>
26
+ `).join("");
27
+ return html || "<li><span>No samples included.</span></li>";
28
+ }
29
+
30
+ function typeIcon(sectionId) {
31
+ const icons = {
32
+ boundary_violations: "shield-alert",
33
+ circular_dependencies: "refresh-ccw",
34
+ complexity_findings: "gauge",
35
+ duplicate_exports: "copy-x",
36
+ duplicated_code: "files",
37
+ re_export_cycles: "repeat-2",
38
+ stale_suppressions: "message-square-warning",
39
+ unlisted_dependencies: "package-plus",
40
+ unresolved_imports: "import",
41
+ unused_class_members: "square-minus",
42
+ unused_dependencies: "package-minus",
43
+ unused_enum_members: "list-minus",
44
+ unused_exports: "package-x",
45
+ unused_files: "file-x-2",
46
+ unused_types: "braces"
47
+ };
48
+ return icons[sectionId] || "circle-x";
49
+ }
50
+
51
+ window.codeScanFindings = {
52
+ renderSection
53
+ };
54
+ }
@@ -0,0 +1,148 @@
1
+ {
2
+ const STATUS_LABELS = {
3
+ failed: "Failed",
4
+ idle: "Idle",
5
+ ready: "Ready",
6
+ running: "Scanning"
7
+ };
8
+
9
+ function effortTone(effort) {
10
+ if (effort === "high") return "critical";
11
+ if (effort === "medium") return "warn";
12
+ if (effort === "low") return "good";
13
+ return "neutral";
14
+ }
15
+
16
+ function formatNumber(value) {
17
+ return new Intl.NumberFormat().format(Number(value) || 0);
18
+ }
19
+
20
+ function formatRecommendation(recommendation) {
21
+ return String(recommendation || "")
22
+ .replace(/\((\d+) LOC\)/g, "($1 lines of code)")
23
+ .replace(/(\d+) dependents amplify every change/g, "$1 files depend on it")
24
+ .replace(/fan-in/g, "incoming references")
25
+ .replace(/fan-out/g, "outgoing dependencies");
26
+ }
27
+
28
+ function thresholdTone(value, goodMax, warnMax) {
29
+ const number = Number(value);
30
+ if (number <= goodMax) return "good";
31
+ if (number <= warnMax) return "warn";
32
+ return "critical";
33
+ }
34
+
35
+ function formatReportText(report) {
36
+ const lines = [
37
+ "Code Scan",
38
+ `Generated: ${report.generatedAt}`,
39
+ `Status: ${report.status}`,
40
+ `Blocking findings: ${report.hardFindings.count}`,
41
+ `Refactoring suggestions: ${report.health.targets.length}`,
42
+ "",
43
+ "Blocking findings"
44
+ ];
45
+
46
+ appendCheckSections(lines, report.hardFindings.sections);
47
+ appendTargets(lines, report.health.targets, true);
48
+ return `${lines.join("\n")}\n`;
49
+ }
50
+
51
+ function formatBlockingFindingsText(report) {
52
+ const lines = ["Blocking findings"];
53
+ appendCheckSections(lines, report.hardFindings.sections);
54
+ return `${lines.join("\n")}\n`;
55
+ }
56
+
57
+ function formatRiskSignalsText(report) {
58
+ const health = report.health;
59
+ const lines = [
60
+ "Risk signals",
61
+ `Functions over complexity limit: ${formatNumber(health.summary.functionsAboveThreshold)}`,
62
+ `90th percentile function complexity: ${formatNumber(health.vitalSigns.p90Cyclomatic)}`,
63
+ `Files with high coupling: ${health.vitalSigns.couplingHighPercent}% of scored files`
64
+ ];
65
+ return `${lines.join("\n")}\n`;
66
+ }
67
+
68
+ function formatRefactoringSuggestionsText(report) {
69
+ const lines = ["Refactoring suggestions"];
70
+ appendTargets(lines, report.health.targets, false);
71
+ return `${lines.join("\n")}\n`;
72
+ }
73
+
74
+ function statusText(payload) {
75
+ if (payload.running) return "Scanning";
76
+ return statusLabel(payload.state);
77
+ }
78
+
79
+ function runSummaryText(payload) {
80
+ if (!payload.report) return "No scan has run yet.";
81
+ const generatedAt = formatDate(payload.report.generatedAt || payload.finishedAt);
82
+ const seconds = Math.max(0.1, payload.report.durationMs / 1000).toFixed(1);
83
+ return `${generatedAt} · ${seconds} seconds · Fallow ${payload.report.version || "unknown"}`;
84
+ }
85
+
86
+ function runSummaryHtml(payload) {
87
+ if (!payload.report) return "No scan has run yet.";
88
+ const generatedAt = formatDate(payload.report.generatedAt || payload.finishedAt);
89
+ const seconds = Math.max(0.1, payload.report.durationMs / 1000).toFixed(1);
90
+ const version = payload.report.version || "unknown";
91
+ return `
92
+ <span class="run-summary-item"><i data-lucide="clock"></i>${generatedAt} · ${seconds} seconds</span>
93
+ <span class="run-summary-item"><i data-lucide="git-commit"></i>Fallow ${version}</span>
94
+ `;
95
+ }
96
+
97
+ function statusLabel(state) {
98
+ return STATUS_LABELS[state] || STATUS_LABELS.idle;
99
+ }
100
+
101
+ function formatDate(value) {
102
+ if (!value) return "No timestamp";
103
+ return new Intl.DateTimeFormat(undefined, {
104
+ dateStyle: "medium",
105
+ timeStyle: "medium"
106
+ }).format(new Date(value));
107
+ }
108
+
109
+ function appendCheckSections(lines, sections) {
110
+ if (sections.length === 0) {
111
+ lines.push("- None");
112
+ return;
113
+ }
114
+
115
+ sections.forEach((section) => {
116
+ lines.push(`- ${section.label}: ${section.count}`);
117
+ section.records.forEach((record) => {
118
+ lines.push(` - ${record.title}${record.detail ? ` (${record.detail})` : ""}`);
119
+ });
120
+ });
121
+ }
122
+
123
+ function appendTargets(lines, targets, includeHeading) {
124
+ if (includeHeading) lines.push("", "Refactoring suggestions");
125
+ if (targets.length === 0) {
126
+ lines.push("- None");
127
+ return;
128
+ }
129
+
130
+ targets.forEach((target) => {
131
+ lines.push(`- ${target.path}: ${formatRecommendation(target.recommendation)}`);
132
+ });
133
+ }
134
+
135
+ window.codeScanFormat = {
136
+ effortTone,
137
+ formatBlockingFindingsText,
138
+ formatNumber,
139
+ formatRecommendation,
140
+ formatRefactoringSuggestionsText,
141
+ formatRiskSignalsText,
142
+ formatReportText,
143
+ runSummaryHtml,
144
+ runSummaryText,
145
+ statusText,
146
+ thresholdTone
147
+ };
148
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ function metricIconHtml(id) {
3
+ const icons = {
4
+ "blocking-findings": "circle-x",
5
+ maintainability: "bar-chart-3",
6
+ "refactoring-suggestions": "scissors"
7
+ };
8
+ const icon = icons[id];
9
+ return icon
10
+ ? `<i class="metric-icon" data-lucide="${icon}" aria-hidden="true"></i>`
11
+ : "";
12
+ }
13
+
14
+ function refresh(root = document) {
15
+ if (!window.lucide) return;
16
+ window.lucide.createIcons({
17
+ attrs: { "stroke-width": 2 },
18
+ icons: window.lucide.icons,
19
+ root
20
+ });
21
+ }
22
+
23
+ function setButtonLabel(button, label) {
24
+ button.querySelector(".button-label").textContent = label;
25
+ }
26
+
27
+ window.codeScanIcons = {
28
+ metricIconHtml,
29
+ refresh,
30
+ setButtonLabel
31
+ };
32
+ }