qodfy 0.2.10 → 0.3.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 +5 -1
- package/dist/index.js +554 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,15 +24,18 @@ Print machine-readable JSON:
|
|
|
24
24
|
npx qodfy scan --json
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
Write JSON or
|
|
27
|
+
Write JSON, Markdown, or HTML reports:
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
30
|
npx qodfy scan --json --output qodfy-report.json
|
|
31
31
|
npx qodfy scan --report qodfy-report.md
|
|
32
|
+
npx qodfy scan --html qodfy-report.html
|
|
32
33
|
```
|
|
33
34
|
|
|
34
35
|
The Markdown report is the **Qodfy Launch Report**: a senior-engineer-style review with a launch status, executive summary, top priorities, what looks good, and per-issue context (what Qodfy found, why it matters, evidence, suggested fix, and an AI fix prompt).
|
|
35
36
|
|
|
37
|
+
The HTML report is a standalone, self-contained file (inline CSS, no external CDN, no JavaScript) that you can open directly in a browser to share with teammates or clients.
|
|
38
|
+
|
|
36
39
|
## What Qodfy Checks Today
|
|
37
40
|
|
|
38
41
|
Qodfy scans locally and looks for common launch-readiness risks:
|
|
@@ -85,6 +88,7 @@ qodfy scan --path <project-path>
|
|
|
85
88
|
qodfy scan --json
|
|
86
89
|
qodfy scan --json --output qodfy-report.json
|
|
87
90
|
qodfy scan --report qodfy-report.md
|
|
91
|
+
qodfy scan --html qodfy-report.html
|
|
88
92
|
qodfy --help
|
|
89
93
|
qodfy --version
|
|
90
94
|
```
|
package/dist/index.js
CHANGED
|
@@ -11,11 +11,11 @@ import {
|
|
|
11
11
|
scanProject,
|
|
12
12
|
validScanChecks
|
|
13
13
|
} from "@qodfy/core";
|
|
14
|
-
var CLI_VERSION = "0.
|
|
14
|
+
var CLI_VERSION = "0.3.0";
|
|
15
15
|
var DEFAULT_MAX_ISSUES = 5;
|
|
16
16
|
var program = new Command();
|
|
17
17
|
program.name("qodfy").description("Launch readiness scanner for AI-built apps.").version(CLI_VERSION);
|
|
18
|
-
program.command("scan").description("Scan a project for launch readiness issues.").option("-p, --path <path>", "Project path to scan", process.cwd()).option("--max-issues <number>", "Maximum number of issues to display", String(DEFAULT_MAX_ISSUES)).option("--prompts", "Show safe copy-paste fix prompts for displayed issues").option("--prompt <issue-id>", "Show the safe AI fix prompt for one issue").option("--checks <checks>", "Comma-separated checks to run").option("--all", "Run all checks without prompting").option("--no-interactive", "Skip interactive prompts and run the recommended scan").option("--json", "Print a machine-readable JSON report").option("--output <file>", "Write the JSON report to a file").option("--report <file>", "Write a human-readable Markdown report to a file").action(async (options) => {
|
|
18
|
+
program.command("scan").description("Scan a project for launch readiness issues.").option("-p, --path <path>", "Project path to scan", process.cwd()).option("--max-issues <number>", "Maximum number of issues to display", String(DEFAULT_MAX_ISSUES)).option("--prompts", "Show safe copy-paste fix prompts for displayed issues").option("--prompt <issue-id>", "Show the safe AI fix prompt for one issue").option("--checks <checks>", "Comma-separated checks to run").option("--all", "Run all checks without prompting").option("--no-interactive", "Skip interactive prompts and run the recommended scan").option("--json", "Print a machine-readable JSON report").option("--output <file>", "Write the JSON report to a file").option("--report <file>", "Write a human-readable Markdown report to a file").option("--html <file>", "Write a standalone HTML report to a file").action(async (options) => {
|
|
19
19
|
const outputOptionsResult = validateScanOutputOptions(options);
|
|
20
20
|
if (!outputOptionsResult.ok) {
|
|
21
21
|
printScanError(outputOptionsResult.reason, !isOutputMode(options));
|
|
@@ -64,6 +64,11 @@ program.command("scan").description("Scan a project for launch readiness issues.
|
|
|
64
64
|
console.log(`Qodfy report saved to ${options.report}`);
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
|
+
if (options.html) {
|
|
68
|
+
await writeReportFile(options.html, renderHtmlReport(outputReport));
|
|
69
|
+
console.log(`Qodfy HTML report saved to ${options.html}`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
67
72
|
if (options.prompt) {
|
|
68
73
|
printPromptFromReport(report, options.prompt);
|
|
69
74
|
return;
|
|
@@ -151,16 +156,34 @@ function validateScanOutputOptions(options) {
|
|
|
151
156
|
reason: "Use either --json or --report for one scan command, not both."
|
|
152
157
|
};
|
|
153
158
|
}
|
|
154
|
-
if (
|
|
159
|
+
if (options.json && options.html) {
|
|
155
160
|
return {
|
|
156
161
|
ok: false,
|
|
157
|
-
reason: "Use
|
|
162
|
+
reason: "Use either --json or --html for one scan command, not both."
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (options.report && options.html) {
|
|
166
|
+
return {
|
|
167
|
+
ok: false,
|
|
168
|
+
reason: "Use either --report or --html for one scan command, not both."
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (options.html && options.output) {
|
|
172
|
+
return {
|
|
173
|
+
ok: false,
|
|
174
|
+
reason: "--output is only used with --json. For HTML, just pass --html <file>."
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
if ((options.json || options.report || options.html) && options.prompt) {
|
|
178
|
+
return {
|
|
179
|
+
ok: false,
|
|
180
|
+
reason: "Use qodfy prompt <issue-id> for fix prompts, or run qodfy scan --json/--report/--html for reports."
|
|
158
181
|
};
|
|
159
182
|
}
|
|
160
183
|
return { ok: true };
|
|
161
184
|
}
|
|
162
185
|
function isOutputMode(options) {
|
|
163
|
-
return Boolean(options.json || options.output || options.report);
|
|
186
|
+
return Boolean(options.json || options.output || options.report || options.html);
|
|
164
187
|
}
|
|
165
188
|
async function resolveScanMode(options) {
|
|
166
189
|
if (options.checks) {
|
|
@@ -540,6 +563,532 @@ function appendMarkdownFooter(lines) {
|
|
|
540
563
|
lines.push("", "## Generated by Qodfy", "");
|
|
541
564
|
lines.push("Qodfy scans locally and does not print secret values in reports.");
|
|
542
565
|
}
|
|
566
|
+
function escapeHtml(value) {
|
|
567
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
568
|
+
}
|
|
569
|
+
function renderHtmlReport(report) {
|
|
570
|
+
const projectName = path.basename(report.projectPath) || report.projectPath;
|
|
571
|
+
const statusLabel = getStatusLabel(report.score);
|
|
572
|
+
const statusTone = getStatusTone(report.score);
|
|
573
|
+
const criticalCount = countIssuesBySeverity(report.issues, "critical");
|
|
574
|
+
const warningCount = countIssuesBySeverity(report.issues, "warning");
|
|
575
|
+
const infoCount = countIssuesBySeverity(report.issues, "info");
|
|
576
|
+
const executiveSummary = getExecutiveSummary(report);
|
|
577
|
+
const priorities = getTopPriorities(report.issues);
|
|
578
|
+
const observations = getWhatLooksGood(report);
|
|
579
|
+
const sortedIssues = getSortedDisplayIssues(report.issues);
|
|
580
|
+
const severityOrder = ["critical", "warning", "info"];
|
|
581
|
+
const summaryCards = [
|
|
582
|
+
{ label: "Critical issues", value: criticalCount, tone: criticalCount > 0 ? "critical" : "neutral" },
|
|
583
|
+
{ label: "Warnings", value: warningCount, tone: warningCount > 0 ? "warning" : "neutral" },
|
|
584
|
+
{ label: "Info", value: infoCount, tone: infoCount > 0 ? "info" : "neutral" },
|
|
585
|
+
{ label: "Files scanned", value: report.stats.totalFiles, tone: "neutral" },
|
|
586
|
+
{ label: "API routes", value: report.stats.apiRoutes, tone: "neutral" },
|
|
587
|
+
{ label: "Scan duration", value: formatDuration(report.stats.durationMs), tone: "neutral" }
|
|
588
|
+
];
|
|
589
|
+
const summaryCardsHtml = summaryCards.map(
|
|
590
|
+
(card) => ` <div class="stat-card stat-${card.tone}">
|
|
591
|
+
<div class="stat-label">${escapeHtml(card.label)}</div>
|
|
592
|
+
<div class="stat-value">${escapeHtml(String(card.value))}</div>
|
|
593
|
+
</div>`
|
|
594
|
+
).join("\n");
|
|
595
|
+
const prioritiesHtml = priorities.length === 0 ? `<p class="muted">No urgent priorities found. Review warnings below before launch.</p>` : `<ol class="priority-list">
|
|
596
|
+
${priorities.map((priority) => ` <li>${escapeHtml(priority)}</li>`).join("\n")}
|
|
597
|
+
</ol>`;
|
|
598
|
+
const observationsHtml = observations.length === 0 ? `<p class="muted">No positive observations to highlight from this scan.</p>` : `<ul class="observation-list">
|
|
599
|
+
${observations.map((observation) => ` <li>${escapeHtml(observation)}</li>`).join("\n")}
|
|
600
|
+
</ul>`;
|
|
601
|
+
const issueSectionsHtml = report.issues.length === 0 ? `<p class="muted">No issues found.</p>` : severityOrder.map((severity) => renderHtmlSeveritySection(severity, sortedIssues.filter((issue) => issue.severity === severity))).filter(Boolean).join("\n");
|
|
602
|
+
return `<!doctype html>
|
|
603
|
+
<html lang="en">
|
|
604
|
+
<head>
|
|
605
|
+
<meta charset="utf-8" />
|
|
606
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
607
|
+
<meta name="generator" content="Qodfy" />
|
|
608
|
+
<title>Qodfy Launch Readiness Report</title>
|
|
609
|
+
<style>${getHtmlReportStyles()}</style>
|
|
610
|
+
</head>
|
|
611
|
+
<body>
|
|
612
|
+
<main class="page">
|
|
613
|
+
<header class="hero">
|
|
614
|
+
<p class="eyebrow">Qodfy</p>
|
|
615
|
+
<h1>Launch Readiness Report</h1>
|
|
616
|
+
<dl class="hero-meta">
|
|
617
|
+
<div><dt>Project</dt><dd>${escapeHtml(projectName)}</dd></div>
|
|
618
|
+
<div><dt>Path</dt><dd><code>${escapeHtml(report.projectPath)}</code></dd></div>
|
|
619
|
+
<div><dt>Scan mode</dt><dd>${escapeHtml(report.scanMode)}</dd></div>
|
|
620
|
+
<div><dt>Generated</dt><dd>${escapeHtml(report.generatedAt)}</dd></div>
|
|
621
|
+
</dl>
|
|
622
|
+
<div class="score-card score-${statusTone}">
|
|
623
|
+
<div class="score-value">${escapeHtml(String(report.score))}<span class="score-max">/100</span></div>
|
|
624
|
+
<div class="score-status">${escapeHtml(statusLabel)}</div>
|
|
625
|
+
</div>
|
|
626
|
+
</header>
|
|
627
|
+
|
|
628
|
+
<section class="stats" aria-label="Scan summary">
|
|
629
|
+
${summaryCardsHtml}
|
|
630
|
+
</section>
|
|
631
|
+
|
|
632
|
+
<section class="card" aria-labelledby="executive-summary">
|
|
633
|
+
<h2 id="executive-summary">Executive Summary</h2>
|
|
634
|
+
<p>${escapeHtml(executiveSummary)}</p>
|
|
635
|
+
</section>
|
|
636
|
+
|
|
637
|
+
<section class="card" aria-labelledby="top-priorities">
|
|
638
|
+
<h2 id="top-priorities">Top Priorities</h2>
|
|
639
|
+
${prioritiesHtml}
|
|
640
|
+
</section>
|
|
641
|
+
|
|
642
|
+
<section class="card" aria-labelledby="what-looks-good">
|
|
643
|
+
<h2 id="what-looks-good">What Looks Good</h2>
|
|
644
|
+
${observationsHtml}
|
|
645
|
+
</section>
|
|
646
|
+
|
|
647
|
+
<section aria-labelledby="issues-by-priority">
|
|
648
|
+
<h2 id="issues-by-priority">Issues by Priority</h2>
|
|
649
|
+
${issueSectionsHtml}
|
|
650
|
+
</section>
|
|
651
|
+
|
|
652
|
+
<section class="card" aria-labelledby="next-steps">
|
|
653
|
+
<h2 id="next-steps">Recommended Next Steps</h2>
|
|
654
|
+
<ul>
|
|
655
|
+
<li>Fix critical issues first.</li>
|
|
656
|
+
<li>Review warnings before launch.</li>
|
|
657
|
+
<li>Re-run Qodfy after changes.</li>
|
|
658
|
+
<li>Use <code>qodfy prompt <issue-id></code> for focused AI repair prompts.</li>
|
|
659
|
+
</ul>
|
|
660
|
+
</section>
|
|
661
|
+
|
|
662
|
+
<footer class="footer">
|
|
663
|
+
<p><strong>Generated by Qodfy.</strong></p>
|
|
664
|
+
<p class="muted">Qodfy scans locally and does not print secret values in reports.</p>
|
|
665
|
+
</footer>
|
|
666
|
+
</main>
|
|
667
|
+
</body>
|
|
668
|
+
</html>
|
|
669
|
+
`;
|
|
670
|
+
}
|
|
671
|
+
function renderHtmlSeveritySection(severity, issues) {
|
|
672
|
+
if (issues.length === 0) {
|
|
673
|
+
return "";
|
|
674
|
+
}
|
|
675
|
+
const heading = severity === "critical" ? "Critical" : severity === "warning" ? "Warnings" : "Info";
|
|
676
|
+
const groupsHtml = categoryOrder.map((category) => {
|
|
677
|
+
const categoryIssues = issues.filter((issue) => issue.category === category);
|
|
678
|
+
if (categoryIssues.length === 0) {
|
|
679
|
+
return "";
|
|
680
|
+
}
|
|
681
|
+
const cardsHtml = categoryIssues.map(renderHtmlIssueCard).join("\n");
|
|
682
|
+
return ` <div class="category-group">
|
|
683
|
+
<h4 class="category-heading">${escapeHtml(categoryLabels[category])}</h4>
|
|
684
|
+
${cardsHtml}
|
|
685
|
+
</div>`;
|
|
686
|
+
}).filter(Boolean).join("\n");
|
|
687
|
+
return ` <section class="severity-section severity-${severity}" aria-label="${escapeHtml(heading)} issues">
|
|
688
|
+
<h3 class="severity-heading"><span class="severity-dot"></span>${escapeHtml(heading)} <span class="severity-count">(${issues.length})</span></h3>
|
|
689
|
+
${groupsHtml}
|
|
690
|
+
</section>`;
|
|
691
|
+
}
|
|
692
|
+
function renderHtmlIssueCard(issue) {
|
|
693
|
+
const evidenceHtml = renderHtmlEvidenceList(issue.evidence);
|
|
694
|
+
const contextHtml = renderHtmlEvidenceList(issue.context);
|
|
695
|
+
const tests = getAfterFixTests(issue);
|
|
696
|
+
const testsHtml = tests.length === 0 ? `<p class="muted">No specific tests suggested.</p>` : `<ul>
|
|
697
|
+
${tests.map((test) => ` <li>${escapeHtml(test)}</li>`).join("\n")}
|
|
698
|
+
</ul>`;
|
|
699
|
+
const fileLine = issue.file ? `<div class="meta-row"><span class="meta-label">File</span><code class="meta-value">${escapeHtml(issue.file)}</code></div>` : "";
|
|
700
|
+
const suggestion = issue.suggestion ?? "No specific suggestion provided for this rule yet.";
|
|
701
|
+
const fixPrompt = issue.fixPrompt ?? "No AI fix prompt available for this rule yet.";
|
|
702
|
+
return ` <article class="issue-card issue-${issue.severity}" aria-labelledby="issue-${escapeHtml(issue.id)}-title">
|
|
703
|
+
<header class="issue-header">
|
|
704
|
+
<div class="issue-badges">
|
|
705
|
+
<span class="badge badge-${issue.severity}">${escapeHtml(issue.severity.toUpperCase())}</span>
|
|
706
|
+
<span class="badge badge-confidence badge-confidence-${issue.confidence}">Confidence: ${escapeHtml(issue.confidence)}</span>
|
|
707
|
+
<span class="badge badge-category">${escapeHtml(categoryLabels[issue.category])}</span>
|
|
708
|
+
</div>
|
|
709
|
+
<h4 id="issue-${escapeHtml(issue.id)}-title" class="issue-title">${escapeHtml(issue.title)}</h4>
|
|
710
|
+
<div class="issue-meta">
|
|
711
|
+
<div class="meta-row"><span class="meta-label">ID</span><code class="meta-value">${escapeHtml(issue.id)}</code></div>
|
|
712
|
+
${fileLine}
|
|
713
|
+
</div>
|
|
714
|
+
</header>
|
|
715
|
+
|
|
716
|
+
<section class="issue-section">
|
|
717
|
+
<h5>What Qodfy found</h5>
|
|
718
|
+
<p>${escapeHtml(issue.message)}</p>
|
|
719
|
+
</section>
|
|
720
|
+
|
|
721
|
+
<section class="issue-section">
|
|
722
|
+
<h5>Why it matters</h5>
|
|
723
|
+
<p>${escapeHtml(getWhyItMatters(issue))}</p>
|
|
724
|
+
</section>
|
|
725
|
+
|
|
726
|
+
<section class="issue-section">
|
|
727
|
+
<h5>Evidence</h5>
|
|
728
|
+
${evidenceHtml}
|
|
729
|
+
</section>
|
|
730
|
+
|
|
731
|
+
<section class="issue-section">
|
|
732
|
+
<h5>Context</h5>
|
|
733
|
+
${contextHtml}
|
|
734
|
+
</section>
|
|
735
|
+
|
|
736
|
+
<section class="issue-section">
|
|
737
|
+
<h5>Suggested fix</h5>
|
|
738
|
+
<p>${escapeHtml(suggestion)}</p>
|
|
739
|
+
</section>
|
|
740
|
+
|
|
741
|
+
<section class="issue-section">
|
|
742
|
+
<h5>AI Fix Prompt</h5>
|
|
743
|
+
<pre class="code-block"><code>${escapeHtml(fixPrompt)}</code></pre>
|
|
744
|
+
</section>
|
|
745
|
+
|
|
746
|
+
<section class="issue-section">
|
|
747
|
+
<h5>After fixing, test this</h5>
|
|
748
|
+
${testsHtml}
|
|
749
|
+
</section>
|
|
750
|
+
</article>`;
|
|
751
|
+
}
|
|
752
|
+
function renderHtmlEvidenceList(items) {
|
|
753
|
+
if (!items || items.length === 0) {
|
|
754
|
+
return `<p class="muted">None.</p>`;
|
|
755
|
+
}
|
|
756
|
+
const listItems = items.map((item) => {
|
|
757
|
+
const detail = item.detail ? `: <code>${escapeHtml(item.detail)}</code>` : "";
|
|
758
|
+
return ` <li><strong>${escapeHtml(item.label)}</strong>${detail}</li>`;
|
|
759
|
+
}).join("\n");
|
|
760
|
+
return `<ul class="evidence-list">
|
|
761
|
+
${listItems}
|
|
762
|
+
</ul>`;
|
|
763
|
+
}
|
|
764
|
+
function getStatusTone(score) {
|
|
765
|
+
if (score >= 90) {
|
|
766
|
+
return "ready";
|
|
767
|
+
}
|
|
768
|
+
if (score >= 75) {
|
|
769
|
+
return "almost";
|
|
770
|
+
}
|
|
771
|
+
if (score >= 50) {
|
|
772
|
+
return "needs-fixes";
|
|
773
|
+
}
|
|
774
|
+
return "not-ready";
|
|
775
|
+
}
|
|
776
|
+
function getHtmlReportStyles() {
|
|
777
|
+
return `
|
|
778
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
779
|
+
html { -webkit-text-size-adjust: 100%; }
|
|
780
|
+
body {
|
|
781
|
+
margin: 0;
|
|
782
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
783
|
+
background: #f6f7fb;
|
|
784
|
+
color: #1f2330;
|
|
785
|
+
line-height: 1.55;
|
|
786
|
+
}
|
|
787
|
+
code, pre {
|
|
788
|
+
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
|
|
789
|
+
}
|
|
790
|
+
.page {
|
|
791
|
+
max-width: 960px;
|
|
792
|
+
margin: 0 auto;
|
|
793
|
+
padding: 32px 20px 64px;
|
|
794
|
+
}
|
|
795
|
+
.eyebrow {
|
|
796
|
+
margin: 0 0 4px;
|
|
797
|
+
font-size: 12px;
|
|
798
|
+
font-weight: 600;
|
|
799
|
+
letter-spacing: 0.12em;
|
|
800
|
+
text-transform: uppercase;
|
|
801
|
+
color: #5b6478;
|
|
802
|
+
}
|
|
803
|
+
h1 {
|
|
804
|
+
margin: 0 0 16px;
|
|
805
|
+
font-size: 28px;
|
|
806
|
+
font-weight: 700;
|
|
807
|
+
letter-spacing: -0.01em;
|
|
808
|
+
}
|
|
809
|
+
h2 {
|
|
810
|
+
margin: 0 0 12px;
|
|
811
|
+
font-size: 20px;
|
|
812
|
+
font-weight: 700;
|
|
813
|
+
letter-spacing: -0.005em;
|
|
814
|
+
}
|
|
815
|
+
h3 {
|
|
816
|
+
margin: 0 0 12px;
|
|
817
|
+
font-size: 16px;
|
|
818
|
+
font-weight: 700;
|
|
819
|
+
}
|
|
820
|
+
h4 {
|
|
821
|
+
margin: 0 0 8px;
|
|
822
|
+
font-size: 16px;
|
|
823
|
+
font-weight: 600;
|
|
824
|
+
}
|
|
825
|
+
h5 {
|
|
826
|
+
margin: 16px 0 6px;
|
|
827
|
+
font-size: 13px;
|
|
828
|
+
font-weight: 700;
|
|
829
|
+
text-transform: uppercase;
|
|
830
|
+
letter-spacing: 0.06em;
|
|
831
|
+
color: #5b6478;
|
|
832
|
+
}
|
|
833
|
+
p { margin: 0 0 12px; }
|
|
834
|
+
p:last-child { margin-bottom: 0; }
|
|
835
|
+
ul, ol { margin: 0 0 12px; padding-left: 22px; }
|
|
836
|
+
ul li, ol li { margin: 4px 0; }
|
|
837
|
+
.muted { color: #6b7384; }
|
|
838
|
+
.hero {
|
|
839
|
+
background: #ffffff;
|
|
840
|
+
border: 1px solid #e6e8ef;
|
|
841
|
+
border-radius: 16px;
|
|
842
|
+
padding: 24px;
|
|
843
|
+
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
|
844
|
+
margin-bottom: 20px;
|
|
845
|
+
display: grid;
|
|
846
|
+
gap: 16px;
|
|
847
|
+
}
|
|
848
|
+
.hero-meta {
|
|
849
|
+
margin: 0;
|
|
850
|
+
display: grid;
|
|
851
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
852
|
+
gap: 12px 24px;
|
|
853
|
+
}
|
|
854
|
+
.hero-meta div { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
855
|
+
.hero-meta dt {
|
|
856
|
+
font-size: 11px;
|
|
857
|
+
font-weight: 600;
|
|
858
|
+
letter-spacing: 0.08em;
|
|
859
|
+
text-transform: uppercase;
|
|
860
|
+
color: #5b6478;
|
|
861
|
+
}
|
|
862
|
+
.hero-meta dd {
|
|
863
|
+
margin: 0;
|
|
864
|
+
font-size: 14px;
|
|
865
|
+
color: #1f2330;
|
|
866
|
+
word-break: break-word;
|
|
867
|
+
}
|
|
868
|
+
.hero-meta code {
|
|
869
|
+
font-size: 12px;
|
|
870
|
+
background: #f1f3f8;
|
|
871
|
+
padding: 2px 6px;
|
|
872
|
+
border-radius: 6px;
|
|
873
|
+
}
|
|
874
|
+
.score-card {
|
|
875
|
+
border-radius: 12px;
|
|
876
|
+
padding: 16px 20px;
|
|
877
|
+
display: flex;
|
|
878
|
+
align-items: center;
|
|
879
|
+
justify-content: space-between;
|
|
880
|
+
gap: 16px;
|
|
881
|
+
border: 1px solid transparent;
|
|
882
|
+
}
|
|
883
|
+
.score-card .score-value {
|
|
884
|
+
font-size: 36px;
|
|
885
|
+
font-weight: 700;
|
|
886
|
+
letter-spacing: -0.02em;
|
|
887
|
+
}
|
|
888
|
+
.score-card .score-max {
|
|
889
|
+
font-size: 18px;
|
|
890
|
+
font-weight: 500;
|
|
891
|
+
color: #5b6478;
|
|
892
|
+
margin-left: 2px;
|
|
893
|
+
}
|
|
894
|
+
.score-card .score-status {
|
|
895
|
+
font-size: 14px;
|
|
896
|
+
font-weight: 600;
|
|
897
|
+
padding: 6px 10px;
|
|
898
|
+
border-radius: 999px;
|
|
899
|
+
background: rgba(255,255,255,0.6);
|
|
900
|
+
}
|
|
901
|
+
.score-ready { background: #ecfdf5; border-color: #a7f3d0; color: #065f46; }
|
|
902
|
+
.score-ready .score-status { background: #d1fae5; color: #065f46; }
|
|
903
|
+
.score-almost { background: #f0f9ff; border-color: #bae6fd; color: #075985; }
|
|
904
|
+
.score-almost .score-status { background: #e0f2fe; color: #075985; }
|
|
905
|
+
.score-needs-fixes { background: #fffbeb; border-color: #fcd34d; color: #92400e; }
|
|
906
|
+
.score-needs-fixes .score-status { background: #fef3c7; color: #92400e; }
|
|
907
|
+
.score-not-ready { background: #fef2f2; border-color: #fecaca; color: #991b1b; }
|
|
908
|
+
.score-not-ready .score-status { background: #fee2e2; color: #991b1b; }
|
|
909
|
+
.stats {
|
|
910
|
+
display: grid;
|
|
911
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
912
|
+
gap: 12px;
|
|
913
|
+
margin-bottom: 20px;
|
|
914
|
+
}
|
|
915
|
+
.stat-card {
|
|
916
|
+
background: #ffffff;
|
|
917
|
+
border: 1px solid #e6e8ef;
|
|
918
|
+
border-radius: 12px;
|
|
919
|
+
padding: 14px 16px;
|
|
920
|
+
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
|
921
|
+
}
|
|
922
|
+
.stat-label {
|
|
923
|
+
font-size: 11px;
|
|
924
|
+
font-weight: 600;
|
|
925
|
+
letter-spacing: 0.08em;
|
|
926
|
+
text-transform: uppercase;
|
|
927
|
+
color: #5b6478;
|
|
928
|
+
}
|
|
929
|
+
.stat-value {
|
|
930
|
+
margin-top: 4px;
|
|
931
|
+
font-size: 22px;
|
|
932
|
+
font-weight: 700;
|
|
933
|
+
letter-spacing: -0.01em;
|
|
934
|
+
}
|
|
935
|
+
.stat-critical .stat-value { color: #b91c1c; }
|
|
936
|
+
.stat-warning .stat-value { color: #b45309; }
|
|
937
|
+
.stat-info .stat-value { color: #1d4ed8; }
|
|
938
|
+
.card {
|
|
939
|
+
background: #ffffff;
|
|
940
|
+
border: 1px solid #e6e8ef;
|
|
941
|
+
border-radius: 12px;
|
|
942
|
+
padding: 20px;
|
|
943
|
+
margin-bottom: 16px;
|
|
944
|
+
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
|
945
|
+
}
|
|
946
|
+
.priority-list, .observation-list { margin-bottom: 0; }
|
|
947
|
+
.severity-section {
|
|
948
|
+
background: #ffffff;
|
|
949
|
+
border: 1px solid #e6e8ef;
|
|
950
|
+
border-radius: 14px;
|
|
951
|
+
padding: 20px;
|
|
952
|
+
margin-bottom: 16px;
|
|
953
|
+
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
|
954
|
+
}
|
|
955
|
+
.severity-heading {
|
|
956
|
+
display: flex;
|
|
957
|
+
align-items: center;
|
|
958
|
+
gap: 10px;
|
|
959
|
+
font-size: 18px;
|
|
960
|
+
margin-bottom: 16px;
|
|
961
|
+
}
|
|
962
|
+
.severity-dot {
|
|
963
|
+
width: 10px;
|
|
964
|
+
height: 10px;
|
|
965
|
+
border-radius: 999px;
|
|
966
|
+
display: inline-block;
|
|
967
|
+
background: #94a3b8;
|
|
968
|
+
}
|
|
969
|
+
.severity-critical .severity-dot { background: #dc2626; }
|
|
970
|
+
.severity-warning .severity-dot { background: #d97706; }
|
|
971
|
+
.severity-info .severity-dot { background: #2563eb; }
|
|
972
|
+
.severity-count { color: #6b7384; font-weight: 500; }
|
|
973
|
+
.category-group { margin-top: 16px; }
|
|
974
|
+
.category-group:first-of-type { margin-top: 0; }
|
|
975
|
+
.category-heading {
|
|
976
|
+
font-size: 12px;
|
|
977
|
+
font-weight: 700;
|
|
978
|
+
letter-spacing: 0.08em;
|
|
979
|
+
text-transform: uppercase;
|
|
980
|
+
color: #5b6478;
|
|
981
|
+
margin-bottom: 10px;
|
|
982
|
+
}
|
|
983
|
+
.issue-card {
|
|
984
|
+
border: 1px solid #e6e8ef;
|
|
985
|
+
border-radius: 12px;
|
|
986
|
+
padding: 18px 20px;
|
|
987
|
+
margin-bottom: 12px;
|
|
988
|
+
background: #fbfbfd;
|
|
989
|
+
border-left-width: 4px;
|
|
990
|
+
}
|
|
991
|
+
.issue-critical { border-left-color: #dc2626; }
|
|
992
|
+
.issue-warning { border-left-color: #d97706; }
|
|
993
|
+
.issue-info { border-left-color: #2563eb; }
|
|
994
|
+
.issue-header { margin-bottom: 8px; }
|
|
995
|
+
.issue-badges {
|
|
996
|
+
display: flex;
|
|
997
|
+
flex-wrap: wrap;
|
|
998
|
+
gap: 6px;
|
|
999
|
+
margin-bottom: 8px;
|
|
1000
|
+
}
|
|
1001
|
+
.badge {
|
|
1002
|
+
display: inline-flex;
|
|
1003
|
+
align-items: center;
|
|
1004
|
+
gap: 6px;
|
|
1005
|
+
font-size: 11px;
|
|
1006
|
+
font-weight: 700;
|
|
1007
|
+
letter-spacing: 0.04em;
|
|
1008
|
+
padding: 3px 8px;
|
|
1009
|
+
border-radius: 999px;
|
|
1010
|
+
background: #eef0f5;
|
|
1011
|
+
color: #1f2330;
|
|
1012
|
+
text-transform: uppercase;
|
|
1013
|
+
}
|
|
1014
|
+
.badge-critical { background: #fee2e2; color: #991b1b; }
|
|
1015
|
+
.badge-warning { background: #fef3c7; color: #92400e; }
|
|
1016
|
+
.badge-info { background: #dbeafe; color: #1e40af; }
|
|
1017
|
+
.badge-confidence {
|
|
1018
|
+
background: #eef0f5;
|
|
1019
|
+
color: #334155;
|
|
1020
|
+
text-transform: none;
|
|
1021
|
+
font-weight: 600;
|
|
1022
|
+
letter-spacing: 0;
|
|
1023
|
+
}
|
|
1024
|
+
.badge-confidence-high { background: #dcfce7; color: #166534; }
|
|
1025
|
+
.badge-confidence-medium { background: #fef3c7; color: #92400e; }
|
|
1026
|
+
.badge-confidence-low { background: #e0e7ff; color: #3730a3; }
|
|
1027
|
+
.badge-category {
|
|
1028
|
+
background: #eef0f5;
|
|
1029
|
+
color: #475569;
|
|
1030
|
+
text-transform: none;
|
|
1031
|
+
font-weight: 600;
|
|
1032
|
+
letter-spacing: 0;
|
|
1033
|
+
}
|
|
1034
|
+
.issue-title {
|
|
1035
|
+
margin: 4px 0 8px;
|
|
1036
|
+
font-size: 16px;
|
|
1037
|
+
font-weight: 700;
|
|
1038
|
+
color: #0f172a;
|
|
1039
|
+
}
|
|
1040
|
+
.issue-meta {
|
|
1041
|
+
display: grid;
|
|
1042
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1043
|
+
gap: 6px 16px;
|
|
1044
|
+
}
|
|
1045
|
+
.meta-row { display: flex; gap: 6px; align-items: baseline; min-width: 0; }
|
|
1046
|
+
.meta-label {
|
|
1047
|
+
font-size: 11px;
|
|
1048
|
+
font-weight: 700;
|
|
1049
|
+
letter-spacing: 0.06em;
|
|
1050
|
+
text-transform: uppercase;
|
|
1051
|
+
color: #5b6478;
|
|
1052
|
+
}
|
|
1053
|
+
.meta-value {
|
|
1054
|
+
font-size: 12px;
|
|
1055
|
+
background: #f1f3f8;
|
|
1056
|
+
padding: 2px 6px;
|
|
1057
|
+
border-radius: 6px;
|
|
1058
|
+
word-break: break-all;
|
|
1059
|
+
}
|
|
1060
|
+
.issue-section { margin-top: 8px; }
|
|
1061
|
+
.issue-section p { margin: 0; }
|
|
1062
|
+
.evidence-list { margin: 0; }
|
|
1063
|
+
.code-block {
|
|
1064
|
+
margin: 0;
|
|
1065
|
+
padding: 12px 14px;
|
|
1066
|
+
background: #0f172a;
|
|
1067
|
+
color: #e2e8f0;
|
|
1068
|
+
border-radius: 10px;
|
|
1069
|
+
overflow: auto;
|
|
1070
|
+
font-size: 12.5px;
|
|
1071
|
+
line-height: 1.55;
|
|
1072
|
+
white-space: pre-wrap;
|
|
1073
|
+
word-break: break-word;
|
|
1074
|
+
}
|
|
1075
|
+
.footer {
|
|
1076
|
+
margin-top: 24px;
|
|
1077
|
+
padding: 20px;
|
|
1078
|
+
border-radius: 12px;
|
|
1079
|
+
background: #ffffff;
|
|
1080
|
+
border: 1px solid #e6e8ef;
|
|
1081
|
+
text-align: center;
|
|
1082
|
+
}
|
|
1083
|
+
@media (max-width: 720px) {
|
|
1084
|
+
.page { padding: 20px 14px 48px; }
|
|
1085
|
+
h1 { font-size: 24px; }
|
|
1086
|
+
.hero-meta, .stats, .issue-meta { grid-template-columns: 1fr; }
|
|
1087
|
+
.stats { gap: 8px; }
|
|
1088
|
+
.score-card { flex-direction: column; align-items: flex-start; }
|
|
1089
|
+
}
|
|
1090
|
+
`;
|
|
1091
|
+
}
|
|
543
1092
|
function appendMarkdownIssue(lines, issue) {
|
|
544
1093
|
lines.push(`### ${issue.title}`);
|
|
545
1094
|
lines.push("");
|