vibecodedshit 0.0.0-stage → 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.
Files changed (236) hide show
  1. package/LICENSE +95 -0
  2. package/README.md +73 -2
  3. package/assets/fonts/NotoEmoji-OFL.txt +93 -0
  4. package/assets/fonts/NotoEmoji-Poop.ttf +0 -0
  5. package/assets/fonts/SpaceGrotesk-Bold.ttf +0 -0
  6. package/assets/fonts/SpaceGrotesk-OFL.txt +93 -0
  7. package/dist/bin.js +11 -0
  8. package/dist/card/previous.js +38 -0
  9. package/dist/card/render.js +71 -0
  10. package/dist/cli.js +138 -0
  11. package/dist/commands/card.js +69 -0
  12. package/dist/commands/common.js +1 -0
  13. package/dist/commands/diff.js +54 -0
  14. package/dist/commands/footer.js +28 -0
  15. package/dist/commands/report.js +49 -0
  16. package/dist/commands/route.js +62 -0
  17. package/dist/commands/submit.js +125 -0
  18. package/dist/commands/summary.js +77 -0
  19. package/dist/diff/compare.js +73 -0
  20. package/dist/diff/conditions.js +87 -0
  21. package/dist/diff/format.js +23 -0
  22. package/dist/exec/docker.js +158 -0
  23. package/dist/exec/findings.js +73 -0
  24. package/dist/exec/plan.js +50 -0
  25. package/dist/exec/reports.js +90 -0
  26. package/dist/exec/roots.js +49 -0
  27. package/dist/exec/run.js +182 -0
  28. package/dist/exec/steps/build.js +45 -0
  29. package/dist/exec/steps/coverage-install.js +12 -0
  30. package/dist/exec/steps/install.js +41 -0
  31. package/dist/exec/steps/knip.js +28 -0
  32. package/dist/exec/steps/lint.js +25 -0
  33. package/dist/exec/steps/lockfiles.js +25 -0
  34. package/dist/exec/steps/test.js +17 -0
  35. package/dist/exec/steps/typecheck.js +40 -0
  36. package/dist/exec/test-reports/coverage-py.js +23 -0
  37. package/dist/exec/test-reports/coverage-ranges.js +65 -0
  38. package/dist/exec/test-reports/coverage-summary.js +23 -0
  39. package/dist/exec/test-reports/jest-json.js +31 -0
  40. package/dist/exec/test-reports/junit.js +24 -0
  41. package/dist/exec/test-reports/mocha-json.js +18 -0
  42. package/dist/exec/test-reports/vitest-json.js +5 -0
  43. package/dist/exec/test-runners/detect.js +99 -0
  44. package/dist/exec/test-runners/index.js +6 -0
  45. package/dist/exec/test-runners/jest.js +27 -0
  46. package/dist/exec/test-runners/mocha.js +28 -0
  47. package/dist/exec/test-runners/node-test.js +24 -0
  48. package/dist/exec/test-runners/pytest.js +18 -0
  49. package/dist/exec/test-runners/script.js +48 -0
  50. package/dist/exec/test-runners/spec.js +1 -0
  51. package/dist/exec/test-runners/vitest.js +28 -0
  52. package/dist/exec/tests.js +120 -0
  53. package/dist/exec/types.js +1 -0
  54. package/dist/interactive/after-scan.js +121 -0
  55. package/dist/interactive/docker-setup.js +97 -0
  56. package/dist/interactive/prompt.js +17 -0
  57. package/dist/interactive/system.js +10 -0
  58. package/dist/manifest/package-json.js +36 -0
  59. package/dist/manifest/python.js +106 -0
  60. package/dist/measure/d3/index.js +45 -0
  61. package/dist/measure/d3/presets.js +100 -0
  62. package/dist/measure/d3/py/adapter.js +67 -0
  63. package/dist/measure/d3/py/config.js +80 -0
  64. package/dist/measure/d3/py/contracts.js +57 -0
  65. package/dist/measure/d3/py/domain.js +57 -0
  66. package/dist/measure/d3/py/graph.js +214 -0
  67. package/dist/measure/d3/py/interpreter.js +31 -0
  68. package/dist/measure/d3/py/layers.js +120 -0
  69. package/dist/measure/d3/py/roots.js +58 -0
  70. package/dist/measure/d3/py/types.js +1 -0
  71. package/dist/measure/d3/scc.js +64 -0
  72. package/dist/measure/d3/ts/adapter.js +371 -0
  73. package/dist/measure/d3/ts/classify.js +107 -0
  74. package/dist/measure/d3/ts/config.js +318 -0
  75. package/dist/measure/d3/ts/domain.js +23 -0
  76. package/dist/measure/d3/ts/rules.js +65 -0
  77. package/dist/measure/d3/types.js +5 -0
  78. package/dist/measure/deps/cvss.js +66 -0
  79. package/dist/measure/deps/declared.js +195 -0
  80. package/dist/measure/deps/files.js +40 -0
  81. package/dist/measure/deps/lockfiles.js +308 -0
  82. package/dist/measure/deps/osv.js +122 -0
  83. package/dist/measure/deps/registry.js +142 -0
  84. package/dist/measure/deps/resolved.js +78 -0
  85. package/dist/measure/deps/units.js +5 -0
  86. package/dist/measure/eslint/health-plugin.js +19 -0
  87. package/dist/measure/ops/docs-commands.js +73 -0
  88. package/dist/measure/ops/docs-resolve.js +217 -0
  89. package/dist/measure/ops/entry-points.js +49 -0
  90. package/dist/measure/ops/env-example.js +20 -0
  91. package/dist/measure/ops/env-reads.js +46 -0
  92. package/dist/measure/ops/runtime.js +73 -0
  93. package/dist/measure/p1-1.js +8 -0
  94. package/dist/measure/p1-2.js +8 -0
  95. package/dist/measure/p1-3.js +8 -0
  96. package/dist/measure/p1-4.js +41 -0
  97. package/dist/measure/p1-5.js +59 -0
  98. package/dist/measure/p2-1.js +8 -0
  99. package/dist/measure/p2-2.js +44 -0
  100. package/dist/measure/p2-3.js +81 -0
  101. package/dist/measure/p2-4.js +288 -0
  102. package/dist/measure/p2-5.js +8 -0
  103. package/dist/measure/p3-1.js +16 -0
  104. package/dist/measure/p3-2.js +26 -0
  105. package/dist/measure/p3-3.js +37 -0
  106. package/dist/measure/p3-4.js +26 -0
  107. package/dist/measure/p3-5.js +26 -0
  108. package/dist/measure/p4-1.js +184 -0
  109. package/dist/measure/p4-2.js +27 -0
  110. package/dist/measure/p4-3.js +106 -0
  111. package/dist/measure/p4-4.js +309 -0
  112. package/dist/measure/p5-1.js +117 -0
  113. package/dist/measure/p5-2.js +80 -0
  114. package/dist/measure/p5-3.js +21 -0
  115. package/dist/measure/p5-4.js +161 -0
  116. package/dist/measure/p5-5.js +158 -0
  117. package/dist/measure/p6-1.js +20 -0
  118. package/dist/measure/p6-2.js +20 -0
  119. package/dist/measure/p6-3.js +40 -0
  120. package/dist/measure/p6-4.js +31 -0
  121. package/dist/measure/p6-5.js +60 -0
  122. package/dist/measure/p6-6.js +54 -0
  123. package/dist/measure/py/typecheck-config.js +95 -0
  124. package/dist/measure/registry.js +31 -0
  125. package/dist/measure/safety/py-tests.js +124 -0
  126. package/dist/measure/safety/ts-barrels.js +68 -0
  127. package/dist/measure/safety/ts-cases.js +111 -0
  128. package/dist/measure/safety/ts-credits.js +101 -0
  129. package/dist/measure/safety/ts-imports.js +63 -0
  130. package/dist/measure/safety/ts-resolve.js +43 -0
  131. package/dist/measure/ts/directives.js +37 -0
  132. package/dist/measure/ts/exports.js +60 -0
  133. package/dist/measure/ts/tsconfig.js +179 -0
  134. package/dist/measure/types.js +1 -0
  135. package/dist/measure/units.js +41 -0
  136. package/dist/methodology/load.js +159 -0
  137. package/dist/methodology/schema.js +105 -0
  138. package/dist/python/deadcode.js +27 -0
  139. package/dist/python/env.js +13 -0
  140. package/dist/python/helper.js +24 -0
  141. package/dist/python/imports.js +40 -0
  142. package/dist/python/nox.js +16 -0
  143. package/dist/python/scope.js +35 -0
  144. package/dist/python/tests.js +19 -0
  145. package/dist/python/typing.js +27 -0
  146. package/dist/report/escape.js +5 -0
  147. package/dist/report/format.js +75 -0
  148. package/dist/report/render.js +213 -0
  149. package/dist/report/roadmap.js +58 -0
  150. package/dist/report/scares.js +83 -0
  151. package/dist/report/style.js +27 -0
  152. package/dist/report-only/hotspots.js +48 -0
  153. package/dist/route/check.js +37 -0
  154. package/dist/route/files.js +74 -0
  155. package/dist/route/order.js +22 -0
  156. package/dist/route/select.js +114 -0
  157. package/dist/route/write.js +13 -0
  158. package/dist/run/read.js +89 -0
  159. package/dist/run/recompute.js +19 -0
  160. package/dist/run/scan.js +196 -0
  161. package/dist/run/schema.js +1 -0
  162. package/dist/run/score-run.js +42 -0
  163. package/dist/run/write.js +4 -0
  164. package/dist/scope/build.js +173 -0
  165. package/dist/scope/packages.js +87 -0
  166. package/dist/scope/rules.js +21 -0
  167. package/dist/scope/tracked.js +34 -0
  168. package/dist/scope/ts-lines.js +63 -0
  169. package/dist/scope/types.js +7 -0
  170. package/dist/scoring/aggregate.js +38 -0
  171. package/dist/scoring/pool.js +65 -0
  172. package/dist/scoring/score.js +81 -0
  173. package/dist/scoring/what-if.js +38 -0
  174. package/dist/scripts/checks.js +53 -0
  175. package/dist/scripts/ci.js +139 -0
  176. package/dist/scripts/entry-points/find.js +31 -0
  177. package/dist/scripts/entry-points/index.js +18 -0
  178. package/dist/scripts/entry-points/justfile.js +62 -0
  179. package/dist/scripts/entry-points/makefile.js +66 -0
  180. package/dist/scripts/entry-points/nox.js +29 -0
  181. package/dist/scripts/entry-points/package-json.js +21 -0
  182. package/dist/scripts/entry-points/pyproject.js +73 -0
  183. package/dist/scripts/entry-points/tox.js +130 -0
  184. package/dist/scripts/expand.js +300 -0
  185. package/dist/scripts/triggers.js +151 -0
  186. package/dist/scripts/types.js +1 -0
  187. package/dist/submit/constants.js +10 -0
  188. package/dist/submit/payload.js +42 -0
  189. package/dist/submit/repo-identity.js +46 -0
  190. package/dist/submit/send.js +29 -0
  191. package/dist/tools/catalog.js +46 -0
  192. package/dist/tools/image.js +180 -0
  193. package/dist/tools/kit.js +71 -0
  194. package/dist/tools/knip.js +169 -0
  195. package/dist/tools/outcome.js +21 -0
  196. package/dist/tools/resolve.js +66 -0
  197. package/dist/tools/workcopy.js +27 -0
  198. package/dist/util/ini.js +37 -0
  199. package/dist/util/pep440.js +163 -0
  200. package/dist/util/toml.js +27 -0
  201. package/docker/Dockerfile +142 -0
  202. package/docker/bin/health-capture +5 -0
  203. package/docker/bin/health-coverage-install +5 -0
  204. package/docker/bin/health-eslint +66 -0
  205. package/docker/bin/health-pytest +10 -0
  206. package/docker/bin/health-tsc +10 -0
  207. package/docker/bin/health-venv +11 -0
  208. package/docker/config/eslint.config.mjs +4 -0
  209. package/docker/config/eslint.rules.mjs +65 -0
  210. package/docker/config/eslint.untyped.config.mjs +4 -0
  211. package/docker/config/knip.json +1 -0
  212. package/docker/config/ruff.toml +7 -0
  213. package/methodology/LICENSE +396 -0
  214. package/methodology/v1.0/METHODOLOGY.md +1176 -0
  215. package/methodology/v1.0/dimensions/1-safety-net.yaml +240 -0
  216. package/methodology/v1.0/dimensions/2-type-static-rigor.yaml +289 -0
  217. package/methodology/v1.0/dimensions/3-architecture-boundaries.yaml +266 -0
  218. package/methodology/v1.0/dimensions/4-complexity-maintainability.yaml +192 -0
  219. package/methodology/v1.0/dimensions/5-dependency-security-hygiene.yaml +223 -0
  220. package/methodology/v1.0/dimensions/6-operability-feedback-loop.yaml +279 -0
  221. package/methodology/v1.0/methodology.yaml +242 -0
  222. package/methodology/v1.0/presets/django.yaml +43 -0
  223. package/methodology/v1.0/presets/express.yaml +47 -0
  224. package/methodology/v1.0/presets/fastapi.yaml +52 -0
  225. package/methodology/v1.0/presets/generic.yaml +41 -0
  226. package/methodology/v1.0/presets/nextjs.yaml +50 -0
  227. package/methodology/v1.0/scope.yaml +115 -0
  228. package/package.json +61 -4
  229. package/python/dead_code_helper.py +265 -0
  230. package/python/env_helper.py +71 -0
  231. package/python/graph_helper.py +277 -0
  232. package/python/imports_helper.py +132 -0
  233. package/python/nox_helper.py +79 -0
  234. package/python/scope_helper.py +96 -0
  235. package/python/tests_helper.py +171 -0
  236. package/python/typing_helper.py +235 -0
@@ -0,0 +1,28 @@
1
+ import { parseCoverageRanges } from '../test-reports/coverage-ranges.js';
2
+ import { parseCoverageSummary } from '../test-reports/coverage-summary.js';
3
+ import { parseVitestJson } from '../test-reports/vitest-json.js';
4
+ /** vitest 4 has no `coverage.all`; `coverage.include` reports files no test loads. The provider is `@vitest/coverage-v8`, see `health-coverage-install`. */
5
+ export const vitest = {
6
+ results: 'results.json',
7
+ coverage: 'coverage/coverage-summary.json',
8
+ ranges: 'coverage/coverage-final.json',
9
+ argv: (invocation, globs) => [
10
+ 'npx',
11
+ '--no-install',
12
+ 'vitest',
13
+ 'run',
14
+ '--reporter=json',
15
+ '--outputFile=/out/results.json',
16
+ '--coverage.enabled',
17
+ '--coverage.provider=v8',
18
+ '--coverage.reporter=json-summary',
19
+ '--coverage.reporter=json',
20
+ '--coverage.reportsDirectory=/out/coverage',
21
+ '--coverage.reportOnFailure',
22
+ ...globs.map((glob) => `--coverage.include=${glob}`),
23
+ ...invocation.args,
24
+ ],
25
+ parseResults: parseVitestJson,
26
+ parseCoverage: parseCoverageSummary,
27
+ parseRanges: parseCoverageRanges,
28
+ };
@@ -0,0 +1,120 @@
1
+ import { emptyTallies, tallyUnits, unitId } from '../measure/units.js';
2
+ import { analyzeTsFile } from '../scope/ts-lines.js';
3
+ import { failedRootOf, withFailedUnits } from './roots.js';
4
+ import { coveredLines } from './test-reports/coverage-ranges.js';
5
+ /** Why the test run is a repository failure (1.1 to 1.3, 6.3 score 0), or null when every run has a usable results report. */
6
+ export function testFailure(exec) {
7
+ if (!exec.checksRan)
8
+ return 'install failed, so the step did not run';
9
+ for (const run of exec.test.invocations) {
10
+ if (run.outcome.status === 'timeout')
11
+ return `${run.runner} timed out in ${run.dir}`;
12
+ if (!run.results)
13
+ return `${run.runner} wrote no usable results report in ${run.dir}`;
14
+ }
15
+ return null;
16
+ }
17
+ export function totalCases(exec) {
18
+ return exec.test.invocations.reduce((sum, run) => sum + (run.results ? run.results.passed + run.results.failed + run.results.errored + run.results.skipped : 0), 0);
19
+ }
20
+ function isUnder(file, dir) {
21
+ return dir === '.' || file.startsWith(`${dir}/`);
22
+ }
23
+ /** 1.1: passed / (passed + failed + errored) per test invocation. Skipped cases are in neither. With no case anywhere every unit is 0 of 0. */
24
+ export function measurePassRate(ctx) {
25
+ const exec = ctx.exec;
26
+ if (!exec)
27
+ return { status: 'not_measured', reason: 'execution layer did not run' };
28
+ const reason = testFailure(exec);
29
+ if (reason !== null)
30
+ return { status: 'failed', reason };
31
+ const logic = ctx.scope.files.filter((f) => f.role === 'logic');
32
+ if (exec.test.invocations.length === 0)
33
+ return { status: 'measured', units: withFailedUnits(exec, ctx.scope, tallyUnits(emptyTallies(logic))) };
34
+ const units = exec.test.invocations.map((run) => {
35
+ const counts = run.results ?? { passed: 0, failed: 0, errored: 0, skipped: 0 };
36
+ return {
37
+ package: run.dir,
38
+ group: run.group,
39
+ weight: logic.filter((f) => f.group === run.group && isUnder(f.path, run.dir)).reduce((sum, f) => sum + f.logicLines, 0),
40
+ raw: { kind: 'ratio', numerator: counts.passed, denominator: counts.passed + counts.failed + counts.errored, scale: 100 },
41
+ };
42
+ });
43
+ return { status: 'measured', units: withFailedUnits(exec, ctx.scope, units) };
44
+ }
45
+ /** Code lines of a TS/JS logic file and how many ran, or null when no invocation's coverage-final.json lists the file. */
46
+ function tsLineCoverage(ctx, exec, file) {
47
+ const reported = exec.test.invocations.map((run) => run.ranges?.get(file)).filter((r) => r !== undefined);
48
+ if (reported.length === 0)
49
+ return null;
50
+ const codeLines = analyzeTsFile(file, ctx.scope.getTsSourceFile(file).text).codeLineNumbers;
51
+ const covered = new Set();
52
+ for (const ranges of reported)
53
+ for (const line of coveredLines(codeLines, ranges))
54
+ covered.add(line);
55
+ return { total: codeLines.size, covered: covered.size };
56
+ }
57
+ /**
58
+ * 1.2 (lines) and 1.3 (branches): covered over coverable across the in-scope logic files, per (package,
59
+ * group) unit. 1.2 does not take the runner's line counts, which differ between runners for the same code:
60
+ * for a TS/JS file the coverable lines are health's own code lines and a line is covered when the innermost
61
+ * statement or function range holding it ran in any invocation (see `coveredLines`). Python lines and all
62
+ * branches are the tool's counts. A logic file the coverage report lacks counts as 0 covered with its logic
63
+ * lines coverable (it adds no branches), so it cannot raise the ratio. Coverage entries outside the logic set are dropped.
64
+ * With no test case at all every unit is 0 of 0 and scores `when_empty`.
65
+ */
66
+ export function measureCoverage(ctx, kind) {
67
+ const exec = ctx.exec;
68
+ if (!exec)
69
+ return { status: 'not_measured', reason: 'execution layer did not run' };
70
+ const reason = testFailure(exec);
71
+ if (reason !== null)
72
+ return { status: 'failed', reason };
73
+ const logic = ctx.scope.files.filter((f) => f.role === 'logic');
74
+ const tallies = emptyTallies(logic);
75
+ if (totalCases(exec) === 0)
76
+ return { status: 'measured', units: withFailedUnits(exec, ctx.scope, tallyUnits(tallies)) };
77
+ const merged = new Map();
78
+ for (const run of exec.test.invocations) {
79
+ for (const [file, cov] of run.coverage ?? []) {
80
+ const had = merged.get(file);
81
+ merged.set(file, {
82
+ lines: { total: (had?.lines.total ?? 0) + cov.lines.total, covered: (had?.lines.covered ?? 0) + cov.lines.covered },
83
+ branches: { total: (had?.branches.total ?? 0) + cov.branches.total, covered: (had?.branches.covered ?? 0) + cov.branches.covered },
84
+ });
85
+ }
86
+ }
87
+ let missing = 0;
88
+ for (const file of logic) {
89
+ const tally = tallies.get(unitId(file));
90
+ if (!tally || failedRootOf(exec, file.path, file.group))
91
+ continue;
92
+ if (kind === 'lines' && file.group === 'typescript_javascript') {
93
+ const lines = tsLineCoverage(ctx, exec, file.path);
94
+ if (lines === null) {
95
+ missing += 1;
96
+ tally.denominator += file.logicLines;
97
+ }
98
+ else {
99
+ tally.numerator += lines.covered;
100
+ tally.denominator += lines.total;
101
+ }
102
+ continue;
103
+ }
104
+ const cov = merged.get(file.path);
105
+ if (cov) {
106
+ tally.numerator += cov[kind].covered;
107
+ tally.denominator += cov[kind].total;
108
+ }
109
+ else {
110
+ missing += 1;
111
+ if (kind === 'lines')
112
+ tally.denominator += file.logicLines;
113
+ }
114
+ }
115
+ return {
116
+ status: 'measured',
117
+ units: withFailedUnits(exec, ctx.scope, tallyUnits(tallies)),
118
+ ...(kind === 'lines' && missing > 0 ? { warnings: [`${missing} logic file(s) missing from the coverage report were counted as 0% covered`] } : {}),
119
+ };
120
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,121 @@
1
+ import { renderReport } from '../report/render.js';
2
+ import { EMAIL_PATTERN, MAX_EMAIL_CHARS, MAX_PAYLOAD_BYTES, PRIVACY_URL, QUOTE_URL } from '../submit/constants.js';
3
+ import { buildPayload, PayloadError } from '../submit/payload.js';
4
+ import { repoIdentity, RepoIdentityError } from '../submit/repo-identity.js';
5
+ import { sendPayload } from '../submit/send.js';
6
+ /** Bands whose owners are offered a quote for the fixing service. */
7
+ const QUOTE_BANDS = new Set(['vibe_coded', 'fragile']);
8
+ const QUOTE_TIMEOUT_MS = 30_000;
9
+ async function askEmail(prompt, io) {
10
+ for (let attempt = 0; attempt < 3; attempt += 1) {
11
+ const email = await prompt.text('Your email:');
12
+ if (email.length <= MAX_EMAIL_CHARS && EMAIL_PATTERN.test(email))
13
+ return email;
14
+ io.stderr('That is not a valid email address.\n');
15
+ }
16
+ return null;
17
+ }
18
+ async function submitToLeaderboard(run, ctx, emailFor) {
19
+ const { io, prompt } = ctx;
20
+ if (run.mode !== 'full' || !run.comparable) {
21
+ const why = run.mode === 'full' ? (run.comparableReason ?? 'not comparable') : 'a preview scan, a full scan with Docker is needed';
22
+ io.stdout(`This run cannot go on the global leaderboard: ${why}.\n`);
23
+ return;
24
+ }
25
+ io.stdout(`The leaderboard gets your scores, a hash of your repository URL and your email. No code, no file paths. Privacy: ${PRIVACY_URL}\n`);
26
+ if (!(await prompt.confirm('Submit your score to the global leaderboard?')))
27
+ return;
28
+ const email = await emailFor();
29
+ if (email === null)
30
+ return;
31
+ try {
32
+ const identity = await repoIdentity(run.repo.path, ctx.git);
33
+ const payload = buildPayload({ run, repoHash: identity.hash, email });
34
+ if (Buffer.byteLength(JSON.stringify(payload), 'utf8') > MAX_PAYLOAD_BYTES) {
35
+ io.stderr('Not submitted: the payload is larger than 64 KB, the server limit.\n');
36
+ return;
37
+ }
38
+ const result = await sendPayload(payload, ctx.fetch);
39
+ if (!result.ok) {
40
+ io.stderr(`Not submitted. ${result.message}\n`);
41
+ return;
42
+ }
43
+ const rank = result.rank;
44
+ const rankLine = rank ? ` Rank ${rank.rank} of ${rank.total}.${rank.top10Threshold === null ? '' : ` Top 10 starts at ${rank.top10Threshold}.`}` : '';
45
+ io.stdout(`Submitted.${rankLine}\n`);
46
+ }
47
+ catch (err) {
48
+ if (err instanceof RepoIdentityError || err instanceof PayloadError) {
49
+ io.stderr(`Not submitted: ${err.message}\n`);
50
+ return;
51
+ }
52
+ throw err;
53
+ }
54
+ }
55
+ async function requestQuote(run, methodology, ctx, emailFor) {
56
+ const { io, prompt } = ctx;
57
+ if (!run.health || !QUOTE_BANDS.has(run.health.band))
58
+ return;
59
+ const bandName = methodology.bands.find((b) => b.key === run.health?.band)?.name ?? run.health.band;
60
+ io.stdout(`\nYour app is ${bandName}. Peter, the creator of this scan, runs a service that fixes vibe-coded apps.\n`);
61
+ io.stdout('A quote request sends your name, email, run.json and the HTML report (scores and file paths, no source code).\n');
62
+ if (!(await prompt.confirm('Do you want to ask for a quote?')))
63
+ return;
64
+ const name = await prompt.text('Your name:');
65
+ if (name === '') {
66
+ io.stderr('A name is needed for the quote request. Nothing was sent.\n');
67
+ return;
68
+ }
69
+ const email = await emailFor();
70
+ if (email === null)
71
+ return;
72
+ const note = await prompt.text('Anything Peter should know? (optional, press Enter to skip)');
73
+ let repoHash;
74
+ try {
75
+ repoHash = (await repoIdentity(run.repo.path, ctx.git)).hash;
76
+ }
77
+ catch (err) {
78
+ if (!(err instanceof RepoIdentityError))
79
+ throw err;
80
+ io.stderr(`Nothing was sent: ${err.message}\n`);
81
+ return;
82
+ }
83
+ const body = {
84
+ contact: { name, email, ...(note === '' ? {} : { note }) },
85
+ methodologyVersion: run.methodology.version,
86
+ health: run.health.displayed,
87
+ band: run.health.band,
88
+ repoHash,
89
+ run,
90
+ report: renderReport(run, methodology),
91
+ };
92
+ try {
93
+ const response = await ctx.fetch(QUOTE_URL, {
94
+ method: 'POST',
95
+ headers: { 'content-type': 'application/json' },
96
+ body: JSON.stringify(body),
97
+ signal: AbortSignal.timeout(QUOTE_TIMEOUT_MS),
98
+ });
99
+ if (response.ok) {
100
+ io.stdout('Quote requested. Peter will reply by email.\n');
101
+ return;
102
+ }
103
+ io.stderr(`The quote request failed (${response.status}): ${(await response.text()).slice(0, 300)}. Reach Peter at https://vibecodedshit.com\n`);
104
+ }
105
+ catch (err) {
106
+ io.stderr(`The quote request failed: ${err instanceof Error ? err.message : String(err)}. Reach Peter at https://vibecodedshit.com\n`);
107
+ }
108
+ }
109
+ /** After an interactive scan: offer the leaderboard, then a quote for Vibe-coded and Fragile apps. Nothing is sent without a yes. */
110
+ export async function afterScan(run, methodology, ctx) {
111
+ if (!run.valid)
112
+ return;
113
+ let email;
114
+ const emailFor = async () => {
115
+ if (email === undefined)
116
+ email = await askEmail(ctx.prompt, ctx.io);
117
+ return email;
118
+ };
119
+ await submitToLeaderboard(run, ctx, emailFor);
120
+ await requestQuote(run, methodology, ctx, emailFor);
121
+ }
@@ -0,0 +1,97 @@
1
+ import { imageBuildCommand } from '../commands/footer.js';
2
+ import { ToolError } from '../scope/types.js';
3
+ const DOWNLOAD_URL = 'https://www.docker.com/products/docker-desktop/';
4
+ const START_TIMEOUT_MS = 180_000;
5
+ const POLL_MS = 3_000;
6
+ const ok = (code) => code === 0;
7
+ async function install(ctx) {
8
+ const { system, io } = ctx;
9
+ if (system.platform === 'darwin') {
10
+ if (!ok(await system.exec('brew', ['--version']))) {
11
+ io.stderr(`Homebrew is not installed, so Docker cannot be installed for you. Download Docker Desktop from ${DOWNLOAD_URL}\n`);
12
+ return false;
13
+ }
14
+ return ok(await system.exec('brew', ['install', '--cask', 'docker-desktop'], { inherit: true }));
15
+ }
16
+ if (system.platform === 'linux') {
17
+ return ok(await system.exec('sh', ['-c', 'curl -fsSL https://get.docker.com | sudo sh'], { inherit: true }));
18
+ }
19
+ if (system.platform === 'win32') {
20
+ const args = ['install', '-e', '--id', 'Docker.DockerDesktop', '--accept-package-agreements', '--accept-source-agreements'];
21
+ return ok(await system.exec('winget', args, { inherit: true }));
22
+ }
23
+ io.stderr(`Docker cannot be installed for you on ${system.platform}. Download it from ${DOWNLOAD_URL}\n`);
24
+ return false;
25
+ }
26
+ async function start(ctx) {
27
+ const { system, io } = ctx;
28
+ if (system.platform === 'darwin') {
29
+ await system.exec('open', ['-a', 'Docker']);
30
+ io.stdout('Starting Docker Desktop. If it asks you to accept its terms, do that in its window.\n');
31
+ }
32
+ else if (system.platform === 'linux') {
33
+ await system.exec('sudo', ['systemctl', 'start', 'docker'], { inherit: true });
34
+ }
35
+ else if (system.platform === 'win32') {
36
+ await system.exec('cmd', ['/c', 'start', '', 'C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe']);
37
+ io.stdout('Starting Docker Desktop. If it asks you to accept its terms, do that in its window.\n');
38
+ }
39
+ io.stdout('Waiting for Docker to start (up to 3 minutes)...\n');
40
+ for (let waited = 0; waited < START_TIMEOUT_MS; waited += POLL_MS) {
41
+ if (ok(await system.exec('docker', ['info'])))
42
+ return true;
43
+ await system.sleep(POLL_MS);
44
+ }
45
+ return false;
46
+ }
47
+ function notStarted(platform) {
48
+ const linux = platform === 'linux' ? ' If Docker runs but needs sudo, run `sudo usermod -aG docker $USER`, log in again and rerun the scan.' : '';
49
+ return `Docker did not start within 3 minutes.${linux}`;
50
+ }
51
+ /**
52
+ * Get Docker and the scan image ready for a full run, asking before each install, start or build.
53
+ * Any No, or a step that fails, returns the reason to fall back to a preview scan.
54
+ */
55
+ export async function setUpDocker(tag, ctx) {
56
+ const { system, prompt, io } = ctx;
57
+ if (!ok(await system.exec('docker', ['--version']))) {
58
+ if (!(await prompt.confirm('Docker is not installed. A full scan needs it. Install and start Docker now?'))) {
59
+ return { previewReason: 'Docker is not installed.' };
60
+ }
61
+ if (!(await install(ctx)))
62
+ return { previewReason: 'Docker could not be installed.' };
63
+ if (!(await start(ctx)))
64
+ return { previewReason: notStarted(system.platform) };
65
+ }
66
+ else if (!ok(await system.exec('docker', ['info']))) {
67
+ if (!(await prompt.confirm('Docker is installed but not running. Start it now?'))) {
68
+ return { previewReason: 'Docker is not running.' };
69
+ }
70
+ if (!(await start(ctx)))
71
+ return { previewReason: notStarted(system.platform) };
72
+ }
73
+ try {
74
+ return { digest: await ctx.imageDigest(tag) };
75
+ }
76
+ catch (err) {
77
+ if (!(err instanceof ToolError))
78
+ throw err;
79
+ const state = err.message.includes('out of date') ? 'is out of date' : 'is not built yet';
80
+ if (!(await prompt.confirm(`The scan image ${tag} ${state}. Build it now? The first build takes about 5 to 10 minutes.`))) {
81
+ return { previewReason: `The scan image ${tag} ${state}.` };
82
+ }
83
+ }
84
+ const build = imageBuildCommand(ctx.packageRoot, tag).split(' ');
85
+ if (!ok(await system.exec(build[0] ?? 'docker', build.slice(1), { inherit: true }))) {
86
+ return { previewReason: `Building the scan image failed. Retry with: ${build.join(' ')}` };
87
+ }
88
+ try {
89
+ io.stdout(`Built ${tag}.\n`);
90
+ return { digest: await ctx.imageDigest(tag) };
91
+ }
92
+ catch (err) {
93
+ if (!(err instanceof ToolError))
94
+ throw err;
95
+ return { previewReason: err.message };
96
+ }
97
+ }
@@ -0,0 +1,17 @@
1
+ import { createInterface } from 'node:readline/promises';
2
+ async function ask(question) {
3
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
4
+ try {
5
+ return (await rl.question(question)).trim();
6
+ }
7
+ finally {
8
+ rl.close();
9
+ }
10
+ }
11
+ export const terminalPrompter = {
12
+ async confirm(question) {
13
+ const answer = await ask(`${question} [y/N] `);
14
+ return /^y(es)?$/i.test(answer);
15
+ },
16
+ text: (question) => ask(`${question} `),
17
+ };
@@ -0,0 +1,10 @@
1
+ import { spawn } from 'node:child_process';
2
+ export const hostSystem = {
3
+ platform: process.platform,
4
+ exec: (command, args, opts = {}) => new Promise((resolve) => {
5
+ const child = spawn(command, args, { stdio: opts.inherit ? 'inherit' : 'ignore' });
6
+ child.on('error', () => resolve(null));
7
+ child.on('close', (code) => resolve(code));
8
+ }),
9
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
10
+ };
@@ -0,0 +1,36 @@
1
+ import path from 'node:path';
2
+ import { readTracked } from '../scope/tracked.js';
3
+ const DEPENDENCY_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
4
+ function stringRecord(value) {
5
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
6
+ return {};
7
+ return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === 'string'));
8
+ }
9
+ /** The `package.json` in `dir` (repo-relative, "." for the root). Missing or invalid JSON gives undefined. */
10
+ export function readPackageJson(repoPath, dir) {
11
+ const text = readTracked(repoPath, path.posix.join(dir === '' ? '.' : dir, 'package.json'));
12
+ if (text === null)
13
+ return undefined;
14
+ let json;
15
+ try {
16
+ json = JSON.parse(text);
17
+ }
18
+ catch {
19
+ return undefined;
20
+ }
21
+ if (json === null || typeof json !== 'object' || Array.isArray(json))
22
+ return undefined;
23
+ const bag = json;
24
+ const dependencies = {};
25
+ for (const field of DEPENDENCY_FIELDS)
26
+ Object.assign(dependencies, stringRecord(bag[field]));
27
+ return {
28
+ ...(typeof bag.name === 'string' ? { name: bag.name } : {}),
29
+ scripts: stringRecord(bag.scripts),
30
+ dependencies,
31
+ engines: stringRecord(bag.engines),
32
+ ...(bag.volta !== null && typeof bag.volta === 'object' ? { volta: bag.volta } : {}),
33
+ ...(typeof bag.packageManager === 'string' ? { packageManager: bag.packageManager } : {}),
34
+ ...(bag.workspaces === undefined ? {} : { workspaces: bag.workspaces }),
35
+ };
36
+ }
@@ -0,0 +1,106 @@
1
+ import path from 'node:path';
2
+ import { parse } from 'smol-toml';
3
+ import { readTracked, trackedPaths } from '../scope/tracked.js';
4
+ import { parseIni } from '../util/ini.js';
5
+ /** PEP 503 normalisation: lowercase, runs of `-`, `_` and `.` become `-`. */
6
+ export function normalizeName(name) {
7
+ return name.toLowerCase().replace(/[-_.]+/g, '-');
8
+ }
9
+ /** The distribution name at the start of a requirement string (`Django[argon2]>=4 ; python_version>"3"`). */
10
+ function requirementName(text) {
11
+ const match = /^\s*([A-Za-z0-9][A-Za-z0-9._-]*)/.exec(text);
12
+ return match?.[1] === undefined ? undefined : normalizeName(match[1]);
13
+ }
14
+ function readTomlTracked(repoPath, rel) {
15
+ const text = readTracked(repoPath, rel);
16
+ if (text === null)
17
+ return undefined;
18
+ try {
19
+ return parse(text);
20
+ }
21
+ catch {
22
+ return undefined;
23
+ }
24
+ }
25
+ const asRecord = (value) => (typeof value === 'object' && value !== null && !Array.isArray(value) ? value : {});
26
+ function addRequirements(out, value) {
27
+ if (!Array.isArray(value))
28
+ return;
29
+ for (const entry of value) {
30
+ if (typeof entry !== 'string')
31
+ continue;
32
+ const name = requirementName(entry);
33
+ if (name !== undefined)
34
+ out.add(name);
35
+ }
36
+ }
37
+ function addKeys(out, value) {
38
+ for (const key of Object.keys(asRecord(value))) {
39
+ if (key.toLowerCase() !== 'python')
40
+ out.add(normalizeName(key));
41
+ }
42
+ }
43
+ function addRequirementsFile(repoPath, rel, out, followIncludes) {
44
+ const text = readTracked(repoPath, rel);
45
+ if (text === null)
46
+ return;
47
+ for (const raw of text.split(/\r\n|\r|\n/)) {
48
+ const line = raw.replace(/(^|\s)#.*$/, '').trim();
49
+ if (line === '')
50
+ continue;
51
+ const include = /^(?:-r|--requirement)[\s=]+(\S+)/.exec(line);
52
+ if (include?.[1] !== undefined) {
53
+ if (followIncludes)
54
+ addRequirementsFile(repoPath, path.posix.normalize(path.posix.join(path.posix.dirname(rel), include[1])), out, false);
55
+ continue;
56
+ }
57
+ if (line.startsWith('-'))
58
+ continue;
59
+ const name = requirementName(line);
60
+ if (name !== undefined)
61
+ out.add(name);
62
+ }
63
+ }
64
+ /**
65
+ * Dependency names declared by the manifests in `dir` only (never an ancestor's): `[project]` dependencies and
66
+ * optional dependencies, `[dependency-groups]`, `[tool.poetry]` tables, `requirements*.txt` (with `-r` includes one
67
+ * level deep), `Pipfile` packages and `setup.cfg` `install_requires`. Names are PEP 503 normalised.
68
+ */
69
+ export function pythonDeclaredDeps(repoPath, dir) {
70
+ const out = new Set();
71
+ const at = (name) => (dir === '.' || dir === '' ? name : path.posix.join(dir, name));
72
+ const pyproject = readTomlTracked(repoPath, at('pyproject.toml'));
73
+ if (pyproject !== undefined) {
74
+ const project = asRecord(pyproject.project);
75
+ addRequirements(out, project.dependencies);
76
+ for (const list of Object.values(asRecord(project['optional-dependencies'])))
77
+ addRequirements(out, list);
78
+ for (const list of Object.values(asRecord(pyproject['dependency-groups'])))
79
+ addRequirements(out, list);
80
+ const poetry = asRecord(asRecord(pyproject.tool).poetry);
81
+ addKeys(out, poetry.dependencies);
82
+ addKeys(out, poetry['dev-dependencies']);
83
+ for (const group of Object.values(asRecord(poetry.group)))
84
+ addKeys(out, asRecord(group).dependencies);
85
+ }
86
+ const prefix = dir === '.' || dir === '' ? '' : `${dir}/`;
87
+ for (const file of trackedPaths(repoPath)) {
88
+ if (!file.startsWith(prefix))
89
+ continue;
90
+ const name = file.slice(prefix.length);
91
+ if (/^requirements[^/]*\.txt$/.test(name))
92
+ addRequirementsFile(repoPath, file, out, true);
93
+ }
94
+ const pipfile = readTomlTracked(repoPath, at('Pipfile'));
95
+ if (pipfile !== undefined) {
96
+ addKeys(out, pipfile.packages);
97
+ addKeys(out, pipfile['dev-packages']);
98
+ }
99
+ const setupCfg = readTracked(repoPath, at('setup.cfg'));
100
+ if (setupCfg !== null) {
101
+ const required = parseIni(setupCfg).options?.install_requires;
102
+ if (required !== undefined)
103
+ addRequirements(out, required.split('\n'));
104
+ }
105
+ return out;
106
+ }
@@ -0,0 +1,45 @@
1
+ import { pyAdapter } from './py/adapter.js';
2
+ import { tsAdapter } from './ts/adapter.js';
3
+ /** Explicit list. */
4
+ const D3_ADAPTERS = [tsAdapter, pyAdapter];
5
+ function adaptersFor(ctx) {
6
+ const groups = new Set(ctx.scope.files.filter((f) => f.role === 'logic').map((f) => f.group));
7
+ return D3_ADAPTERS.filter((adapter) => groups.has(adapter.group));
8
+ }
9
+ /**
10
+ * Why D3 cannot be measured as a whole: a language group has logic files but no adapter yet.
11
+ * Partial pooling would score only the languages that have one.
12
+ */
13
+ export function d3Gap(ctx) {
14
+ const covered = new Set(D3_ADAPTERS.map((adapter) => adapter.group));
15
+ const missing = [...new Set(ctx.scope.files.filter((f) => f.role === 'logic').map((f) => f.group))].filter((g) => !covered.has(g));
16
+ return missing.length === 0 ? undefined : `D3 is not implemented yet for ${missing.join(', ')}`;
17
+ }
18
+ /** The rules facet: needs no external tool. Memoized per scan. */
19
+ export function getD3Rules(ctx) {
20
+ return ctx.tools.memo(`d3:rules:${ctx.repoPath}`, async () => {
21
+ const units = [];
22
+ const warnings = [];
23
+ for (const adapter of adaptersFor(ctx)) {
24
+ const result = await adapter.rules(ctx);
25
+ units.push(...result.units);
26
+ warnings.push(...result.warnings);
27
+ }
28
+ return { units, warnings };
29
+ });
30
+ }
31
+ /** The graph facet. When any needed adapter is unavailable, the whole facet is (no partial pool). Memoized per scan. */
32
+ export function getD3Graph(ctx) {
33
+ return ctx.tools.memo(`d3:graph:${ctx.repoPath}`, async () => {
34
+ const units = [];
35
+ const warnings = [];
36
+ for (const adapter of adaptersFor(ctx)) {
37
+ const result = await adapter.graph(ctx);
38
+ if ('unavailable' in result)
39
+ return result;
40
+ units.push(...result.units);
41
+ warnings.push(...result.warnings);
42
+ }
43
+ return { units, warnings };
44
+ });
45
+ }