openpitstop 1.0.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 +21 -0
- package/README.md +512 -0
- package/demo-repo/README.md +24 -0
- package/demo-repo/package-lock.json +4483 -0
- package/demo-repo/package.json +17 -0
- package/demo-repo/src/config.js +5 -0
- package/demo-repo/src/db.js +31 -0
- package/demo-repo/src/server.js +13 -0
- package/demo-repo/src/userController.js +22 -0
- package/demo-repo/src/userRepo.js +18 -0
- package/demo-repo/src/userService.js +59 -0
- package/demo-repo/test/user.test.js +12 -0
- package/demo-repo-fintech/README.md +27 -0
- package/demo-repo-fintech/package-lock.json +1023 -0
- package/demo-repo-fintech/package.json +14 -0
- package/demo-repo-fintech/pitstop-repro-ledger-order-ord-conc-1-charged-2.test.mjs +175 -0
- package/demo-repo-fintech/pitstop-repro-ledger-order-ord-retry-2-charged-2.test.mjs +175 -0
- package/demo-repo-fintech/pitstop-repro-ledger-order-ord-webhook-3-charged-2.test.mjs +175 -0
- package/demo-repo-fintech/src/gateway.js +26 -0
- package/demo-repo-fintech/src/server.js +61 -0
- package/demo-repo-generators/package-lock.json +1508 -0
- package/demo-repo-generators/package.json +17 -0
- package/demo-repo-generators/public/index.html +10 -0
- package/demo-repo-generators/scripts/build.js +17 -0
- package/demo-repo-generators/src/counter.js +16 -0
- package/demo-repo-integrity/README.md +49 -0
- package/demo-repo-integrity/package.json +12 -0
- package/demo-repo-integrity/src/money.js +19 -0
- package/demo-repo-integrity/test/money.test.js +17 -0
- package/dist/analyzers/accessibility.d.ts +18 -0
- package/dist/analyzers/accessibility.js +248 -0
- package/dist/analyzers/accessibility.js.map +1 -0
- package/dist/analyzers/dependencyGraph.d.ts +14 -0
- package/dist/analyzers/dependencyGraph.js +206 -0
- package/dist/analyzers/dependencyGraph.js.map +1 -0
- package/dist/analyzers/devex.d.ts +19 -0
- package/dist/analyzers/devex.js +301 -0
- package/dist/analyzers/devex.js.map +1 -0
- package/dist/analyzers/duplication.d.ts +2 -0
- package/dist/analyzers/duplication.js +47 -0
- package/dist/analyzers/duplication.js.map +1 -0
- package/dist/analyzers/index.d.ts +12 -0
- package/dist/analyzers/index.js +38 -0
- package/dist/analyzers/index.js.map +1 -0
- package/dist/analyzers/integrity/assertionLiteralTamper.d.ts +2 -0
- package/dist/analyzers/integrity/assertionLiteralTamper.js +164 -0
- package/dist/analyzers/integrity/assertionLiteralTamper.js.map +1 -0
- package/dist/analyzers/integrity/exceptionSwallow.d.ts +2 -0
- package/dist/analyzers/integrity/exceptionSwallow.js +198 -0
- package/dist/analyzers/integrity/exceptionSwallow.js.map +1 -0
- package/dist/analyzers/integrity/exitCheat.d.ts +2 -0
- package/dist/analyzers/integrity/exitCheat.js +31 -0
- package/dist/analyzers/integrity/exitCheat.js.map +1 -0
- package/dist/analyzers/integrity/git.d.ts +10 -0
- package/dist/analyzers/integrity/git.js +181 -0
- package/dist/analyzers/integrity/git.js.map +1 -0
- package/dist/analyzers/integrity/hardcodedMatch.d.ts +2 -0
- package/dist/analyzers/integrity/hardcodedMatch.js +93 -0
- package/dist/analyzers/integrity/hardcodedMatch.js.map +1 -0
- package/dist/analyzers/integrity/helpers.d.ts +21 -0
- package/dist/analyzers/integrity/helpers.js +66 -0
- package/dist/analyzers/integrity/helpers.js.map +1 -0
- package/dist/analyzers/integrity/index.d.ts +10 -0
- package/dist/analyzers/integrity/index.js +27 -0
- package/dist/analyzers/integrity/index.js.map +1 -0
- package/dist/analyzers/integrity/mockOverreach.d.ts +2 -0
- package/dist/analyzers/integrity/mockOverreach.js +72 -0
- package/dist/analyzers/integrity/mockOverreach.js.map +1 -0
- package/dist/analyzers/integrity/suppressionCreep.d.ts +2 -0
- package/dist/analyzers/integrity/suppressionCreep.js +27 -0
- package/dist/analyzers/integrity/suppressionCreep.js.map +1 -0
- package/dist/analyzers/integrity/testTamper.d.ts +2 -0
- package/dist/analyzers/integrity/testTamper.js +153 -0
- package/dist/analyzers/integrity/testTamper.js.map +1 -0
- package/dist/analyzers/integrity/types.d.ts +63 -0
- package/dist/analyzers/integrity/types.js +13 -0
- package/dist/analyzers/integrity/types.js.map +1 -0
- package/dist/analyzers/ledger/attacks.d.ts +17 -0
- package/dist/analyzers/ledger/attacks.js +135 -0
- package/dist/analyzers/ledger/attacks.js.map +1 -0
- package/dist/analyzers/ledger/discover.d.ts +15 -0
- package/dist/analyzers/ledger/discover.js +122 -0
- package/dist/analyzers/ledger/discover.js.map +1 -0
- package/dist/analyzers/ledger/evidence.d.ts +22 -0
- package/dist/analyzers/ledger/evidence.js +127 -0
- package/dist/analyzers/ledger/evidence.js.map +1 -0
- package/dist/analyzers/ledger/harness.d.ts +4 -0
- package/dist/analyzers/ledger/harness.js +270 -0
- package/dist/analyzers/ledger/harness.js.map +1 -0
- package/dist/analyzers/ledger/index.d.ts +14 -0
- package/dist/analyzers/ledger/index.js +116 -0
- package/dist/analyzers/ledger/index.js.map +1 -0
- package/dist/analyzers/ledger/types.d.ts +31 -0
- package/dist/analyzers/ledger/types.js +2 -0
- package/dist/analyzers/ledger/types.js.map +1 -0
- package/dist/analyzers/perf.d.ts +2 -0
- package/dist/analyzers/perf.js +106 -0
- package/dist/analyzers/perf.js.map +1 -0
- package/dist/analyzers/reliability.d.ts +22 -0
- package/dist/analyzers/reliability.js +339 -0
- package/dist/analyzers/reliability.js.map +1 -0
- package/dist/analyzers/routes.d.ts +17 -0
- package/dist/analyzers/routes.js +115 -0
- package/dist/analyzers/routes.js.map +1 -0
- package/dist/analyzers/security.d.ts +2 -0
- package/dist/analyzers/security.js +302 -0
- package/dist/analyzers/security.js.map +1 -0
- package/dist/analyzers/suiteRunner.d.ts +64 -0
- package/dist/analyzers/suiteRunner.js +381 -0
- package/dist/analyzers/suiteRunner.js.map +1 -0
- package/dist/analyzers/tests.d.ts +2 -0
- package/dist/analyzers/tests.js +215 -0
- package/dist/analyzers/tests.js.map +1 -0
- package/dist/analyzers/types.d.ts +203 -0
- package/dist/analyzers/types.js +2 -0
- package/dist/analyzers/types.js.map +1 -0
- package/dist/analyzers/util.d.ts +35 -0
- package/dist/analyzers/util.js +209 -0
- package/dist/analyzers/util.js.map +1 -0
- package/dist/ciCheckA.d.ts +2 -0
- package/dist/ciCheckA.js +5 -0
- package/dist/ciCheckA.js.map +1 -0
- package/dist/ciCheckB.d.ts +2 -0
- package/dist/ciCheckB.js +6 -0
- package/dist/ciCheckB.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +65 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/budget.d.ts +33 -0
- package/dist/commands/budget.js +108 -0
- package/dist/commands/budget.js.map +1 -0
- package/dist/commands/ci.d.ts +14 -0
- package/dist/commands/ci.js +157 -0
- package/dist/commands/ci.js.map +1 -0
- package/dist/commands/demo.d.ts +2 -0
- package/dist/commands/demo.js +163 -0
- package/dist/commands/demo.js.map +1 -0
- package/dist/commands/digest.d.ts +8 -0
- package/dist/commands/digest.js +189 -0
- package/dist/commands/digest.js.map +1 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +98 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/drive.d.ts +3 -0
- package/dist/commands/drive.js +170 -0
- package/dist/commands/drive.js.map +1 -0
- package/dist/commands/gate.d.ts +2 -0
- package/dist/commands/gate.js +136 -0
- package/dist/commands/gate.js.map +1 -0
- package/dist/commands/honesty.d.ts +2 -0
- package/dist/commands/honesty.js +182 -0
- package/dist/commands/honesty.js.map +1 -0
- package/dist/commands/inspect.d.ts +2 -0
- package/dist/commands/inspect.js +244 -0
- package/dist/commands/inspect.js.map +1 -0
- package/dist/commands/install.d.ts +2 -0
- package/dist/commands/install.js +81 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/integrity.d.ts +2 -0
- package/dist/commands/integrity.js +74 -0
- package/dist/commands/integrity.js.map +1 -0
- package/dist/commands/memory.d.ts +6 -0
- package/dist/commands/memory.js +90 -0
- package/dist/commands/memory.js.map +1 -0
- package/dist/commands/pen.d.ts +17 -0
- package/dist/commands/pen.js +158 -0
- package/dist/commands/pen.js.map +1 -0
- package/dist/commands/prompt.d.ts +2 -0
- package/dist/commands/prompt.js +37 -0
- package/dist/commands/prompt.js.map +1 -0
- package/dist/commands/readyCheck.d.ts +21 -0
- package/dist/commands/readyCheck.js +68 -0
- package/dist/commands/readyCheck.js.map +1 -0
- package/dist/commands/report.d.ts +2 -0
- package/dist/commands/report.js +48 -0
- package/dist/commands/report.js.map +1 -0
- package/dist/commands/repro.d.ts +3 -0
- package/dist/commands/repro.js +61 -0
- package/dist/commands/repro.js.map +1 -0
- package/dist/commands/scan.d.ts +27 -0
- package/dist/commands/scan.js +348 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/commands/share.d.ts +2 -0
- package/dist/commands/share.js +163 -0
- package/dist/commands/share.js.map +1 -0
- package/dist/commands/trends.d.ts +2 -0
- package/dist/commands/trends.js +103 -0
- package/dist/commands/trends.js.map +1 -0
- package/dist/commands/try.d.ts +12 -0
- package/dist/commands/try.js +208 -0
- package/dist/commands/try.js.map +1 -0
- package/dist/commands/verify.d.ts +44 -0
- package/dist/commands/verify.js +381 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/commands/watch.d.ts +10 -0
- package/dist/commands/watch.js +70 -0
- package/dist/commands/watch.js.map +1 -0
- package/dist/evidence.d.ts +37 -0
- package/dist/evidence.js +61 -0
- package/dist/evidence.js.map +1 -0
- package/dist/graph/correlate.d.ts +7 -0
- package/dist/graph/correlate.js +246 -0
- package/dist/graph/correlate.js.map +1 -0
- package/dist/graph/integrity.d.ts +22 -0
- package/dist/graph/integrity.js +39 -0
- package/dist/graph/integrity.js.map +1 -0
- package/dist/installer/targets.d.ts +23 -0
- package/dist/installer/targets.js +162 -0
- package/dist/installer/targets.js.map +1 -0
- package/dist/memory/store.d.ts +33 -0
- package/dist/memory/store.js +131 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/pen/dynamic.d.ts +36 -0
- package/dist/pen/dynamic.js +622 -0
- package/dist/pen/dynamic.js.map +1 -0
- package/dist/pen/fix.d.ts +35 -0
- package/dist/pen/fix.js +239 -0
- package/dist/pen/fix.js.map +1 -0
- package/dist/pen/proof.d.ts +46 -0
- package/dist/pen/proof.js +112 -0
- package/dist/pen/proof.js.map +1 -0
- package/dist/pen/report.d.ts +11 -0
- package/dist/pen/report.js +258 -0
- package/dist/pen/report.js.map +1 -0
- package/dist/pen/static.d.ts +17 -0
- package/dist/pen/static.js +565 -0
- package/dist/pen/static.js.map +1 -0
- package/dist/pen/store.d.ts +11 -0
- package/dist/pen/store.js +36 -0
- package/dist/pen/store.js.map +1 -0
- package/dist/pen/types.d.ts +112 -0
- package/dist/pen/types.js +53 -0
- package/dist/pen/types.js.map +1 -0
- package/dist/report/format.d.ts +126 -0
- package/dist/report/format.js +706 -0
- package/dist/report/format.js.map +1 -0
- package/dist/report/score.d.ts +47 -0
- package/dist/report/score.js +164 -0
- package/dist/report/score.js.map +1 -0
- package/dist/repro/framework.d.ts +26 -0
- package/dist/repro/framework.js +224 -0
- package/dist/repro/framework.js.map +1 -0
- package/dist/repro/generate.d.ts +14 -0
- package/dist/repro/generate.js +669 -0
- package/dist/repro/generate.js.map +1 -0
- package/dist/repro/ids.d.ts +32 -0
- package/dist/repro/ids.js +175 -0
- package/dist/repro/ids.js.map +1 -0
- package/dist/repro/index.d.ts +24 -0
- package/dist/repro/index.js +69 -0
- package/dist/repro/index.js.map +1 -0
- package/dist/repro/pen.d.ts +26 -0
- package/dist/repro/pen.js +321 -0
- package/dist/repro/pen.js.map +1 -0
- package/dist/repro/run.d.ts +18 -0
- package/dist/repro/run.js +36 -0
- package/dist/repro/run.js.map +1 -0
- package/dist/sandbox/proxy.d.ts +41 -0
- package/dist/sandbox/proxy.js +231 -0
- package/dist/sandbox/proxy.js.map +1 -0
- package/dist/sandbox/startCmd.d.ts +24 -0
- package/dist/sandbox/startCmd.js +71 -0
- package/dist/sandbox/startCmd.js.map +1 -0
- package/dist/ui/spinner.d.ts +18 -0
- package/dist/ui/spinner.js +26 -0
- package/dist/ui/spinner.js.map +1 -0
- package/dist/verify/metrics.d.ts +13 -0
- package/dist/verify/metrics.js +72 -0
- package/dist/verify/metrics.js.map +1 -0
- package/package.json +68 -0
- package/scripts/cheat-demo.cjs +102 -0
- package/templates/ledger/preload.cjs +331 -0
- package/templates/pen/preload.cjs +196 -0
- package/templates/pitstop.prompt.md +371 -0
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import boxen from "boxen";
|
|
5
|
+
import { computeScore, gradeColor, gradeHex, gradeOf, renderBadgeSvg } from "./score.js";
|
|
6
|
+
export function loadHistory(repo) {
|
|
7
|
+
const dir = path.join(repo, ".pitstop");
|
|
8
|
+
const read = (prefix) => {
|
|
9
|
+
if (!fs.existsSync(dir))
|
|
10
|
+
return [];
|
|
11
|
+
return fs
|
|
12
|
+
.readdirSync(dir)
|
|
13
|
+
.filter((f) => f.startsWith(prefix) && f.endsWith(".json"))
|
|
14
|
+
.map((f) => {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(fs.readFileSync(path.join(dir, f), "utf8"));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
.filter(Boolean);
|
|
23
|
+
};
|
|
24
|
+
const scans = read("scan-");
|
|
25
|
+
const verifies = read("verify-");
|
|
26
|
+
scans.sort((a, b) => (a.timestamp < b.timestamp ? -1 : 1));
|
|
27
|
+
verifies.sort((a, b) => (a.timestamp < b.timestamp ? -1 : 1));
|
|
28
|
+
return { repo, scans, verifies };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Gather every Phase 16 integrity verdict recorded by the loop (from verify-*.json
|
|
32
|
+
* and integrity-*.json in `.pitstop/`) and summarize catches vs self-corrections.
|
|
33
|
+
*/
|
|
34
|
+
export function collectIntegrity(repo) {
|
|
35
|
+
const dir = path.join(repo, ".pitstop");
|
|
36
|
+
if (!fs.existsSync(dir))
|
|
37
|
+
return undefined;
|
|
38
|
+
const events = [];
|
|
39
|
+
for (const f of fs.readdirSync(dir)) {
|
|
40
|
+
if (!f.endsWith(".json"))
|
|
41
|
+
continue;
|
|
42
|
+
if (!(f.startsWith("verify-") || f.startsWith("integrity-")))
|
|
43
|
+
continue;
|
|
44
|
+
try {
|
|
45
|
+
const j = JSON.parse(fs.readFileSync(path.join(dir, f), "utf8"));
|
|
46
|
+
const verdict = j.integrity?.verdict ?? j.verdict;
|
|
47
|
+
const findings = j.integrity?.findings ?? j.findings ?? [];
|
|
48
|
+
if (verdict)
|
|
49
|
+
events.push({ timestamp: j.timestamp ?? "", verdict, findings });
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
/* ignore */
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (events.length === 0)
|
|
56
|
+
return undefined;
|
|
57
|
+
events.sort((a, b) => (a.timestamp < b.timestamp ? -1 : 1));
|
|
58
|
+
const catches = events.filter((e) => e.verdict !== "CLEAN");
|
|
59
|
+
const selfCorrected = catches.filter((c) => events.some((e) => e.timestamp > c.timestamp && e.verdict === "CLEAN")).length;
|
|
60
|
+
return { checks: events.length, catches: catches.length, selfCorrected, events };
|
|
61
|
+
}
|
|
62
|
+
const REPRO_TEST_RE = /^pitstop-repro-.+\.test\.(mjs|js|ts|py)$/;
|
|
63
|
+
const REPRO_ID_RE = /\b(?:pitstop repro|finding)[^\w-]*([A-Za-z][A-Za-z0-9]*-[0-9a-f]{6,})/i;
|
|
64
|
+
/** Scan the repo for committed permanent proof files. */
|
|
65
|
+
export function loadProofs(repo) {
|
|
66
|
+
const out = [];
|
|
67
|
+
const stack = [repo];
|
|
68
|
+
const ignore = new Set(["node_modules", ".git", ".pitstop", "dist", "coverage", ".next"]);
|
|
69
|
+
while (stack.length) {
|
|
70
|
+
const dir = stack.pop();
|
|
71
|
+
let entries;
|
|
72
|
+
try {
|
|
73
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
for (const e of entries) {
|
|
79
|
+
if (e.isDirectory()) {
|
|
80
|
+
if (!ignore.has(e.name) && !e.name.startsWith("."))
|
|
81
|
+
stack.push(path.join(dir, e.name));
|
|
82
|
+
}
|
|
83
|
+
else if (e.isFile() && REPRO_TEST_RE.test(e.name)) {
|
|
84
|
+
const abs = path.join(dir, e.name);
|
|
85
|
+
let findingId;
|
|
86
|
+
try {
|
|
87
|
+
const head = fs.readFileSync(abs, "utf8").slice(0, 1200);
|
|
88
|
+
const m = head.match(REPRO_ID_RE);
|
|
89
|
+
if (m && m[1])
|
|
90
|
+
findingId = m[1];
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
/* ignore */
|
|
94
|
+
}
|
|
95
|
+
out.push({ file: path.relative(repo, abs), findingId });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
out.sort((a, b) => (a.file < b.file ? -1 : 1));
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
const NOT_SCANNED = "not scanned";
|
|
103
|
+
const NOT_ASSESSED = "not assessed";
|
|
104
|
+
function bundleKB(b) {
|
|
105
|
+
return b == null ? NOT_SCANNED : `${(b / 1024).toFixed(1)} KB`;
|
|
106
|
+
}
|
|
107
|
+
function ms(n) {
|
|
108
|
+
return n == null ? NOT_SCANNED : `${n} ms`;
|
|
109
|
+
}
|
|
110
|
+
export function buildModel(repo) {
|
|
111
|
+
const hist = loadHistory(repo);
|
|
112
|
+
const latestScan = hist.scans[hist.scans.length - 1] ?? null;
|
|
113
|
+
const latestVerify = hist.verifies[hist.verifies.length - 1] ?? null;
|
|
114
|
+
const integrity = collectIntegrity(repo);
|
|
115
|
+
const lines = [];
|
|
116
|
+
// OpenPitStop Score — the headline number, computed from the latest scan.
|
|
117
|
+
if (!latestScan) {
|
|
118
|
+
lines.push({ label: "OpenPitStop Score", value: NOT_SCANNED, color: "dim" });
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
const sc = computeScore(latestScan);
|
|
122
|
+
const analyzed = sc.analyzed < sc.total ? ` — ${sc.analyzed}/${sc.total} categories` : "";
|
|
123
|
+
lines.push({
|
|
124
|
+
label: "OpenPitStop Score",
|
|
125
|
+
value: `${sc.score}/100 (${sc.grade})${analyzed}`,
|
|
126
|
+
color: gradeColor(sc.grade),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
// Critical Issues Fixed — derived from improvements in the latest verify.
|
|
130
|
+
if (!latestVerify) {
|
|
131
|
+
lines.push({ label: "Critical Issues Fixed", value: NOT_ASSESSED, color: "dim" });
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
const d = latestVerify.deltas;
|
|
135
|
+
let fixed = 0;
|
|
136
|
+
if (d.security < 0)
|
|
137
|
+
fixed += -d.security;
|
|
138
|
+
if (d.duplication < 0)
|
|
139
|
+
fixed += -d.duplication;
|
|
140
|
+
if (d.failed < 0)
|
|
141
|
+
fixed += -d.failed;
|
|
142
|
+
lines.push({ label: "Critical Issues Fixed", value: String(fixed) });
|
|
143
|
+
}
|
|
144
|
+
// Security Vulnerabilities
|
|
145
|
+
if (!latestScan || latestScan.security.status !== "ok") {
|
|
146
|
+
lines.push({ label: "Security Vulnerabilities", value: NOT_SCANNED, color: "dim" });
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
const issues = latestScan.security.issues;
|
|
150
|
+
const bySev = {};
|
|
151
|
+
for (const i of issues)
|
|
152
|
+
bySev[i.severity] = (bySev[i.severity] ?? 0) + 1;
|
|
153
|
+
const crit = bySev.critical ?? 0;
|
|
154
|
+
const high = bySev.high ?? 0;
|
|
155
|
+
const color = issues.length > 0 ? (crit > 0 ? "red" : "yellow") : "green";
|
|
156
|
+
const detail = issues.length === 0 ? "" : ` (crit ${crit} · high ${high})`;
|
|
157
|
+
lines.push({
|
|
158
|
+
label: "Security Vulnerabilities",
|
|
159
|
+
value: `${issues.length}${detail}`,
|
|
160
|
+
color,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
// Memory Leaks — no analyzer exists yet.
|
|
164
|
+
lines.push({ label: "Memory Leaks", value: NOT_SCANNED, color: "dim" });
|
|
165
|
+
// Broken Tests
|
|
166
|
+
if (!latestScan || latestScan.tests.status !== "ok") {
|
|
167
|
+
lines.push({ label: "Broken Tests", value: NOT_SCANNED, color: "dim" });
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const t = latestScan.tests;
|
|
171
|
+
const color = t.failed > 0 ? "red" : "green";
|
|
172
|
+
lines.push({
|
|
173
|
+
label: "Broken Tests",
|
|
174
|
+
value: `${t.failed} failed / ${t.total} total`,
|
|
175
|
+
color,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
// Reliability (flaky tests + race-condition heuristics)
|
|
179
|
+
if (!latestScan || latestScan.reliability?.status !== "ok") {
|
|
180
|
+
lines.push({ label: "Reliability", value: NOT_SCANNED, color: "dim" });
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
const rel = latestScan.reliability;
|
|
184
|
+
const dirty = rel.flakyTests.length > 0 || rel.raceSmells.length > 0;
|
|
185
|
+
lines.push({
|
|
186
|
+
label: "Reliability",
|
|
187
|
+
value: `${rel.flakyTests.length} flaky · ${rel.raceSmells.length} race smell(s) · ${rel.runs} runs`,
|
|
188
|
+
color: dirty ? "yellow" : "green",
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
// Accessibility
|
|
192
|
+
if (!latestScan || latestScan.accessibility?.status !== "ok") {
|
|
193
|
+
lines.push({ label: "Accessibility", value: NOT_SCANNED, color: "dim" });
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
const a = latestScan.accessibility;
|
|
197
|
+
const dirty = a.issues.length > 0;
|
|
198
|
+
lines.push({
|
|
199
|
+
label: "Accessibility",
|
|
200
|
+
value: `${a.issues.length} issues${a.engine ? ` · ${a.engine}` : ""}`,
|
|
201
|
+
color: dirty ? "yellow" : "green",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
// Performance (with deltas from verify when available)
|
|
205
|
+
if (!latestScan || latestScan.perf.status !== "ok") {
|
|
206
|
+
lines.push({ label: "Performance", value: NOT_SCANNED, color: "dim" });
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
const p = latestScan.perf;
|
|
210
|
+
const parts = [`build ${ms(p.buildTimeMs)}`, `bundle ${bundleKB(p.bundleSizeBytes)}`];
|
|
211
|
+
if (latestVerify) {
|
|
212
|
+
const d = latestVerify.deltas;
|
|
213
|
+
if (p.buildTimeMs != null && d.buildTimeMs !== 0) {
|
|
214
|
+
const good = d.buildTimeMs < 0;
|
|
215
|
+
parts[0] += ` (${good ? "" : "+"}${d.buildTimeMs}ms)`;
|
|
216
|
+
}
|
|
217
|
+
if (p.bundleSizeBytes != null && d.bundleSizeBytes !== 0) {
|
|
218
|
+
const good = d.bundleSizeBytes < 0;
|
|
219
|
+
parts[1] += ` (${good ? "-" : "+"}${(Math.abs(d.bundleSizeBytes) / 1024).toFixed(1)}KB)`;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
lines.push({ label: "Performance", value: parts.join(" · "), color: "green" });
|
|
223
|
+
}
|
|
224
|
+
// Devex (unused exports / duplicate functions)
|
|
225
|
+
if (!latestScan || latestScan.devex?.status !== "ok") {
|
|
226
|
+
lines.push({ label: "Devex", value: NOT_SCANNED, color: "dim" });
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
const dx = latestScan.devex;
|
|
230
|
+
const dirty = dx.unusedExports.length > 0 || dx.duplicateFunctions.length > 0;
|
|
231
|
+
lines.push({
|
|
232
|
+
label: "Devex",
|
|
233
|
+
value: `${dx.unusedExports.length} unused export(s) · ${dx.duplicateFunctions.length} dup function(s)`,
|
|
234
|
+
color: dirty ? "yellow" : "green",
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
// Technical Debt
|
|
238
|
+
if (!latestScan || latestScan.dependencyGraph.status !== "ok") {
|
|
239
|
+
lines.push({ label: "Technical Debt", value: NOT_SCANNED, color: "dim" });
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
const dg = latestScan.dependencyGraph;
|
|
243
|
+
const clones = latestScan.duplication.status === "ok"
|
|
244
|
+
? String(latestScan.duplication.cloneCount)
|
|
245
|
+
: NOT_SCANNED;
|
|
246
|
+
lines.push({
|
|
247
|
+
label: "Technical Debt",
|
|
248
|
+
value: `${dg.circular.length} circular · ${clones} clones · ${dg.orphans.length} orphans`,
|
|
249
|
+
color: dg.circular.length > 0 ? "yellow" : "green",
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
// Regression Risk
|
|
253
|
+
if (!latestVerify) {
|
|
254
|
+
lines.push({ label: "Regression Risk", value: NOT_ASSESSED, color: "dim" });
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
const c = latestVerify.risk === "High" ? "red" : latestVerify.risk === "Medium" ? "yellow" : "green";
|
|
258
|
+
lines.push({ label: "Regression Risk", value: latestVerify.risk, color: c });
|
|
259
|
+
}
|
|
260
|
+
// Integrity (Phase 16 gate) — aggregated from every verify/integrity run.
|
|
261
|
+
if (!integrity || integrity.checks === 0) {
|
|
262
|
+
lines.push({ label: "Integrity", value: NOT_ASSESSED, color: "dim" });
|
|
263
|
+
}
|
|
264
|
+
else if (integrity.catches === 0) {
|
|
265
|
+
lines.push({ label: "Integrity", value: "clean", color: "green" });
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
const anyConfirmed = integrity.events.some((e) => e.verdict === "CONFIRMED_CHEAT");
|
|
269
|
+
lines.push({
|
|
270
|
+
label: "Integrity",
|
|
271
|
+
value: `${integrity.catches} catch(es) · ${integrity.selfCorrected} self-corrected`,
|
|
272
|
+
color: anyConfirmed ? "red" : "yellow",
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
repo,
|
|
277
|
+
generatedAt: new Date().toISOString(),
|
|
278
|
+
latestScan,
|
|
279
|
+
latestVerify,
|
|
280
|
+
scansCount: hist.scans.length,
|
|
281
|
+
verifiesCount: hist.verifies.length,
|
|
282
|
+
lines,
|
|
283
|
+
clusters: latestScan?.clusters ?? [],
|
|
284
|
+
hasVerify: !!latestVerify,
|
|
285
|
+
proofs: loadProofs(repo),
|
|
286
|
+
integrity,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
export function renderTerminal(model) {
|
|
290
|
+
if (model.scansCount === 0) {
|
|
291
|
+
return boxen(chalk.yellow("No scan history found.\nRun `pitstop scan` to generate a report."), {
|
|
292
|
+
title: " PITSTOP — Repository Analysis Complete ",
|
|
293
|
+
titleAlignment: "center",
|
|
294
|
+
borderStyle: "double",
|
|
295
|
+
padding: 1,
|
|
296
|
+
borderColor: "yellow",
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
const lines = model.lines.map((l) => {
|
|
300
|
+
const colorFn = l.color === "green"
|
|
301
|
+
? chalk.green
|
|
302
|
+
: l.color === "yellow"
|
|
303
|
+
? chalk.yellow
|
|
304
|
+
: l.color === "red"
|
|
305
|
+
? chalk.red
|
|
306
|
+
: chalk.dim;
|
|
307
|
+
return ` ${chalk.bold(l.label.padEnd(24))}: ${colorFn(l.value)}`;
|
|
308
|
+
});
|
|
309
|
+
const proofLine = model.proofs.length > 0
|
|
310
|
+
? ` ${chalk.bold("Permanent proof".padEnd(24))}: ${chalk.green(`${model.proofs.length} repro test(s) shipped with fixes`)}`
|
|
311
|
+
: ` ${chalk.bold("Permanent proof".padEnd(24))}: ${chalk.yellow("none — no pitstop-repro-*.test.* committed")}`;
|
|
312
|
+
const header = `${chalk.dim(`repo: ${model.repo}`)} · ${chalk.dim(`${model.scansCount} scan(s), ${model.verifiesCount} verify(ies)`)}`;
|
|
313
|
+
const contentParts = [header, "", ...lines, proofLine];
|
|
314
|
+
const integrity = integrityTerminalBlock(model);
|
|
315
|
+
if (integrity)
|
|
316
|
+
contentParts.push("", integrity);
|
|
317
|
+
const content = contentParts.join("\n");
|
|
318
|
+
return boxen(content, {
|
|
319
|
+
title: " PITSTOP — Repository Analysis Complete ",
|
|
320
|
+
titleAlignment: "center",
|
|
321
|
+
borderStyle: "double",
|
|
322
|
+
padding: 1,
|
|
323
|
+
borderColor: "cyan",
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
function deltaCell(n, unit, higherIsBetter, isPct = false) {
|
|
327
|
+
if (n === 0)
|
|
328
|
+
return "0";
|
|
329
|
+
const good = higherIsBetter ? n > 0 : n < 0;
|
|
330
|
+
const sign = n > 0 ? "+" : "";
|
|
331
|
+
const val = isPct ? `${sign}${n.toFixed(1)}%` : unit === "KB" ? `${sign}${(n / 1024).toFixed(1)} KB` : `${sign}${n}${unit}`;
|
|
332
|
+
return good ? chalk.green(val) : chalk.red(val);
|
|
333
|
+
}
|
|
334
|
+
function summarySection(lines) {
|
|
335
|
+
const out = [];
|
|
336
|
+
out.push(`## Summary`);
|
|
337
|
+
out.push("");
|
|
338
|
+
out.push(`| Metric | Value |`);
|
|
339
|
+
out.push(`| --- | --- |`);
|
|
340
|
+
for (const l of lines)
|
|
341
|
+
out.push(`| ${l.label} | ${l.value} |`);
|
|
342
|
+
out.push("");
|
|
343
|
+
return out;
|
|
344
|
+
}
|
|
345
|
+
/** Root cause → symptoms cluster framing, mirroring the interactive scan box. */
|
|
346
|
+
function clustersSection(clusters) {
|
|
347
|
+
const out = [];
|
|
348
|
+
out.push(`## Root-Cause Clusters`);
|
|
349
|
+
out.push("");
|
|
350
|
+
if (clusters.length === 0) {
|
|
351
|
+
out.push(`No root-cause clusters identified.`);
|
|
352
|
+
out.push("");
|
|
353
|
+
return out;
|
|
354
|
+
}
|
|
355
|
+
clusters.forEach((c, i) => {
|
|
356
|
+
const sev = c.rootCause.severity.toUpperCase();
|
|
357
|
+
out.push(`${i + 1}. **Root cause** (${sev} ${c.rootCause.type}): ${c.rootCause.description}`);
|
|
358
|
+
out.push(` → ${c.symptoms.length} symptom(s) · shared: \`${c.sharedFiles.join("`, `")}\``);
|
|
359
|
+
for (const s of c.symptoms) {
|
|
360
|
+
out.push(` - (${s.severity.toUpperCase()} ${s.type}): ${s.description}`);
|
|
361
|
+
}
|
|
362
|
+
if (i < clusters.length - 1)
|
|
363
|
+
out.push("");
|
|
364
|
+
});
|
|
365
|
+
out.push("");
|
|
366
|
+
return out;
|
|
367
|
+
}
|
|
368
|
+
function beforeAfterSection(model) {
|
|
369
|
+
const out = [];
|
|
370
|
+
out.push(`## Before / After`);
|
|
371
|
+
out.push("");
|
|
372
|
+
if (!model.hasVerify || !model.latestVerify) {
|
|
373
|
+
out.push(`No verify history — run \`pitstop verify\` (or \`pitstop ci\` in a PR) to populate the before/after diff.`);
|
|
374
|
+
out.push("");
|
|
375
|
+
return out;
|
|
376
|
+
}
|
|
377
|
+
const v = model.latestVerify;
|
|
378
|
+
const b = v.baseline;
|
|
379
|
+
const cur = v.current;
|
|
380
|
+
const d = v.deltas;
|
|
381
|
+
out.push(`Latest verify risk: **${v.risk}** (baseline ${v.baselineTimestamp ?? "unknown"})`);
|
|
382
|
+
out.push("");
|
|
383
|
+
out.push(`| Metric | Baseline | Current | Δ |`);
|
|
384
|
+
out.push(`| --- | --- | --- | --- |`);
|
|
385
|
+
const row = (name, base, curr, deltaStr) => {
|
|
386
|
+
out.push(`| ${name} | ${base} | ${curr} | ${deltaStr} |`);
|
|
387
|
+
};
|
|
388
|
+
row("Tests passed", String(b.tests.passed), String(cur.tests.passed), deltaCell(d.passed, "", true));
|
|
389
|
+
row("Tests failed", String(b.tests.failed), String(cur.tests.failed), deltaCell(d.failed, "", false));
|
|
390
|
+
row("Duration", ms(b.tests.durationMs), ms(cur.tests.durationMs), deltaCell(d.durationMs, "ms", false));
|
|
391
|
+
row("Coverage", b.tests.coverage != null ? `${b.tests.coverage.toFixed(1)}%` : "—", cur.tests.coverage != null ? `${cur.tests.coverage.toFixed(1)}%` : "—", deltaCell(d.coverage, "%", true, true));
|
|
392
|
+
row("Build time", ms(b.perf.buildTimeMs), ms(cur.perf.buildTimeMs), deltaCell(d.buildTimeMs, "ms", false));
|
|
393
|
+
row("Bundle size", bundleKB(b.perf.bundleSizeBytes), bundleKB(cur.perf.bundleSizeBytes), deltaCell(d.bundleSizeBytes, "KB", false));
|
|
394
|
+
row("Security findings", String(b.securityCount), String(cur.securityCount), deltaCell(d.security, "", false));
|
|
395
|
+
row("Duplication clones", String(b.duplicationCount), String(cur.duplicationCount), deltaCell(d.duplication, "", false));
|
|
396
|
+
out.push("");
|
|
397
|
+
return out;
|
|
398
|
+
}
|
|
399
|
+
function proofSection(proofs) {
|
|
400
|
+
const out = [];
|
|
401
|
+
out.push(`## Fixes shipped with permanent proof`);
|
|
402
|
+
out.push("");
|
|
403
|
+
if (proofs.length === 0) {
|
|
404
|
+
out.push(`No \`pitstop-repro-*.test.*\` files are committed. Every fix in the loop is`);
|
|
405
|
+
out.push(`supposed to ship with a permanent repro test that proves it — their absence is a`);
|
|
406
|
+
out.push(`red flag that fixes went out without a captured failing-then-passing proof.`);
|
|
407
|
+
out.push("");
|
|
408
|
+
return out;
|
|
409
|
+
}
|
|
410
|
+
out.push(`One line per committed fix, linking the test that permanently proves it:`);
|
|
411
|
+
out.push("");
|
|
412
|
+
out.push(`| Finding | Proved by |`);
|
|
413
|
+
out.push(`| --- | --- |`);
|
|
414
|
+
for (const p of proofs) {
|
|
415
|
+
out.push(`| \`${p.findingId ?? "?"}\` | \`${p.file}\` |`);
|
|
416
|
+
}
|
|
417
|
+
out.push("");
|
|
418
|
+
return out;
|
|
419
|
+
}
|
|
420
|
+
function integrityTerminalBlock(model) {
|
|
421
|
+
const integ = model.integrity;
|
|
422
|
+
if (!integ || integ.checks === 0)
|
|
423
|
+
return null;
|
|
424
|
+
if (integ.catches === 0) {
|
|
425
|
+
return `${chalk.bold("Integrity gate")}: ${chalk.green("clean — no cheating detected across " + integ.checks + " check(s)")}`;
|
|
426
|
+
}
|
|
427
|
+
const lines = [
|
|
428
|
+
`${chalk.bold("Integrity gate")}: ${chalk.yellow(integ.catches + " catch(es)")}, ${chalk.green(integ.selfCorrected + " self-corrected by the loop")}`,
|
|
429
|
+
];
|
|
430
|
+
for (const e of integ.events.filter((x) => x.verdict !== "CLEAN")) {
|
|
431
|
+
for (const f of e.findings) {
|
|
432
|
+
lines.push(` - [${f.confidence}] ${f.detector}/${f.pattern} @ ${f.file}${f.line ? ":" + f.line : ""}: ${f.evidence}`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return lines.join("\n");
|
|
436
|
+
}
|
|
437
|
+
function integritySection(model) {
|
|
438
|
+
const out = ["## Integrity gate", ""];
|
|
439
|
+
const integ = model.integrity;
|
|
440
|
+
if (!integ || integ.checks === 0) {
|
|
441
|
+
out.push("No integrity checks recorded by the loop. Run `pitstop verify` (or `pitstop integrity`) to populate.");
|
|
442
|
+
out.push("");
|
|
443
|
+
return out;
|
|
444
|
+
}
|
|
445
|
+
if (integ.catches === 0) {
|
|
446
|
+
out.push(`Clean — the integrity gate ran ${integ.checks} time(s) and caught no cheating. Honest fixes sailed through, which is exactly what a trustworthy gate must allow.`);
|
|
447
|
+
out.push("");
|
|
448
|
+
return out;
|
|
449
|
+
}
|
|
450
|
+
out.push(`OpenPitStop's integrity gate caught **${integ.catches}** cheating attempt(s) across ${integ.checks} check(s).`);
|
|
451
|
+
if (integ.selfCorrected > 0) {
|
|
452
|
+
out.push(`OpenPitStop caught ${integ.selfCorrected} attempted cheat(s), reverted them, retried the same cluster with the real fix, and proceeded. A catch that ends in a clean verify is the gate working — this is the single most credible line in this report.`);
|
|
453
|
+
}
|
|
454
|
+
if (integ.catches - integ.selfCorrected > 0) {
|
|
455
|
+
out.push(`**${integ.catches - integ.selfCorrected}** marked **"requires human review"** — the loop escalated instead of looping forever or hiding the cheat.`);
|
|
456
|
+
}
|
|
457
|
+
out.push("");
|
|
458
|
+
out.push(`| Verdict | Detector | Evidence |`);
|
|
459
|
+
out.push(`| --- | --- | --- |`);
|
|
460
|
+
for (const e of integ.events.filter((x) => x.verdict !== "CLEAN")) {
|
|
461
|
+
for (const f of e.findings) {
|
|
462
|
+
out.push(`| ${e.verdict} | ${f.detector}/${f.pattern} | \`${f.file}${f.line ? ":" + f.line : ""}\` — ${f.evidence} |`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
out.push("");
|
|
466
|
+
return out;
|
|
467
|
+
}
|
|
468
|
+
function notesSection() {
|
|
469
|
+
return [
|
|
470
|
+
`## Notes`,
|
|
471
|
+
``,
|
|
472
|
+
`- Every figure above is read directly from \`.pitstop/scan-*.json\` and \`.pitstop/verify-*.json\`.`,
|
|
473
|
+
`- Categories marked **${NOT_SCANNED}** could not be analyzed for this repo (see the scan's per-category notes for why).`,
|
|
474
|
+
`- Categories marked **${NOT_ASSESSED}** depend on a \`pitstop verify\` run that has not happened.`,
|
|
475
|
+
`- Accessibility can only run a live page test with pa11y/axe; otherwise it falls back to static JSX linting.`,
|
|
476
|
+
`- Reliability and race-condition findings are heuristics — confirm each before acting.`,
|
|
477
|
+
``,
|
|
478
|
+
];
|
|
479
|
+
}
|
|
480
|
+
export function renderMarkdown(model, opts = {}) {
|
|
481
|
+
const out = [];
|
|
482
|
+
out.push(`# PITSTOP — ${opts.title ?? "Repository Analysis Complete"}`);
|
|
483
|
+
out.push("");
|
|
484
|
+
out.push(`_Generated ${model.generatedAt}_ `);
|
|
485
|
+
out.push(`_Repo: \`${model.repo}\` · ${model.scansCount} scan(s), ${model.verifiesCount} verify(ies)_`);
|
|
486
|
+
if (opts.headerNote)
|
|
487
|
+
out.push(`_${opts.headerNote}_`);
|
|
488
|
+
if (model.latestScan) {
|
|
489
|
+
out.push("");
|
|
490
|
+
out.push(``);
|
|
491
|
+
}
|
|
492
|
+
out.push("");
|
|
493
|
+
if (opts.clustersFirst) {
|
|
494
|
+
out.push(...clustersSection(model.clusters));
|
|
495
|
+
out.push(...summarySection(model.lines));
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
out.push(...summarySection(model.lines));
|
|
499
|
+
out.push(...clustersSection(model.clusters));
|
|
500
|
+
}
|
|
501
|
+
out.push(...beforeAfterSection(model));
|
|
502
|
+
out.push(...proofSection(model.proofs));
|
|
503
|
+
out.push(...integritySection(model));
|
|
504
|
+
out.push(...notesSection());
|
|
505
|
+
return out.join("\n");
|
|
506
|
+
}
|
|
507
|
+
/* ------------------------------------------------------------------ */
|
|
508
|
+
/* Single-file HTML report */
|
|
509
|
+
/* ------------------------------------------------------------------ */
|
|
510
|
+
export function escapeHtml(s) {
|
|
511
|
+
return s
|
|
512
|
+
.replace(/&/g, "&")
|
|
513
|
+
.replace(/</g, "<")
|
|
514
|
+
.replace(/>/g, ">")
|
|
515
|
+
.replace(/"/g, """);
|
|
516
|
+
}
|
|
517
|
+
/** Minimal inline-SVG line chart for a metric across scans. */
|
|
518
|
+
export function svgTrend(values, colorHex) {
|
|
519
|
+
const pts = [];
|
|
520
|
+
values.forEach((v, i) => {
|
|
521
|
+
if (v != null)
|
|
522
|
+
pts.push({ i, v });
|
|
523
|
+
});
|
|
524
|
+
if (pts.length === 0)
|
|
525
|
+
return `<p class="muted">no data</p>`;
|
|
526
|
+
const n = pts.length;
|
|
527
|
+
const W = Math.max(260, n * 44 + 40);
|
|
528
|
+
const H = 72;
|
|
529
|
+
const min = Math.min(...pts.map((p) => p.v));
|
|
530
|
+
const max = Math.max(...pts.map((p) => p.v));
|
|
531
|
+
const span = max - min || 1;
|
|
532
|
+
const x = (i) => 18 + (n === 1 ? 0 : (i * (W - 44)) / (n - 1));
|
|
533
|
+
const y = (v) => H - 14 - ((v - min) / span) * (H - 30);
|
|
534
|
+
const line = pts.map((p) => `${x(p.i).toFixed(1)},${y(p.v).toFixed(1)}`).join(" ");
|
|
535
|
+
const dots = pts
|
|
536
|
+
.map((p) => `<circle cx="${x(p.i).toFixed(1)}" cy="${y(p.v).toFixed(1)}" r="3" fill="${colorHex}"/>`)
|
|
537
|
+
.join("");
|
|
538
|
+
return `<svg viewBox="0 0 ${W} ${H}" width="100%" height="${H}" role="img" aria-label="trend">
|
|
539
|
+
<polyline points="${line}" fill="none" stroke="${colorHex}" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>
|
|
540
|
+
${dots}
|
|
541
|
+
<text x="18" y="${H - 2}" font-size="10" fill="#8b949e">${min}</text>
|
|
542
|
+
<text x="18" y="11" font-size="10" fill="#8b949e">${max}</text>
|
|
543
|
+
</svg>`;
|
|
544
|
+
}
|
|
545
|
+
const LINE_HEX = {
|
|
546
|
+
green: "#3fb950",
|
|
547
|
+
yellow: "#d29922",
|
|
548
|
+
red: "#f85149",
|
|
549
|
+
dim: "#8b949e",
|
|
550
|
+
};
|
|
551
|
+
function verdictHex(v) {
|
|
552
|
+
return v === "CLEAN" ? "#3fb950" : v === "SUSPICIOUS" ? "#d29922" : "#f85149";
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Self-contained HTML report (inline CSS, inline SVG charts, zero external
|
|
556
|
+
* assets) — the artifact you send to a stakeholder and it just works.
|
|
557
|
+
*/
|
|
558
|
+
export function renderHtml(model) {
|
|
559
|
+
const hist = loadHistory(model.repo);
|
|
560
|
+
const scans = hist.scans;
|
|
561
|
+
const sc = model.latestScan ? computeScore(model.latestScan) : null;
|
|
562
|
+
const heroColor = sc ? gradeHex(sc.grade) : "#58a6ff";
|
|
563
|
+
const badge = sc ? `<div class="badge">${escapeHtml(renderBadgeSvg(sc))}</div>` : "";
|
|
564
|
+
const time = model.generatedAt.slice(0, 19).replace("T", " ");
|
|
565
|
+
const summaryRows = model.lines
|
|
566
|
+
.map((l) => {
|
|
567
|
+
const c = LINE_HEX[l.color ?? "dim"] ?? "#8b949e";
|
|
568
|
+
return `<tr><td class="label">${escapeHtml(l.label)}</td><td style="color:${c}">${escapeHtml(l.value)}</td></tr>`;
|
|
569
|
+
})
|
|
570
|
+
.join("\n");
|
|
571
|
+
const clusterCards = (() => {
|
|
572
|
+
if (model.clusters.length === 0)
|
|
573
|
+
return `<p class="muted">No root-cause clusters identified.</p>`;
|
|
574
|
+
return model.clusters
|
|
575
|
+
.map((c, i) => `
|
|
576
|
+
<div class="card">
|
|
577
|
+
<h3>${i + 1}. Root cause <span class="chip" style="color:${c.rootCause.severity === "critical" || c.rootCause.severity === "high" ? "#f85149" : "#d29922"}">${escapeHtml(c.rootCause.severity.toUpperCase())} ${escapeHtml(c.rootCause.type)}</span>${c.rootCause.id ? ` <code>${escapeHtml(c.rootCause.id)}</code>` : ""}</h3>
|
|
578
|
+
<p>${escapeHtml(c.rootCause.description)}</p>
|
|
579
|
+
<p class="muted">→ ${c.symptoms.length} symptom(s) · shared: <code>${escapeHtml(c.sharedFiles.join("`, `"))}</code></p>
|
|
580
|
+
<ul>${c.symptoms.map((s) => `<li>(${escapeHtml(s.severity.toUpperCase())} ${escapeHtml(s.type)}) ${escapeHtml(s.description)}</li>`).join("")}</ul>
|
|
581
|
+
</div>`)
|
|
582
|
+
.join("\n");
|
|
583
|
+
})();
|
|
584
|
+
const beforeAfter = (() => {
|
|
585
|
+
const v = model.latestVerify;
|
|
586
|
+
if (!v)
|
|
587
|
+
return `<p class="muted">No verify history — run \`pitstop verify\` to populate the before/after diff.</p>`;
|
|
588
|
+
const b = v.baseline;
|
|
589
|
+
const cur = v.current;
|
|
590
|
+
const d = v.deltas;
|
|
591
|
+
const cell = (n, unit, higherIsBetter) => {
|
|
592
|
+
if (n === 0)
|
|
593
|
+
return `<span class="muted">0</span>`;
|
|
594
|
+
const good = higherIsBetter ? n > 0 : n < 0;
|
|
595
|
+
const sign = n > 0 ? "+" : "";
|
|
596
|
+
const val = unit === "KB" ? `${sign}${(n / 1024).toFixed(1)} KB` : unit === "ms" ? `${sign}${n} ms` : unit === "%" ? `${sign}${n.toFixed(1)}%` : `${sign}${n}`;
|
|
597
|
+
return `<span style="color:${good ? "#3fb950" : "#f85149"}">${val}</span>`;
|
|
598
|
+
};
|
|
599
|
+
const kb = (x) => (x == null ? "—" : `${(x / 1024).toFixed(1)} KB`);
|
|
600
|
+
const ms = (x) => (x == null ? "—" : `${x} ms`);
|
|
601
|
+
const pct = (x) => (x == null ? "—" : `${x.toFixed(1)}%`);
|
|
602
|
+
const r = (name, base, curr, dlt) => `<tr><td class="label">${name}</td><td>${base}</td><td>${curr}</td><td>${dlt}</td></tr>`;
|
|
603
|
+
return `
|
|
604
|
+
<div class="card">
|
|
605
|
+
<h3>Before / After <span class="chip">risk: <b style="color:${v.risk === "High" ? "#f85149" : v.risk === "Medium" ? "#d29922" : "#3fb950"}">${v.risk}</b></span></h3>
|
|
606
|
+
<table><thead><tr><th>Metric</th><th>Baseline</th><th>Current</th><th>Δ</th></tr></thead><tbody>
|
|
607
|
+
${r("OpenPitStop score", `${v.score?.baseline ?? "—"}/100 (${v.score ? gradeOf(v.score.current) : ""})`, `${v.score?.current ?? "—"}/100`, cell(v.score?.delta ?? 0, "", true))}
|
|
608
|
+
${r("Tests passed", String(b.tests.passed), String(cur.tests.passed), cell(d.passed, "", true))}
|
|
609
|
+
${r("Tests failed", String(b.tests.failed), String(cur.tests.failed), cell(d.failed, "", false))}
|
|
610
|
+
${r("Duration", ms(b.tests.durationMs), ms(cur.tests.durationMs), cell(d.durationMs, "ms", false))}
|
|
611
|
+
${r("Coverage", pct(b.tests.coverage), pct(cur.tests.coverage), cell(d.coverage, "%", true))}
|
|
612
|
+
${r("Build time", ms(b.perf.buildTimeMs), ms(cur.perf.buildTimeMs), cell(d.buildTimeMs, "ms", false))}
|
|
613
|
+
${r("Bundle size", kb(b.perf.bundleSizeBytes), kb(cur.perf.bundleSizeBytes), cell(d.bundleSizeBytes, "KB", false))}
|
|
614
|
+
${r("Security findings", String(b.securityCount), String(cur.securityCount), cell(d.security, "", false))}
|
|
615
|
+
${r("Duplication clones", String(b.duplicationCount), String(cur.duplicationCount), cell(d.duplication, "", false))}
|
|
616
|
+
</tbody></table>
|
|
617
|
+
</div>`;
|
|
618
|
+
})();
|
|
619
|
+
return `<!DOCTYPE html>
|
|
620
|
+
<html lang="en">
|
|
621
|
+
<head>
|
|
622
|
+
<meta charset="utf-8"/>
|
|
623
|
+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
624
|
+
<title>PITSTOP — ${escapeHtml(model.repo)}</title>
|
|
625
|
+
<style>
|
|
626
|
+
:root{color-scheme:dark}
|
|
627
|
+
*{box-sizing:border-box}
|
|
628
|
+
body{background:#0d1117;color:#e6edf3;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;margin:0;padding:32px 16px}
|
|
629
|
+
.wrap{max-width:860px;margin:0 auto}
|
|
630
|
+
h1{font-size:20px;margin:0 0 4px}
|
|
631
|
+
.muted{color:#8b949e;font-size:13px}
|
|
632
|
+
.sub{color:#8b949e;font-size:13px;margin-bottom:24px}
|
|
633
|
+
.hero{display:flex;align-items:center;gap:20px;background:#161b22;border:1px solid #30363d;border-radius:12px;padding:24px;margin-bottom:24px;flex-wrap:wrap}
|
|
634
|
+
.score{font-size:56px;font-weight:800;line-height:1;color:${heroColor}}
|
|
635
|
+
.grd{font-size:22px;font-weight:700;color:${heroColor}}
|
|
636
|
+
.badge{margin-top:4px}
|
|
637
|
+
h2{font-size:16px;border-bottom:1px solid #30363d;padding-bottom:8px;margin:32px 0 12px;color:#f0f6fc}
|
|
638
|
+
table{width:100%;border-collapse:collapse;font-size:13px;margin-bottom:8px}
|
|
639
|
+
th,td{text-align:left;padding:7px 10px;border-bottom:1px solid #21262d}
|
|
640
|
+
th{color:#8b949e;font-weight:600}
|
|
641
|
+
.label{font-weight:600;color:#c9d1d9}
|
|
642
|
+
code{background:#21262d;padding:1px 6px;border-radius:5px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px}
|
|
643
|
+
.card{background:#161b22;border:1px solid #30363d;border-radius:10px;padding:16px 20px;margin-bottom:16px}
|
|
644
|
+
.card h3{margin:0 0 8px;font-size:14px}
|
|
645
|
+
.card p{margin:4px 0;font-size:13px}
|
|
646
|
+
.card ul{margin:6px 0 0;padding-left:20px;font-size:13px}
|
|
647
|
+
.card li{margin:2px 0}
|
|
648
|
+
.chip{background:#21262d;border-radius:20px;padding:1px 10px;font-size:12px;margin-left:8px;font-weight:600}
|
|
649
|
+
.verdict{display:inline-block;border-radius:20px;padding:2px 10px;font-size:12px;font-weight:700;margin:2px 4px 2px 0}
|
|
650
|
+
.trend .card{display:flex;align-items:center;gap:16px;flex-wrap:wrap}
|
|
651
|
+
.trend .card h3{min-width:180px;margin:0}
|
|
652
|
+
.trend .card div{flex:1;min-width:260px}
|
|
653
|
+
.footer{color:#8b949e;font-size:12px;margin-top:40px;text-align:center}
|
|
654
|
+
a{color:#58a6ff}
|
|
655
|
+
</style>
|
|
656
|
+
</head>
|
|
657
|
+
<body><div class="wrap">
|
|
658
|
+
<h1>PITSTOP — Repository Analysis</h1>
|
|
659
|
+
<p class="sub">${escapeHtml(model.repo)} · generated ${time} · ${model.scansCount} scan(s), ${model.verifiesCount} verify(ies)</p>
|
|
660
|
+
|
|
661
|
+
<div class="hero">
|
|
662
|
+
${sc ? `<div><div class="score">${sc.score}<span style="font-size:20px">/100</span></div><div class="grd">${sc.grade}</div></div>` : `<div class="score" style="font-size:24px">no scan yet</div>`}
|
|
663
|
+
${badge}
|
|
664
|
+
<p class="muted" style="flex-basis:100%;margin:0">${sc ? `${sc.analyzed}/${sc.total} categories analyzed. A higher number is healthier.` : "Run <code>pitstop scan</code> to score this repo."}</p>
|
|
665
|
+
</div>
|
|
666
|
+
|
|
667
|
+
<h2>Summary</h2>
|
|
668
|
+
<table><tbody>${summaryRows}</tbody></table>
|
|
669
|
+
|
|
670
|
+
<h2>Root-Cause Clusters</h2>
|
|
671
|
+
${clusterCards}
|
|
672
|
+
|
|
673
|
+
<h2>Before / After</h2>
|
|
674
|
+
${beforeAfter}
|
|
675
|
+
|
|
676
|
+
<h2>Trends across scans</h2>
|
|
677
|
+
<div class="trend">
|
|
678
|
+
${[
|
|
679
|
+
{ label: "Security issues", color: "#f85149", get: (s) => (s.security.status === "ok" ? s.security.issues.length : null) },
|
|
680
|
+
{ label: "Circular imports", color: "#d29922", get: (s) => (s.dependencyGraph.status === "ok" ? s.dependencyGraph.circular.length : null) },
|
|
681
|
+
{ label: "Duplication clones", color: "#d29922", get: (s) => (s.duplication.status === "ok" ? s.duplication.cloneCount : null) },
|
|
682
|
+
{ label: "Failed tests", color: "#f85149", get: (s) => (s.tests.status === "ok" ? s.tests.failed : null) },
|
|
683
|
+
{ label: "Test coverage %", color: "#3fb950", get: (s) => (s.tests.status === "ok" ? s.tests.coverage ?? null : null) },
|
|
684
|
+
{ label: "OpenPitStop score", color: "#58a6ff", get: (s) => computeScore(s).score },
|
|
685
|
+
]
|
|
686
|
+
.map((m) => `<div class="card"><h3>${m.label}</h3><div>${svgTrend(scans.map(m.get), m.color)}</div></div>`)
|
|
687
|
+
.join("\n")}
|
|
688
|
+
</div>
|
|
689
|
+
|
|
690
|
+
${model.integrity && model.integrity.checks > 0
|
|
691
|
+
? `<h2>Integrity gate</h2>
|
|
692
|
+
<div class="card"><p>${model.integrity.checks} check(s) · ${model.integrity.catches} catch(es) · ${model.integrity.selfCorrected} self-corrected</p>
|
|
693
|
+
<p>${model.integrity.events
|
|
694
|
+
.map((e) => `<span class="verdict" style="background:${verdictHex(e.verdict)}22;color:${verdictHex(e.verdict)};border:1px solid ${verdictHex(e.verdict)}">${e.verdict}</span><span class="muted">${e.timestamp.slice(0, 19).replace("T", " ")} · ${e.findings.length} finding(s)</span>`)
|
|
695
|
+
.join("<br/>")}</p></div>`
|
|
696
|
+
: ""}
|
|
697
|
+
|
|
698
|
+
<h2>Fixes shipped with permanent proof</h2>
|
|
699
|
+
<div class="card">
|
|
700
|
+
${model.proofs.length === 0 ? `<p class="muted">No pitstop-repro-*.test.* files are committed.</p>` : `<table><thead><tr><th>Finding</th><th>Proved by</th></tr></thead><tbody>${model.proofs.map((p) => `<tr><td><code>${escapeHtml(p.findingId ?? "?")}</code></td><td><code>${escapeHtml(p.file)}</code></td></tr>`).join("")}</tbody></table>`}
|
|
701
|
+
</div>
|
|
702
|
+
|
|
703
|
+
<div class="footer">Generated by openpitstop · every figure read from .pitstop/scan-*.json and .pitstop/verify-*.json</div>
|
|
704
|
+
</div></body></html>`;
|
|
705
|
+
}
|
|
706
|
+
//# sourceMappingURL=format.js.map
|