vibecodedshit 0.0.0-stage → 0.2.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 +95 -0
- package/README.md +64 -2
- package/assets/fonts/NotoEmoji-OFL.txt +93 -0
- package/assets/fonts/NotoEmoji-Poop.ttf +0 -0
- package/assets/fonts/SpaceGrotesk-Bold.ttf +0 -0
- package/assets/fonts/SpaceGrotesk-OFL.txt +93 -0
- package/dist/bin.js +8 -0
- package/dist/card/previous.js +38 -0
- package/dist/card/render.js +71 -0
- package/dist/cli.js +121 -0
- package/dist/commands/card.js +69 -0
- package/dist/commands/common.js +1 -0
- package/dist/commands/diff.js +54 -0
- package/dist/commands/footer.js +25 -0
- package/dist/commands/report.js +49 -0
- package/dist/commands/route.js +62 -0
- package/dist/commands/submit.js +125 -0
- package/dist/commands/summary.js +77 -0
- package/dist/diff/compare.js +73 -0
- package/dist/diff/conditions.js +87 -0
- package/dist/diff/format.js +23 -0
- package/dist/exec/docker.js +158 -0
- package/dist/exec/findings.js +73 -0
- package/dist/exec/plan.js +50 -0
- package/dist/exec/reports.js +90 -0
- package/dist/exec/roots.js +49 -0
- package/dist/exec/run.js +182 -0
- package/dist/exec/steps/build.js +45 -0
- package/dist/exec/steps/coverage-install.js +12 -0
- package/dist/exec/steps/install.js +41 -0
- package/dist/exec/steps/knip.js +28 -0
- package/dist/exec/steps/lint.js +25 -0
- package/dist/exec/steps/lockfiles.js +25 -0
- package/dist/exec/steps/test.js +17 -0
- package/dist/exec/steps/typecheck.js +40 -0
- package/dist/exec/test-reports/coverage-py.js +23 -0
- package/dist/exec/test-reports/coverage-ranges.js +65 -0
- package/dist/exec/test-reports/coverage-summary.js +23 -0
- package/dist/exec/test-reports/jest-json.js +31 -0
- package/dist/exec/test-reports/junit.js +24 -0
- package/dist/exec/test-reports/mocha-json.js +18 -0
- package/dist/exec/test-reports/vitest-json.js +5 -0
- package/dist/exec/test-runners/detect.js +99 -0
- package/dist/exec/test-runners/index.js +6 -0
- package/dist/exec/test-runners/jest.js +27 -0
- package/dist/exec/test-runners/mocha.js +28 -0
- package/dist/exec/test-runners/node-test.js +24 -0
- package/dist/exec/test-runners/pytest.js +18 -0
- package/dist/exec/test-runners/script.js +48 -0
- package/dist/exec/test-runners/spec.js +1 -0
- package/dist/exec/test-runners/vitest.js +28 -0
- package/dist/exec/tests.js +120 -0
- package/dist/exec/types.js +1 -0
- package/dist/manifest/package-json.js +36 -0
- package/dist/manifest/python.js +106 -0
- package/dist/measure/d3/index.js +45 -0
- package/dist/measure/d3/presets.js +100 -0
- package/dist/measure/d3/py/adapter.js +67 -0
- package/dist/measure/d3/py/config.js +80 -0
- package/dist/measure/d3/py/contracts.js +57 -0
- package/dist/measure/d3/py/domain.js +57 -0
- package/dist/measure/d3/py/graph.js +214 -0
- package/dist/measure/d3/py/interpreter.js +31 -0
- package/dist/measure/d3/py/layers.js +120 -0
- package/dist/measure/d3/py/roots.js +58 -0
- package/dist/measure/d3/py/types.js +1 -0
- package/dist/measure/d3/scc.js +64 -0
- package/dist/measure/d3/ts/adapter.js +371 -0
- package/dist/measure/d3/ts/classify.js +107 -0
- package/dist/measure/d3/ts/config.js +318 -0
- package/dist/measure/d3/ts/domain.js +23 -0
- package/dist/measure/d3/ts/rules.js +65 -0
- package/dist/measure/d3/types.js +5 -0
- package/dist/measure/deps/cvss.js +66 -0
- package/dist/measure/deps/declared.js +195 -0
- package/dist/measure/deps/files.js +40 -0
- package/dist/measure/deps/lockfiles.js +308 -0
- package/dist/measure/deps/osv.js +122 -0
- package/dist/measure/deps/registry.js +142 -0
- package/dist/measure/deps/resolved.js +78 -0
- package/dist/measure/deps/units.js +5 -0
- package/dist/measure/eslint/health-plugin.js +19 -0
- package/dist/measure/ops/docs-commands.js +73 -0
- package/dist/measure/ops/docs-resolve.js +217 -0
- package/dist/measure/ops/entry-points.js +49 -0
- package/dist/measure/ops/env-example.js +20 -0
- package/dist/measure/ops/env-reads.js +46 -0
- package/dist/measure/ops/runtime.js +73 -0
- package/dist/measure/p1-1.js +8 -0
- package/dist/measure/p1-2.js +8 -0
- package/dist/measure/p1-3.js +8 -0
- package/dist/measure/p1-4.js +41 -0
- package/dist/measure/p1-5.js +59 -0
- package/dist/measure/p2-1.js +8 -0
- package/dist/measure/p2-2.js +44 -0
- package/dist/measure/p2-3.js +81 -0
- package/dist/measure/p2-4.js +288 -0
- package/dist/measure/p2-5.js +8 -0
- package/dist/measure/p3-1.js +16 -0
- package/dist/measure/p3-2.js +26 -0
- package/dist/measure/p3-3.js +37 -0
- package/dist/measure/p3-4.js +26 -0
- package/dist/measure/p3-5.js +26 -0
- package/dist/measure/p4-1.js +184 -0
- package/dist/measure/p4-2.js +27 -0
- package/dist/measure/p4-3.js +106 -0
- package/dist/measure/p4-4.js +309 -0
- package/dist/measure/p5-1.js +117 -0
- package/dist/measure/p5-2.js +80 -0
- package/dist/measure/p5-3.js +21 -0
- package/dist/measure/p5-4.js +161 -0
- package/dist/measure/p5-5.js +158 -0
- package/dist/measure/p6-1.js +20 -0
- package/dist/measure/p6-2.js +20 -0
- package/dist/measure/p6-3.js +40 -0
- package/dist/measure/p6-4.js +31 -0
- package/dist/measure/p6-5.js +60 -0
- package/dist/measure/p6-6.js +54 -0
- package/dist/measure/py/typecheck-config.js +95 -0
- package/dist/measure/registry.js +31 -0
- package/dist/measure/safety/py-tests.js +124 -0
- package/dist/measure/safety/ts-barrels.js +68 -0
- package/dist/measure/safety/ts-cases.js +111 -0
- package/dist/measure/safety/ts-credits.js +101 -0
- package/dist/measure/safety/ts-imports.js +63 -0
- package/dist/measure/safety/ts-resolve.js +43 -0
- package/dist/measure/ts/directives.js +37 -0
- package/dist/measure/ts/exports.js +60 -0
- package/dist/measure/ts/tsconfig.js +179 -0
- package/dist/measure/types.js +1 -0
- package/dist/measure/units.js +41 -0
- package/dist/methodology/load.js +159 -0
- package/dist/methodology/schema.js +105 -0
- package/dist/python/deadcode.js +27 -0
- package/dist/python/env.js +13 -0
- package/dist/python/helper.js +24 -0
- package/dist/python/imports.js +40 -0
- package/dist/python/nox.js +16 -0
- package/dist/python/scope.js +35 -0
- package/dist/python/tests.js +19 -0
- package/dist/python/typing.js +27 -0
- package/dist/report/escape.js +5 -0
- package/dist/report/format.js +75 -0
- package/dist/report/render.js +213 -0
- package/dist/report/roadmap.js +58 -0
- package/dist/report/scares.js +83 -0
- package/dist/report/style.js +27 -0
- package/dist/report-only/hotspots.js +48 -0
- package/dist/route/check.js +37 -0
- package/dist/route/files.js +74 -0
- package/dist/route/order.js +22 -0
- package/dist/route/select.js +114 -0
- package/dist/route/write.js +13 -0
- package/dist/run/read.js +89 -0
- package/dist/run/recompute.js +19 -0
- package/dist/run/scan.js +196 -0
- package/dist/run/schema.js +1 -0
- package/dist/run/score-run.js +42 -0
- package/dist/run/write.js +4 -0
- package/dist/scope/build.js +173 -0
- package/dist/scope/packages.js +87 -0
- package/dist/scope/rules.js +21 -0
- package/dist/scope/tracked.js +34 -0
- package/dist/scope/ts-lines.js +63 -0
- package/dist/scope/types.js +7 -0
- package/dist/scoring/aggregate.js +38 -0
- package/dist/scoring/pool.js +65 -0
- package/dist/scoring/score.js +81 -0
- package/dist/scoring/what-if.js +38 -0
- package/dist/scripts/checks.js +53 -0
- package/dist/scripts/ci.js +139 -0
- package/dist/scripts/entry-points/find.js +31 -0
- package/dist/scripts/entry-points/index.js +18 -0
- package/dist/scripts/entry-points/justfile.js +62 -0
- package/dist/scripts/entry-points/makefile.js +66 -0
- package/dist/scripts/entry-points/nox.js +29 -0
- package/dist/scripts/entry-points/package-json.js +21 -0
- package/dist/scripts/entry-points/pyproject.js +73 -0
- package/dist/scripts/entry-points/tox.js +130 -0
- package/dist/scripts/expand.js +300 -0
- package/dist/scripts/triggers.js +151 -0
- package/dist/scripts/types.js +1 -0
- package/dist/submit/constants.js +9 -0
- package/dist/submit/payload.js +42 -0
- package/dist/submit/repo-identity.js +46 -0
- package/dist/submit/send.js +29 -0
- package/dist/tools/catalog.js +46 -0
- package/dist/tools/image.js +180 -0
- package/dist/tools/kit.js +71 -0
- package/dist/tools/knip.js +169 -0
- package/dist/tools/outcome.js +21 -0
- package/dist/tools/resolve.js +66 -0
- package/dist/tools/workcopy.js +27 -0
- package/dist/util/ini.js +37 -0
- package/dist/util/pep440.js +163 -0
- package/dist/util/toml.js +27 -0
- package/docker/Dockerfile +142 -0
- package/docker/bin/health-capture +5 -0
- package/docker/bin/health-coverage-install +5 -0
- package/docker/bin/health-eslint +66 -0
- package/docker/bin/health-pytest +10 -0
- package/docker/bin/health-tsc +10 -0
- package/docker/bin/health-venv +11 -0
- package/docker/config/eslint.config.mjs +4 -0
- package/docker/config/eslint.rules.mjs +65 -0
- package/docker/config/eslint.untyped.config.mjs +4 -0
- package/docker/config/knip.json +1 -0
- package/docker/config/ruff.toml +7 -0
- package/methodology/LICENSE +396 -0
- package/methodology/v1.0/METHODOLOGY.md +1176 -0
- package/methodology/v1.0/dimensions/1-safety-net.yaml +240 -0
- package/methodology/v1.0/dimensions/2-type-static-rigor.yaml +289 -0
- package/methodology/v1.0/dimensions/3-architecture-boundaries.yaml +266 -0
- package/methodology/v1.0/dimensions/4-complexity-maintainability.yaml +192 -0
- package/methodology/v1.0/dimensions/5-dependency-security-hygiene.yaml +223 -0
- package/methodology/v1.0/dimensions/6-operability-feedback-loop.yaml +279 -0
- package/methodology/v1.0/methodology.yaml +242 -0
- package/methodology/v1.0/presets/django.yaml +43 -0
- package/methodology/v1.0/presets/express.yaml +47 -0
- package/methodology/v1.0/presets/fastapi.yaml +52 -0
- package/methodology/v1.0/presets/generic.yaml +41 -0
- package/methodology/v1.0/presets/nextjs.yaml +50 -0
- package/methodology/v1.0/scope.yaml +115 -0
- package/package.json +61 -4
- package/python/dead_code_helper.py +265 -0
- package/python/env_helper.py +71 -0
- package/python/graph_helper.py +277 -0
- package/python/imports_helper.py +132 -0
- package/python/nox_helper.py +79 -0
- package/python/scope_helper.py +96 -0
- package/python/tests_helper.py +171 -0
- package/python/typing_helper.py +235 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const anchor = z.object({ raw: z.number(), score: z.number(), descriptor: z.string().optional() });
|
|
3
|
+
const levelEntry = z.object({ level: z.string(), score: z.number(), descriptor: z.string().optional() });
|
|
4
|
+
const component = z.object({
|
|
5
|
+
key: z.string(),
|
|
6
|
+
method: z.enum(['levels', 'ratio']),
|
|
7
|
+
levels: z.array(levelEntry).optional(),
|
|
8
|
+
definition: z.string().optional(),
|
|
9
|
+
measure: z.string().optional(),
|
|
10
|
+
});
|
|
11
|
+
const scoring = z.discriminatedUnion('method', [
|
|
12
|
+
z.object({ method: z.literal('piecewise_linear'), anchors: z.array(anchor).min(2) }),
|
|
13
|
+
z.object({
|
|
14
|
+
method: z.literal('levels'),
|
|
15
|
+
levels: z.union([z.array(levelEntry), z.record(z.string(), z.array(levelEntry))]),
|
|
16
|
+
}),
|
|
17
|
+
z.object({ method: z.literal('mean_of_components'), components: z.array(component).min(1) }),
|
|
18
|
+
]);
|
|
19
|
+
const parameterSchema = z.object({
|
|
20
|
+
id: z.string(),
|
|
21
|
+
key: z.string(),
|
|
22
|
+
name: z.string(),
|
|
23
|
+
layer: z.enum(['static', 'execution']),
|
|
24
|
+
weight: z.number(),
|
|
25
|
+
pooling: z.enum(['ratio', 'unit_weighted']),
|
|
26
|
+
raw: z.object({ unit: z.string(), when_empty: z.number() }),
|
|
27
|
+
scoring,
|
|
28
|
+
/** Prose per language or topic, for example the 3.5 forbidden-import lists. */
|
|
29
|
+
measure: z.record(z.string(), z.string()).optional(),
|
|
30
|
+
not_applicable_when: z.string().nullable().optional(),
|
|
31
|
+
});
|
|
32
|
+
export const dimensionFileSchema = z.object({
|
|
33
|
+
dimension: z.object({ id: z.number(), key: z.string(), name: z.string() }),
|
|
34
|
+
/** Dimension 6 prose naming the recognised tools and the removed prefixes. */
|
|
35
|
+
script_resolution: z.string().optional(),
|
|
36
|
+
parameters: z.array(parameterSchema).min(1),
|
|
37
|
+
presets: z.object({ files: z.array(z.string()) }).optional(),
|
|
38
|
+
});
|
|
39
|
+
const stringList = z.array(z.string());
|
|
40
|
+
const presetRule = z.object({
|
|
41
|
+
name: z.string(),
|
|
42
|
+
description: z.string(),
|
|
43
|
+
from: z.object({ in: stringList.optional(), not_in: stringList.optional() }),
|
|
44
|
+
to: z.object({ in: stringList.optional(), packages: stringList.optional() }),
|
|
45
|
+
severity: z.enum(['error', 'warn', 'info']),
|
|
46
|
+
type_only_imports: z.enum(['included', 'excluded']),
|
|
47
|
+
});
|
|
48
|
+
export const presetFileSchema = z.object({
|
|
49
|
+
preset: z.object({
|
|
50
|
+
key: z.enum(['nextjs', 'express', 'django', 'fastapi', 'generic']),
|
|
51
|
+
name: z.string(),
|
|
52
|
+
languages: stringList,
|
|
53
|
+
}),
|
|
54
|
+
conventions: z.string().optional(),
|
|
55
|
+
/** Glob lists, or prose for content layers (nextjs `server_only` and `client`). */
|
|
56
|
+
layers: z.record(z.string(), z.union([stringList, z.string()])),
|
|
57
|
+
modules: z.object({
|
|
58
|
+
roots: z.union([stringList, z.string()]),
|
|
59
|
+
public_entry: z.union([z.string(), stringList, z.record(z.string(), z.string())]),
|
|
60
|
+
}),
|
|
61
|
+
rules: z.array(presetRule),
|
|
62
|
+
});
|
|
63
|
+
export const methodologyFileSchema = z.object({
|
|
64
|
+
version: z.string(),
|
|
65
|
+
dimensions: z.array(z.object({ id: z.number(), key: z.string(), file: z.string() })).min(1),
|
|
66
|
+
aggregation: z.object({
|
|
67
|
+
dimension_to_health: z.object({ cap_margin: z.number() }),
|
|
68
|
+
}),
|
|
69
|
+
bands: z
|
|
70
|
+
.array(z.object({ key: z.string(), name: z.string(), min: z.number(), max: z.number(), meaning: z.string().optional() }))
|
|
71
|
+
.min(1),
|
|
72
|
+
execution: z.object({
|
|
73
|
+
runner: z.object({ image: z.string(), cpus: z.number(), memory_gb: z.number() }),
|
|
74
|
+
timeouts_seconds: z.object({
|
|
75
|
+
install: z.number(),
|
|
76
|
+
build: z.number(),
|
|
77
|
+
typecheck: z.number(),
|
|
78
|
+
lint: z.number(),
|
|
79
|
+
tests: z.number(),
|
|
80
|
+
}),
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
const globList = z.array(z.string());
|
|
84
|
+
export const scopeFileSchema = z.object({
|
|
85
|
+
source_files: z.object({
|
|
86
|
+
extensions: z.object({
|
|
87
|
+
typescript: globList,
|
|
88
|
+
javascript: globList,
|
|
89
|
+
python: globList,
|
|
90
|
+
}),
|
|
91
|
+
}),
|
|
92
|
+
exclusions: z.object({
|
|
93
|
+
vendored: globList,
|
|
94
|
+
build_output: globList,
|
|
95
|
+
generated: globList,
|
|
96
|
+
minified: globList,
|
|
97
|
+
migrations: globList,
|
|
98
|
+
declarations: globList,
|
|
99
|
+
test_data: globList,
|
|
100
|
+
}),
|
|
101
|
+
file_roles: z.object({
|
|
102
|
+
test: z.object({ typescript_javascript: globList, python: globList }),
|
|
103
|
+
config: globList,
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { ToolError } from '../scope/types.js';
|
|
5
|
+
const helperPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'python', 'dead_code_helper.py');
|
|
6
|
+
/** Symbols, `__all__`, imports and class bases of every file, with one helper process. */
|
|
7
|
+
export function analyzePythonDeadCode(repoPath, files) {
|
|
8
|
+
if (files.length === 0)
|
|
9
|
+
return {};
|
|
10
|
+
const result = spawnSync('python3', [helperPath], {
|
|
11
|
+
cwd: repoPath,
|
|
12
|
+
input: JSON.stringify({ files }),
|
|
13
|
+
encoding: 'utf8',
|
|
14
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
15
|
+
});
|
|
16
|
+
if (result.error)
|
|
17
|
+
throw new ToolError(`Could not run python3: ${result.error.message}`);
|
|
18
|
+
if (result.status !== 0) {
|
|
19
|
+
throw new ToolError(`dead_code_helper.py failed: ${result.stderr.trim() || `exit ${String(result.status)}`}`);
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(result.stdout);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new ToolError('dead_code_helper.py printed invalid JSON');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { runPythonHelper } from './helper.js';
|
|
3
|
+
const cache = new Map();
|
|
4
|
+
/** Environment variable names read by each given Python file: one helper spawn per scan, memoized per repository. */
|
|
5
|
+
export function pythonEnvReads(repoPath, files) {
|
|
6
|
+
const key = path.resolve(repoPath);
|
|
7
|
+
const cached = cache.get(key);
|
|
8
|
+
if (cached)
|
|
9
|
+
return cached;
|
|
10
|
+
const reads = files.length === 0 ? {} : runPythonHelper('env_helper.py', repoPath, { files });
|
|
11
|
+
cache.set(key, reads);
|
|
12
|
+
return reads;
|
|
13
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { ToolError } from '../scope/types.js';
|
|
5
|
+
const pythonDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'python');
|
|
6
|
+
/** Run one stdlib-only helper from `python/` with JSON in and JSON out. A missing interpreter or a crashed helper is a ToolError. */
|
|
7
|
+
export function runPythonHelper(helper, repoPath, input) {
|
|
8
|
+
const result = spawnSync('python3', [path.join(pythonDir, helper)], {
|
|
9
|
+
cwd: repoPath,
|
|
10
|
+
input: JSON.stringify(input),
|
|
11
|
+
encoding: 'utf8',
|
|
12
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
13
|
+
});
|
|
14
|
+
if (result.error)
|
|
15
|
+
throw new ToolError(`Could not run python3: ${result.error.message}`);
|
|
16
|
+
if (result.status !== 0)
|
|
17
|
+
throw new ToolError(`${helper} failed: ${result.stderr.trim() || `exit ${String(result.status)}`}`);
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(result.stdout);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw new ToolError(`${helper} printed invalid JSON`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { ToolError } from '../scope/types.js';
|
|
5
|
+
const helperPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'python', 'imports_helper.py');
|
|
6
|
+
function runHelper(repoPath, files) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const child = spawn('python3', [helperPath], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
9
|
+
let stdout = '';
|
|
10
|
+
let stderr = '';
|
|
11
|
+
child.stdout.on('data', (chunk) => {
|
|
12
|
+
stdout += chunk.toString('utf8');
|
|
13
|
+
});
|
|
14
|
+
child.stderr.on('data', (chunk) => {
|
|
15
|
+
stderr += chunk.toString('utf8');
|
|
16
|
+
});
|
|
17
|
+
child.on('error', (err) => reject(new ToolError(`Could not run python3: ${err.message}`)));
|
|
18
|
+
child.on('close', (code) => {
|
|
19
|
+
if (code !== 0) {
|
|
20
|
+
reject(new ToolError(`imports_helper.py failed: ${stderr.trim() || `exit ${String(code)}`}`));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
resolve(JSON.parse(stdout).files);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
reject(new ToolError('imports_helper.py printed invalid JSON'));
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
child.stdin.on('error', () => undefined);
|
|
31
|
+
child.stdin.end(JSON.stringify({ root: path.resolve(repoPath), files }));
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/** Import statements and builtin `open` calls of every in-scope Python file, one helper process per scan. */
|
|
35
|
+
export function readPythonImports(ctx) {
|
|
36
|
+
return ctx.tools.memo(`d3-py-imports:${ctx.repoPath}`, () => {
|
|
37
|
+
const files = ctx.scope.files.filter((f) => f.group === 'python').map((f) => f.path);
|
|
38
|
+
return files.length === 0 ? Promise.resolve({}) : runHelper(ctx.repoPath, files);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { trackedPaths } from '../scope/tracked.js';
|
|
3
|
+
import { runPythonHelper } from './helper.js';
|
|
4
|
+
const cache = new Map();
|
|
5
|
+
/** The sessions of the tracked `noxfile.py` in `file`'s directory (one helper spawn per scan, memoized per repository). Undefined when it does not parse. */
|
|
6
|
+
export function noxSessions(repoPath, file) {
|
|
7
|
+
const key = path.resolve(repoPath);
|
|
8
|
+
let all = cache.get(key);
|
|
9
|
+
if (!all) {
|
|
10
|
+
const files = trackedPaths(repoPath).filter((f) => path.posix.basename(f) === 'noxfile.py');
|
|
11
|
+
all = files.length === 0 ? {} : runPythonHelper('nox_helper.py', repoPath, { files });
|
|
12
|
+
cache.set(key, all);
|
|
13
|
+
}
|
|
14
|
+
const found = all[file];
|
|
15
|
+
return found?.ok ? found.sessions : undefined;
|
|
16
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { ToolError } from '../scope/types.js';
|
|
5
|
+
const helperPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'python', 'scope_helper.py');
|
|
6
|
+
export function pythonVersion() {
|
|
7
|
+
const result = spawnSync('python3', ['--version'], { encoding: 'utf8' });
|
|
8
|
+
if (result.error || result.status !== 0) {
|
|
9
|
+
throw new ToolError('python3 is not available on PATH, but the repository has Python files');
|
|
10
|
+
}
|
|
11
|
+
return `${result.stdout}${result.stderr}`.trim().replace(/^Python\s+/, '');
|
|
12
|
+
}
|
|
13
|
+
/** Classify all Python files with one helper process. */
|
|
14
|
+
export function analyzePythonFiles(repoPath, files) {
|
|
15
|
+
if (files.length === 0)
|
|
16
|
+
return {};
|
|
17
|
+
const result = spawnSync('python3', [helperPath], {
|
|
18
|
+
cwd: repoPath,
|
|
19
|
+
input: JSON.stringify({ files }),
|
|
20
|
+
encoding: 'utf8',
|
|
21
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
22
|
+
});
|
|
23
|
+
if (result.error) {
|
|
24
|
+
throw new ToolError(`Could not run python3: ${result.error.message}`);
|
|
25
|
+
}
|
|
26
|
+
if (result.status !== 0) {
|
|
27
|
+
throw new ToolError(`scope_helper.py failed: ${result.stderr.trim() || `exit ${result.status}`}`);
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(result.stdout);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw new ToolError('scope_helper.py printed invalid JSON');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { trackedPaths } from '../scope/tracked.js';
|
|
3
|
+
import { runPythonHelper } from './helper.js';
|
|
4
|
+
const cache = new Map();
|
|
5
|
+
const NOT_FIRST_PARTY = /(?:^|\/)(?:node_modules|\.venv|venv|site-packages|__pycache__)\//;
|
|
6
|
+
/**
|
|
7
|
+
* Import and test-case facts of every tracked Python file, packages that only re-export included (they are not scoped files but
|
|
8
|
+
* 1.4 follows imports through them). One helper spawn per scan, memoized per repository.
|
|
9
|
+
*/
|
|
10
|
+
export function pythonTestFacts(repoPath) {
|
|
11
|
+
const key = path.resolve(repoPath);
|
|
12
|
+
const cached = cache.get(key);
|
|
13
|
+
if (cached)
|
|
14
|
+
return cached;
|
|
15
|
+
const files = trackedPaths(repoPath).filter((f) => f.endsWith('.py') && !NOT_FIRST_PARTY.test(f));
|
|
16
|
+
const facts = files.length === 0 ? {} : runPythonHelper('tests_helper.py', repoPath, { files });
|
|
17
|
+
cache.set(key, facts);
|
|
18
|
+
return facts;
|
|
19
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { ToolError } from '../scope/types.js';
|
|
5
|
+
const helperPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'python', 'typing_helper.py');
|
|
6
|
+
/** Typing facts of every file, with one helper process. */
|
|
7
|
+
export function analyzePythonTyping(repoPath, files) {
|
|
8
|
+
if (files.length === 0)
|
|
9
|
+
return {};
|
|
10
|
+
const result = spawnSync('python3', [helperPath], {
|
|
11
|
+
cwd: repoPath,
|
|
12
|
+
input: JSON.stringify({ files }),
|
|
13
|
+
encoding: 'utf8',
|
|
14
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
15
|
+
});
|
|
16
|
+
if (result.error)
|
|
17
|
+
throw new ToolError(`Could not run python3: ${result.error.message}`);
|
|
18
|
+
if (result.status !== 0) {
|
|
19
|
+
throw new ToolError(`typing_helper.py failed: ${result.stderr.trim() || `exit ${String(result.status)}`}`);
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(result.stdout);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new ToolError('typing_helper.py printed invalid JSON');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
const ENTITIES = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
2
|
+
/** Every dynamic string in the report goes through this: package names, paths and reasons are untrusted. */
|
|
3
|
+
export function escapeHtml(value) {
|
|
4
|
+
return String(value).replace(/[&<>"']/g, (c) => ENTITIES[c] ?? c);
|
|
5
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { roundHalfUp } from '../scoring/aggregate.js';
|
|
2
|
+
function round1(value) {
|
|
3
|
+
return roundHalfUp(value, 1).toFixed(1);
|
|
4
|
+
}
|
|
5
|
+
function scaleLabel(scale) {
|
|
6
|
+
if (scale === 100)
|
|
7
|
+
return '%';
|
|
8
|
+
if (scale === 1000)
|
|
9
|
+
return ' per 1,000';
|
|
10
|
+
return ` per ${scale}`;
|
|
11
|
+
}
|
|
12
|
+
function formatRaw(raw, unit) {
|
|
13
|
+
switch (raw.kind) {
|
|
14
|
+
case 'ratio':
|
|
15
|
+
return raw.denominator === 0
|
|
16
|
+
? `${raw.numerator} / 0`
|
|
17
|
+
: `${raw.numerator} / ${raw.denominator} (${round1((raw.numerator / raw.denominator) * raw.scale)}${scaleLabel(raw.scale)})`;
|
|
18
|
+
case 'level':
|
|
19
|
+
return raw.level;
|
|
20
|
+
case 'number':
|
|
21
|
+
return `${raw.value} ${unit}`;
|
|
22
|
+
case 'failed':
|
|
23
|
+
return 'install failed';
|
|
24
|
+
case 'components':
|
|
25
|
+
return Object.entries(raw.components)
|
|
26
|
+
.map(([key, part]) => `${key}: ${formatRaw(part, unit)}`)
|
|
27
|
+
.join('; ');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** The raw value as the report prints it. Level parameters have no pooled raw, so their units' levels are listed. */
|
|
31
|
+
export function rawText(param, def) {
|
|
32
|
+
if (param.raw)
|
|
33
|
+
return formatRaw(param.raw, def.raw.unit);
|
|
34
|
+
const levels = [...new Set((param.units ?? []).flatMap((u) => (u.raw.kind === 'level' ? [u.raw.level] : [])))];
|
|
35
|
+
if (levels.length > 0)
|
|
36
|
+
return levels.join(', ');
|
|
37
|
+
const components = (param.units ?? []).flatMap((u) => (u.raw.kind === 'components' ? [u.raw] : []));
|
|
38
|
+
if (components.length > 0)
|
|
39
|
+
return components.map((c) => formatRaw(c, def.raw.unit)).join(' | ');
|
|
40
|
+
return 'no data';
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The descriptor of the anchor or level whose score is closest to the parameter's score, ties to the lower score.
|
|
44
|
+
* Null for parameters without descriptors (components).
|
|
45
|
+
*/
|
|
46
|
+
export function nearestDescriptor(def, score) {
|
|
47
|
+
const scoring = def.scoring;
|
|
48
|
+
let candidates;
|
|
49
|
+
if (scoring.method === 'piecewise_linear')
|
|
50
|
+
candidates = scoring.anchors;
|
|
51
|
+
else if (scoring.method === 'levels')
|
|
52
|
+
candidates = Array.isArray(scoring.levels) ? scoring.levels : Object.values(scoring.levels).flat();
|
|
53
|
+
else
|
|
54
|
+
return null;
|
|
55
|
+
let best;
|
|
56
|
+
for (const candidate of candidates) {
|
|
57
|
+
if (!candidate.descriptor)
|
|
58
|
+
continue;
|
|
59
|
+
if (!best) {
|
|
60
|
+
best = candidate;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const gap = Math.abs(candidate.score - score);
|
|
64
|
+
const bestGap = Math.abs(best.score - score);
|
|
65
|
+
if (gap < bestGap - 1e-9 || (Math.abs(gap - bestGap) <= 1e-9 && candidate.score < best.score))
|
|
66
|
+
best = candidate;
|
|
67
|
+
}
|
|
68
|
+
return best?.descriptor?.replace(/\s+/g, ' ').trim() ?? null;
|
|
69
|
+
}
|
|
70
|
+
export function statusText(status) {
|
|
71
|
+
return status.replace('_', ' ');
|
|
72
|
+
}
|
|
73
|
+
export function scoreText(score) {
|
|
74
|
+
return score === null || score === undefined ? '–' : String(roundHalfUp(score));
|
|
75
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { SHALLOW_WARNING } from '../report-only/hotspots.js';
|
|
2
|
+
import { escapeHtml as e } from './escape.js';
|
|
3
|
+
import { nearestDescriptor, rawText, scoreText, statusText } from './format.js';
|
|
4
|
+
import { buildRoute, routeLines } from '../route/select.js';
|
|
5
|
+
import { buildRoadmap, gainText, ROADMAP_LIMIT } from './roadmap.js';
|
|
6
|
+
import { NO_SCARES, buildScares, unchecked } from './scares.js';
|
|
7
|
+
import { STYLE } from './style.js';
|
|
8
|
+
const HELP_URL = 'https://vibecodedshit.com';
|
|
9
|
+
const NOT_COLLECTED = '<p class="muted">not collected</p>';
|
|
10
|
+
function table(head, rows) {
|
|
11
|
+
const th = head.map((h) => `<th${h.num ? ' class="num"' : ''}>${e(h.text)}</th>`).join('');
|
|
12
|
+
const body = rows
|
|
13
|
+
.map((row) => `<tr>${row.map((cell, i) => `<td${head[i]?.num ? ' class="num"' : ''}>${cell}</td>`).join('')}</tr>`)
|
|
14
|
+
.join('\n');
|
|
15
|
+
return `<table><thead><tr>${th}</tr></thead><tbody>\n${body}\n</tbody></table>`;
|
|
16
|
+
}
|
|
17
|
+
function bar(percent, marks = []) {
|
|
18
|
+
const width = Math.max(0, Math.min(100, percent));
|
|
19
|
+
const ticks = marks.map((m) => `<i style="left:${m}%"></i>`).join('');
|
|
20
|
+
return `<div class="bar"><span style="width:${width}%"></span>${ticks}</div>`;
|
|
21
|
+
}
|
|
22
|
+
function asRecords(value) {
|
|
23
|
+
if (!Array.isArray(value))
|
|
24
|
+
return null;
|
|
25
|
+
return value.filter((v) => typeof v === 'object' && v !== null);
|
|
26
|
+
}
|
|
27
|
+
const str = (v) => (typeof v === 'string' || typeof v === 'number' ? String(v) : '');
|
|
28
|
+
function header(run, methodology) {
|
|
29
|
+
const parts = ['<h1>Codebase health report</h1>'];
|
|
30
|
+
parts.push(`<p class="muted">${e(run.repo.path)} · ${run.repo.headCommit ? `commit <code>${e(run.repo.headCommit.slice(0, 12))}</code> · ` : ''}as of ${e(run.asOf)} · methodology ${e(run.methodology.version)} · ${e(run.mode)} run</p>`);
|
|
31
|
+
if (run.mode === 'preview' || !run.comparable) {
|
|
32
|
+
parts.push(`<p><span class="label">${e(run.comparableReason ?? 'preview, not comparable')}</span></p>`);
|
|
33
|
+
}
|
|
34
|
+
if (run.health) {
|
|
35
|
+
const band = methodology.bands.find((b) => b.key === run.health?.band);
|
|
36
|
+
parts.push(`<div class="health"><span class="score">${e(run.health.displayed)}</span><span class="band">${e(band?.name ?? run.health.band)}</span></div>`, bar(run.health.displayed, methodology.bands.slice(1).map((b) => b.min)), `<p>${e(band?.meaning?.replace(/\s+/g, ' ').trim() ?? '')}</p>`);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
parts.push('<p class="muted">no health score</p>');
|
|
40
|
+
}
|
|
41
|
+
return parts.join('\n');
|
|
42
|
+
}
|
|
43
|
+
function scaresSection(run) {
|
|
44
|
+
const scares = buildScares(run);
|
|
45
|
+
const parts = ['<h2>Top scares</h2>'];
|
|
46
|
+
if (scares.length > 0)
|
|
47
|
+
parts.push(`<ol>${scares.map((s) => `<li>${e(s)}</li>`).join('')}</ol>`);
|
|
48
|
+
else
|
|
49
|
+
parts.push(`<p>${e(NO_SCARES)}</p>`);
|
|
50
|
+
const missing = unchecked(run);
|
|
51
|
+
if (missing.length > 0)
|
|
52
|
+
parts.push(`<p class="muted">Not checked in this run: ${e(missing.join(', '))}.</p>`);
|
|
53
|
+
return parts.join('\n');
|
|
54
|
+
}
|
|
55
|
+
function dimensionsSection(run, methodology) {
|
|
56
|
+
const rows = methodology.dimensions.map((def) => {
|
|
57
|
+
const score = run.dimensions.find((d) => d.id === def.id)?.score ?? null;
|
|
58
|
+
return [
|
|
59
|
+
e(`${def.id}`),
|
|
60
|
+
e(def.name),
|
|
61
|
+
score === null ? '<span class="muted">not scored</span>' : e(scoreText(score)),
|
|
62
|
+
score === null ? '' : bar(score),
|
|
63
|
+
];
|
|
64
|
+
});
|
|
65
|
+
return `<h2>Dimensions</h2>\n${table([{ text: '#' }, { text: 'Dimension' }, { text: 'Score', num: true }, { text: '' }], rows)}`;
|
|
66
|
+
}
|
|
67
|
+
function roadmapSection(run, methodology) {
|
|
68
|
+
const parts = ['<h2>Roadmap</h2>'];
|
|
69
|
+
const items = buildRoadmap(run, methodology);
|
|
70
|
+
if (!run.health) {
|
|
71
|
+
parts.push('<p class="muted">no roadmap</p>');
|
|
72
|
+
return parts.join('\n');
|
|
73
|
+
}
|
|
74
|
+
parts.push(`<p class="muted">The parameters ranked by Gain: the whole health points the health would rise by if that one parameter alone reached 100. Gains are independent and do not add up, because the dimension means and the cap interact. Top ${ROADMAP_LIMIT}.</p>`);
|
|
75
|
+
if (run.mode === 'preview')
|
|
76
|
+
parts.push('<p class="muted">Execution-layer parameters were not measured, so they are not ranked.</p>');
|
|
77
|
+
if (items.length === 0)
|
|
78
|
+
parts.push('<p class="muted">no parameter is below 100</p>');
|
|
79
|
+
else {
|
|
80
|
+
const top = Math.max(1, ...items.map((i) => i.gain));
|
|
81
|
+
parts.push(table([{ text: '#', num: true }, { text: 'Parameter' }, { text: 'Dimension' }, { text: 'Raw value' }, { text: 'Score', num: true }, { text: 'Gain', num: true }, { text: '' }, { text: 'Next anchor' }], items.map((item, i) => [
|
|
82
|
+
String(i + 1),
|
|
83
|
+
`${e(item.id)} ${e(item.name)}`,
|
|
84
|
+
e(item.dimensionName),
|
|
85
|
+
item.raw === null ? '–' : e(item.raw),
|
|
86
|
+
e(scoreText(item.score)),
|
|
87
|
+
e(gainText(item.gain)),
|
|
88
|
+
bar((item.gain / top) * 100),
|
|
89
|
+
item.target ? e(item.target) : '–',
|
|
90
|
+
])));
|
|
91
|
+
}
|
|
92
|
+
if (run.health.band === 'vibe_coded' || run.health.band === 'fragile') {
|
|
93
|
+
parts.push(`<p>If you want help working through this roadmap to Stable, Peter Kisel takes on this work: <a href="${HELP_URL}">${HELP_URL}</a></p>`);
|
|
94
|
+
}
|
|
95
|
+
return parts.join('\n');
|
|
96
|
+
}
|
|
97
|
+
function routeSection(run, methodology) {
|
|
98
|
+
const plan = buildRoute(run, methodology);
|
|
99
|
+
if (plan.kind === 'none')
|
|
100
|
+
return '';
|
|
101
|
+
const lines = routeLines(plan);
|
|
102
|
+
if (plan.kind === 'preview')
|
|
103
|
+
return `<h2>Route</h2>\n<p class="muted">${e(lines.join(' '))}</p>`;
|
|
104
|
+
const parts = [`<h2>Route to ${e(plan.target.name)}</h2>`, ...lines.map((line) => `<p>${e(line)}</p>`)];
|
|
105
|
+
if (plan.unmeasured.length > 0)
|
|
106
|
+
parts.push(`<p class="muted">Not measured in this run, so the Route leaves them out: ${e(plan.unmeasured.join(', '))}.</p>`);
|
|
107
|
+
if (plan.slices.length > 0) {
|
|
108
|
+
parts.push(table([{ text: '#', num: true }, { text: 'Step' }, { text: 'Score', num: true }, { text: 'Health after', num: true }, { text: 'Target' }], plan.slices.map((s) => [String(s.position), `${e(s.id)} ${e(s.name)}`, e(`${scoreText(s.fromScore)} → ${scoreText(s.toScore)}`), e(s.healthAfter), e(s.descriptor)])));
|
|
109
|
+
}
|
|
110
|
+
return parts.join('\n');
|
|
111
|
+
}
|
|
112
|
+
function parametersSection(run, methodology) {
|
|
113
|
+
const defs = new Map(methodology.dimensions.flatMap((d) => d.parameters).map((p) => [p.id, p]));
|
|
114
|
+
const rows = run.parameters.map((param) => {
|
|
115
|
+
const def = defs.get(param.id);
|
|
116
|
+
const status = `<span class="status-${e(param.status)}">${e(statusText(param.status))}</span>${param.reason ? ` <span class="muted">(${e(param.reason)})</span>` : ''}`;
|
|
117
|
+
const scored = param.status === 'measured' || param.status === 'failed';
|
|
118
|
+
const raw = def && param.status === 'measured' ? e(rawText(param, def)) : '–';
|
|
119
|
+
const descriptor = def && param.status === 'measured' && param.score !== undefined ? nearestDescriptor(def, param.score) : null;
|
|
120
|
+
return [e(param.id), e(def?.name ?? param.id), raw, scored ? e(scoreText(param.score)) : '–', status, descriptor ? e(descriptor) : '–'];
|
|
121
|
+
});
|
|
122
|
+
return `<h2>Parameters</h2>\n${table([{ text: 'ID' }, { text: 'Parameter' }, { text: 'Raw value' }, { text: 'Score', num: true }, { text: 'Status' }, { text: 'Nearest anchor' }], rows)}`;
|
|
123
|
+
}
|
|
124
|
+
function breakdownTable(title, byKey, methodology) {
|
|
125
|
+
const keys = Object.keys(byKey).sort();
|
|
126
|
+
if (keys.length === 0)
|
|
127
|
+
return `<h3>${e(title)}</h3>\n<p class="muted">no units</p>`;
|
|
128
|
+
const head = [{ text: title.replace(/^By /, '') }, ...methodology.dimensions.map((d) => ({ text: `${d.id} ${d.name}`, num: true }))];
|
|
129
|
+
const rows = keys.map((key) => [
|
|
130
|
+
`<code>${e(key)}</code>`,
|
|
131
|
+
...methodology.dimensions.map((d) => e(scoreText(byKey[key]?.[String(d.id)]))),
|
|
132
|
+
]);
|
|
133
|
+
return `<h3>${e(title)}</h3>\n${table(head, rows)}`;
|
|
134
|
+
}
|
|
135
|
+
function breakdownSection(run, methodology) {
|
|
136
|
+
return `<h2>Breakdown</h2>\n${breakdownTable('By package', run.breakdown.byPackage, methodology)}\n${breakdownTable('By language group', run.breakdown.byGroup, methodology)}`;
|
|
137
|
+
}
|
|
138
|
+
function excludedSection(run) {
|
|
139
|
+
return `<h2>Excluded lines</h2>\n<p>${e(run.ignoreSet.excludedLinesPercent)}% of lines excluded by .healthignore</p>`;
|
|
140
|
+
}
|
|
141
|
+
function secretsHtml(value) {
|
|
142
|
+
const record = typeof value === 'object' && value !== null ? value : null;
|
|
143
|
+
const findings = asRecords(record?.findings);
|
|
144
|
+
if (!findings)
|
|
145
|
+
return NOT_COLLECTED;
|
|
146
|
+
if (findings.length === 0)
|
|
147
|
+
return '<p class="muted">none found</p>';
|
|
148
|
+
return table([{ text: 'Rule' }, { text: 'File' }, { text: 'Commit' }], findings.map((f) => [e(str(f.ruleId)), `<code>${e(str(f.file))}</code>`, `<code>${e(str(f.commit).slice(0, 8))}</code>`]));
|
|
149
|
+
}
|
|
150
|
+
function vulnerabilitiesHtml(value) {
|
|
151
|
+
const record = typeof value === 'object' && value !== null ? value : null;
|
|
152
|
+
const advisories = asRecords(record?.advisories);
|
|
153
|
+
if (!advisories)
|
|
154
|
+
return NOT_COLLECTED;
|
|
155
|
+
if (advisories.length === 0)
|
|
156
|
+
return '<p class="muted">none found</p>';
|
|
157
|
+
return table([{ text: 'Package' }, { text: 'Advisory' }, { text: 'Severity' }], advisories.map((a) => [`<code>${e(str(a.name))}</code>`, e(str(a.id)), e(str(a.severity))]));
|
|
158
|
+
}
|
|
159
|
+
function reportOnlySection(run) {
|
|
160
|
+
const only = run.reportOnly ?? {};
|
|
161
|
+
return [
|
|
162
|
+
'<h2>Report-only items</h2>',
|
|
163
|
+
'<p class="muted">These never change a score.</p>',
|
|
164
|
+
'<h3>Secrets in git history</h3>',
|
|
165
|
+
secretsHtml(only.secretsInHistory),
|
|
166
|
+
'<h3>Development-dependency vulnerabilities</h3>',
|
|
167
|
+
vulnerabilitiesHtml(only.devDependencyVulnerabilities),
|
|
168
|
+
].join('\n');
|
|
169
|
+
}
|
|
170
|
+
function hotspotsSection(run) {
|
|
171
|
+
const hotspots = asRecords(run.reportOnly?.hotspots);
|
|
172
|
+
const parts = ['<h2>Fix these first</h2>', '<p class="muted">Files ranked by commits in the last 12 months times cognitive complexity.</p>'];
|
|
173
|
+
if (run.warnings.includes(SHALLOW_WARNING))
|
|
174
|
+
parts.push(`<p class="note">${e(SHALLOW_WARNING)}</p>`);
|
|
175
|
+
if (!hotspots)
|
|
176
|
+
parts.push(NOT_COLLECTED);
|
|
177
|
+
else if (hotspots.length === 0)
|
|
178
|
+
parts.push('<p class="muted">no file has both churn and complexity</p>');
|
|
179
|
+
else {
|
|
180
|
+
parts.push(table([{ text: 'File' }, { text: 'Commits', num: true }, { text: 'Complexity', num: true }, { text: 'Score', num: true }], hotspots.map((h) => [`<code>${e(str(h.path))}</code>`, e(str(h.churn)), e(str(h.complexity)), e(str(h.score))])));
|
|
181
|
+
}
|
|
182
|
+
return parts.join('\n');
|
|
183
|
+
}
|
|
184
|
+
/** One self-contained HTML document. Inline CSS, no script, no external reference, no clock value. */
|
|
185
|
+
export function renderReport(run, methodology) {
|
|
186
|
+
const sections = [
|
|
187
|
+
header(run, methodology),
|
|
188
|
+
scaresSection(run),
|
|
189
|
+
dimensionsSection(run, methodology),
|
|
190
|
+
roadmapSection(run, methodology),
|
|
191
|
+
routeSection(run, methodology),
|
|
192
|
+
parametersSection(run, methodology),
|
|
193
|
+
breakdownSection(run, methodology),
|
|
194
|
+
excludedSection(run),
|
|
195
|
+
reportOnlySection(run),
|
|
196
|
+
hotspotsSection(run),
|
|
197
|
+
];
|
|
198
|
+
return `<!doctype html>
|
|
199
|
+
<html lang="en">
|
|
200
|
+
<head>
|
|
201
|
+
<meta charset="utf-8">
|
|
202
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
203
|
+
<title>Codebase health report</title>
|
|
204
|
+
<style>${STYLE}</style>
|
|
205
|
+
</head>
|
|
206
|
+
<body>
|
|
207
|
+
<main>
|
|
208
|
+
${sections.filter((section) => section !== '').join('\n')}
|
|
209
|
+
</main>
|
|
210
|
+
</body>
|
|
211
|
+
</html>
|
|
212
|
+
`;
|
|
213
|
+
}
|