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.
Files changed (232) hide show
  1. package/LICENSE +95 -0
  2. package/README.md +64 -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 +8 -0
  8. package/dist/card/previous.js +38 -0
  9. package/dist/card/render.js +71 -0
  10. package/dist/cli.js +121 -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 +25 -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/manifest/package-json.js +36 -0
  55. package/dist/manifest/python.js +106 -0
  56. package/dist/measure/d3/index.js +45 -0
  57. package/dist/measure/d3/presets.js +100 -0
  58. package/dist/measure/d3/py/adapter.js +67 -0
  59. package/dist/measure/d3/py/config.js +80 -0
  60. package/dist/measure/d3/py/contracts.js +57 -0
  61. package/dist/measure/d3/py/domain.js +57 -0
  62. package/dist/measure/d3/py/graph.js +214 -0
  63. package/dist/measure/d3/py/interpreter.js +31 -0
  64. package/dist/measure/d3/py/layers.js +120 -0
  65. package/dist/measure/d3/py/roots.js +58 -0
  66. package/dist/measure/d3/py/types.js +1 -0
  67. package/dist/measure/d3/scc.js +64 -0
  68. package/dist/measure/d3/ts/adapter.js +371 -0
  69. package/dist/measure/d3/ts/classify.js +107 -0
  70. package/dist/measure/d3/ts/config.js +318 -0
  71. package/dist/measure/d3/ts/domain.js +23 -0
  72. package/dist/measure/d3/ts/rules.js +65 -0
  73. package/dist/measure/d3/types.js +5 -0
  74. package/dist/measure/deps/cvss.js +66 -0
  75. package/dist/measure/deps/declared.js +195 -0
  76. package/dist/measure/deps/files.js +40 -0
  77. package/dist/measure/deps/lockfiles.js +308 -0
  78. package/dist/measure/deps/osv.js +122 -0
  79. package/dist/measure/deps/registry.js +142 -0
  80. package/dist/measure/deps/resolved.js +78 -0
  81. package/dist/measure/deps/units.js +5 -0
  82. package/dist/measure/eslint/health-plugin.js +19 -0
  83. package/dist/measure/ops/docs-commands.js +73 -0
  84. package/dist/measure/ops/docs-resolve.js +217 -0
  85. package/dist/measure/ops/entry-points.js +49 -0
  86. package/dist/measure/ops/env-example.js +20 -0
  87. package/dist/measure/ops/env-reads.js +46 -0
  88. package/dist/measure/ops/runtime.js +73 -0
  89. package/dist/measure/p1-1.js +8 -0
  90. package/dist/measure/p1-2.js +8 -0
  91. package/dist/measure/p1-3.js +8 -0
  92. package/dist/measure/p1-4.js +41 -0
  93. package/dist/measure/p1-5.js +59 -0
  94. package/dist/measure/p2-1.js +8 -0
  95. package/dist/measure/p2-2.js +44 -0
  96. package/dist/measure/p2-3.js +81 -0
  97. package/dist/measure/p2-4.js +288 -0
  98. package/dist/measure/p2-5.js +8 -0
  99. package/dist/measure/p3-1.js +16 -0
  100. package/dist/measure/p3-2.js +26 -0
  101. package/dist/measure/p3-3.js +37 -0
  102. package/dist/measure/p3-4.js +26 -0
  103. package/dist/measure/p3-5.js +26 -0
  104. package/dist/measure/p4-1.js +184 -0
  105. package/dist/measure/p4-2.js +27 -0
  106. package/dist/measure/p4-3.js +106 -0
  107. package/dist/measure/p4-4.js +309 -0
  108. package/dist/measure/p5-1.js +117 -0
  109. package/dist/measure/p5-2.js +80 -0
  110. package/dist/measure/p5-3.js +21 -0
  111. package/dist/measure/p5-4.js +161 -0
  112. package/dist/measure/p5-5.js +158 -0
  113. package/dist/measure/p6-1.js +20 -0
  114. package/dist/measure/p6-2.js +20 -0
  115. package/dist/measure/p6-3.js +40 -0
  116. package/dist/measure/p6-4.js +31 -0
  117. package/dist/measure/p6-5.js +60 -0
  118. package/dist/measure/p6-6.js +54 -0
  119. package/dist/measure/py/typecheck-config.js +95 -0
  120. package/dist/measure/registry.js +31 -0
  121. package/dist/measure/safety/py-tests.js +124 -0
  122. package/dist/measure/safety/ts-barrels.js +68 -0
  123. package/dist/measure/safety/ts-cases.js +111 -0
  124. package/dist/measure/safety/ts-credits.js +101 -0
  125. package/dist/measure/safety/ts-imports.js +63 -0
  126. package/dist/measure/safety/ts-resolve.js +43 -0
  127. package/dist/measure/ts/directives.js +37 -0
  128. package/dist/measure/ts/exports.js +60 -0
  129. package/dist/measure/ts/tsconfig.js +179 -0
  130. package/dist/measure/types.js +1 -0
  131. package/dist/measure/units.js +41 -0
  132. package/dist/methodology/load.js +159 -0
  133. package/dist/methodology/schema.js +105 -0
  134. package/dist/python/deadcode.js +27 -0
  135. package/dist/python/env.js +13 -0
  136. package/dist/python/helper.js +24 -0
  137. package/dist/python/imports.js +40 -0
  138. package/dist/python/nox.js +16 -0
  139. package/dist/python/scope.js +35 -0
  140. package/dist/python/tests.js +19 -0
  141. package/dist/python/typing.js +27 -0
  142. package/dist/report/escape.js +5 -0
  143. package/dist/report/format.js +75 -0
  144. package/dist/report/render.js +213 -0
  145. package/dist/report/roadmap.js +58 -0
  146. package/dist/report/scares.js +83 -0
  147. package/dist/report/style.js +27 -0
  148. package/dist/report-only/hotspots.js +48 -0
  149. package/dist/route/check.js +37 -0
  150. package/dist/route/files.js +74 -0
  151. package/dist/route/order.js +22 -0
  152. package/dist/route/select.js +114 -0
  153. package/dist/route/write.js +13 -0
  154. package/dist/run/read.js +89 -0
  155. package/dist/run/recompute.js +19 -0
  156. package/dist/run/scan.js +196 -0
  157. package/dist/run/schema.js +1 -0
  158. package/dist/run/score-run.js +42 -0
  159. package/dist/run/write.js +4 -0
  160. package/dist/scope/build.js +173 -0
  161. package/dist/scope/packages.js +87 -0
  162. package/dist/scope/rules.js +21 -0
  163. package/dist/scope/tracked.js +34 -0
  164. package/dist/scope/ts-lines.js +63 -0
  165. package/dist/scope/types.js +7 -0
  166. package/dist/scoring/aggregate.js +38 -0
  167. package/dist/scoring/pool.js +65 -0
  168. package/dist/scoring/score.js +81 -0
  169. package/dist/scoring/what-if.js +38 -0
  170. package/dist/scripts/checks.js +53 -0
  171. package/dist/scripts/ci.js +139 -0
  172. package/dist/scripts/entry-points/find.js +31 -0
  173. package/dist/scripts/entry-points/index.js +18 -0
  174. package/dist/scripts/entry-points/justfile.js +62 -0
  175. package/dist/scripts/entry-points/makefile.js +66 -0
  176. package/dist/scripts/entry-points/nox.js +29 -0
  177. package/dist/scripts/entry-points/package-json.js +21 -0
  178. package/dist/scripts/entry-points/pyproject.js +73 -0
  179. package/dist/scripts/entry-points/tox.js +130 -0
  180. package/dist/scripts/expand.js +300 -0
  181. package/dist/scripts/triggers.js +151 -0
  182. package/dist/scripts/types.js +1 -0
  183. package/dist/submit/constants.js +9 -0
  184. package/dist/submit/payload.js +42 -0
  185. package/dist/submit/repo-identity.js +46 -0
  186. package/dist/submit/send.js +29 -0
  187. package/dist/tools/catalog.js +46 -0
  188. package/dist/tools/image.js +180 -0
  189. package/dist/tools/kit.js +71 -0
  190. package/dist/tools/knip.js +169 -0
  191. package/dist/tools/outcome.js +21 -0
  192. package/dist/tools/resolve.js +66 -0
  193. package/dist/tools/workcopy.js +27 -0
  194. package/dist/util/ini.js +37 -0
  195. package/dist/util/pep440.js +163 -0
  196. package/dist/util/toml.js +27 -0
  197. package/docker/Dockerfile +142 -0
  198. package/docker/bin/health-capture +5 -0
  199. package/docker/bin/health-coverage-install +5 -0
  200. package/docker/bin/health-eslint +66 -0
  201. package/docker/bin/health-pytest +10 -0
  202. package/docker/bin/health-tsc +10 -0
  203. package/docker/bin/health-venv +11 -0
  204. package/docker/config/eslint.config.mjs +4 -0
  205. package/docker/config/eslint.rules.mjs +65 -0
  206. package/docker/config/eslint.untyped.config.mjs +4 -0
  207. package/docker/config/knip.json +1 -0
  208. package/docker/config/ruff.toml +7 -0
  209. package/methodology/LICENSE +396 -0
  210. package/methodology/v1.0/METHODOLOGY.md +1176 -0
  211. package/methodology/v1.0/dimensions/1-safety-net.yaml +240 -0
  212. package/methodology/v1.0/dimensions/2-type-static-rigor.yaml +289 -0
  213. package/methodology/v1.0/dimensions/3-architecture-boundaries.yaml +266 -0
  214. package/methodology/v1.0/dimensions/4-complexity-maintainability.yaml +192 -0
  215. package/methodology/v1.0/dimensions/5-dependency-security-hygiene.yaml +223 -0
  216. package/methodology/v1.0/dimensions/6-operability-feedback-loop.yaml +279 -0
  217. package/methodology/v1.0/methodology.yaml +242 -0
  218. package/methodology/v1.0/presets/django.yaml +43 -0
  219. package/methodology/v1.0/presets/express.yaml +47 -0
  220. package/methodology/v1.0/presets/fastapi.yaml +52 -0
  221. package/methodology/v1.0/presets/generic.yaml +41 -0
  222. package/methodology/v1.0/presets/nextjs.yaml +50 -0
  223. package/methodology/v1.0/scope.yaml +115 -0
  224. package/package.json +61 -4
  225. package/python/dead_code_helper.py +265 -0
  226. package/python/env_helper.py +71 -0
  227. package/python/graph_helper.py +277 -0
  228. package/python/imports_helper.py +132 -0
  229. package/python/nox_helper.py +79 -0
  230. package/python/scope_helper.py +96 -0
  231. package/python/tests_helper.py +171 -0
  232. package/python/typing_helper.py +235 -0
@@ -0,0 +1 @@
1
+ export const EXIT = { ok: 0, usage: 1, methodology: 2, noLogic: 3, tool: 4, diffRefused: 5, sendFailed: 6 };
@@ -0,0 +1,54 @@
1
+ import path from 'node:path';
2
+ import { parseArgs } from 'node:util';
3
+ import { compareRuns, DiffInputError } from '../diff/compare.js';
4
+ import { conditionRefusals } from '../diff/conditions.js';
5
+ import { formatDiff, formatRefusal } from '../diff/format.js';
6
+ import { loadMethodology, MethodologyError } from '../methodology/load.js';
7
+ import { readRun, RunReadError } from '../run/read.js';
8
+ import { EXIT } from './common.js';
9
+ const DIFF_USAGE = 'Usage: vibecodedshit diff <before.json> <after.json> [--json]\n';
10
+ export function runDiff(argv, io) {
11
+ let parsed;
12
+ try {
13
+ parsed = parseArgs({ args: argv, allowPositionals: true, options: { json: { type: 'boolean' } } });
14
+ }
15
+ catch (err) {
16
+ io.stderr(`${err instanceof Error ? err.message : String(err)}\n${DIFF_USAGE}`);
17
+ return EXIT.usage;
18
+ }
19
+ const [beforeFile, afterFile] = parsed.positionals;
20
+ if (!beforeFile || !afterFile || parsed.positionals.length > 2) {
21
+ io.stderr(DIFF_USAGE);
22
+ return EXIT.usage;
23
+ }
24
+ const files = [path.resolve(io.cwd, beforeFile), path.resolve(io.cwd, afterFile)];
25
+ try {
26
+ const before = { run: readRun(files[0]), file: files[0] };
27
+ const after = { run: readRun(files[1]), file: files[1] };
28
+ // Mismatched runs are refused before any methodology is loaded, so a version this CLI lacks does not hide the refusal.
29
+ const early = conditionRefusals(before.run, after.run);
30
+ const result = early.length > 0
31
+ ? { schemaVersion: 1, compared: false, refusals: early }
32
+ : compareRuns(before, after, loadMethodology(before.run.methodology.version));
33
+ if (!result.compared) {
34
+ if (parsed.values.json)
35
+ io.stdout(`${JSON.stringify(result)}\n`);
36
+ else
37
+ io.stderr(formatRefusal(result));
38
+ return EXIT.diffRefused;
39
+ }
40
+ io.stdout(parsed.values.json ? `${JSON.stringify(result)}\n` : formatDiff(result));
41
+ return EXIT.ok;
42
+ }
43
+ catch (err) {
44
+ if (err instanceof RunReadError || err instanceof DiffInputError) {
45
+ io.stderr(`${err.message}\n`);
46
+ return EXIT.usage;
47
+ }
48
+ if (err instanceof MethodologyError) {
49
+ io.stderr(`Methodology error: ${err.message}\n`);
50
+ return EXIT.methodology;
51
+ }
52
+ throw err;
53
+ }
54
+ }
@@ -0,0 +1,25 @@
1
+ import path from 'node:path';
2
+ const PRIVACY = "Your code stayed on this machine. The scan only looked up your dependencies' names and versions on the public npm and PyPI registries (and OSV, if osv-scanner is installed).";
3
+ /** The Docker command a user of an npx install runs to get the full-run image, with the package root resolved. */
4
+ export function imageBuildCommand(packageRoot, tag) {
5
+ const docker = path.join(packageRoot, 'docker');
6
+ return `docker build -t ${tag} -f ${path.join(docker, 'Dockerfile')} ${docker}`;
7
+ }
8
+ /** Lines printed after the scan summary: what a preview skipped, that nothing was uploaded, and how to submit. */
9
+ export function footer(run, outFile) {
10
+ const lines = [];
11
+ if (run.mode === 'preview') {
12
+ const skipped = run.parameters.filter((p) => p.status === 'not_measured' && p.reason?.startsWith('execution layer')).map((p) => p.id);
13
+ if (skipped.length > 0)
14
+ lines.push(`Skipped (needs Docker): ${skipped.join(', ')}. Run a full scan with Docker to measure them.`);
15
+ }
16
+ lines.push(PRIVACY);
17
+ if (run.valid && run.comparable && run.mode === 'full') {
18
+ lines.push(`To compare your result on app.vibecodedshit.com, run: npx vibecodedshit submit ${outFile} --email <you@example.com>`);
19
+ lines.push('It prints the payload first and sends nothing unless you add --confirm.');
20
+ }
21
+ else {
22
+ lines.push('Only full, comparable runs (Docker needed) can be submitted. Nothing is submitted unless you run the submit command yourself.');
23
+ }
24
+ return `${lines.join('\n')}\n`;
25
+ }
@@ -0,0 +1,49 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { parseArgs } from 'node:util';
4
+ import { loadMethodology, MethodologyError } from '../methodology/load.js';
5
+ import { renderReport } from '../report/render.js';
6
+ import { roadmapLines } from '../report/roadmap.js';
7
+ import { scareLines } from '../report/scares.js';
8
+ import { readRun, RunReadError } from '../run/read.js';
9
+ import { EXIT } from './common.js';
10
+ const REPORT_USAGE = 'Usage: vibecodedshit report <run.json> [--out <file>]\n';
11
+ export function runReport(argv, io) {
12
+ let parsed;
13
+ try {
14
+ parsed = parseArgs({ args: argv, allowPositionals: true, options: { out: { type: 'string' } } });
15
+ }
16
+ catch (err) {
17
+ io.stderr(`${err instanceof Error ? err.message : String(err)}\n${REPORT_USAGE}`);
18
+ return EXIT.usage;
19
+ }
20
+ const [input] = parsed.positionals;
21
+ if (!input || parsed.positionals.length > 1) {
22
+ io.stderr(REPORT_USAGE);
23
+ return EXIT.usage;
24
+ }
25
+ // The default is the current directory, never the scanned repository, the same rule as run.json.
26
+ const outFile = path.resolve(io.cwd, parsed.values.out ?? 'health-report.html');
27
+ try {
28
+ const run = readRun(path.resolve(io.cwd, input));
29
+ if (!run.valid) {
30
+ io.stderr(`run.json is invalid: ${run.invalidReason ?? 'no reason given'}\n`);
31
+ return EXIT.usage;
32
+ }
33
+ const methodology = loadMethodology(run.methodology.version);
34
+ writeFileSync(outFile, renderReport(run, methodology), 'utf8');
35
+ io.stdout(`${[outFile, ...roadmapLines(run, methodology), ...scareLines(run)].join('\n')}\n`);
36
+ return EXIT.ok;
37
+ }
38
+ catch (err) {
39
+ if (err instanceof RunReadError) {
40
+ io.stderr(`${err.message}\n`);
41
+ return EXIT.usage;
42
+ }
43
+ if (err instanceof MethodologyError) {
44
+ io.stderr(`Methodology error: ${err.message}\n`);
45
+ return EXIT.methodology;
46
+ }
47
+ throw err;
48
+ }
49
+ }
@@ -0,0 +1,62 @@
1
+ import path from 'node:path';
2
+ import { parseArgs } from 'node:util';
3
+ import { loadMethodology, MethodologyError } from '../methodology/load.js';
4
+ import { readRun, RunReadError } from '../run/read.js';
5
+ import { checkRouteFiles } from '../route/check.js';
6
+ import { buildRouteFiles } from '../route/files.js';
7
+ import { buildRoute, routeLines } from '../route/select.js';
8
+ import { writeRouteFiles } from '../route/write.js';
9
+ import { EXIT } from './common.js';
10
+ const ROUTE_USAGE = 'Usage: vibecodedshit route <run.json> --out <dir>\n vibecodedshit route --check <dir> <run.json>\n';
11
+ export function runRoute(argv, io) {
12
+ let parsed;
13
+ try {
14
+ parsed = parseArgs({ args: argv, allowPositionals: true, options: { out: { type: 'string' }, check: { type: 'string' } } });
15
+ }
16
+ catch (err) {
17
+ io.stderr(`${err instanceof Error ? err.message : String(err)}\n${ROUTE_USAGE}`);
18
+ return EXIT.usage;
19
+ }
20
+ const [input] = parsed.positionals;
21
+ const { out, check } = parsed.values;
22
+ if (!input || parsed.positionals.length > 1 || (out === undefined) === (check === undefined)) {
23
+ io.stderr(ROUTE_USAGE);
24
+ return EXIT.usage;
25
+ }
26
+ try {
27
+ const run = readRun(path.resolve(io.cwd, input));
28
+ if (!run.valid) {
29
+ io.stderr(`run.json is invalid: ${run.invalidReason ?? 'no reason given'}\n`);
30
+ return EXIT.usage;
31
+ }
32
+ const methodology = loadMethodology(run.methodology.version);
33
+ const plan = buildRoute(run, methodology);
34
+ const files = buildRouteFiles(run, methodology, plan);
35
+ if (check !== undefined) {
36
+ const problems = checkRouteFiles(path.resolve(io.cwd, check), files);
37
+ if (problems.length > 0) {
38
+ io.stderr(`${problems.join('\n')}\n`);
39
+ return EXIT.usage;
40
+ }
41
+ io.stdout('Route files match run.json\n');
42
+ return EXIT.ok;
43
+ }
44
+ const lines = routeLines(plan);
45
+ if (files.length > 0 && out !== undefined)
46
+ writeRouteFiles(path.resolve(io.cwd, out), files);
47
+ if (lines.length > 0)
48
+ io.stdout(`${lines.join('\n')}\n`);
49
+ return EXIT.ok;
50
+ }
51
+ catch (err) {
52
+ if (err instanceof RunReadError) {
53
+ io.stderr(`${err.message}\n`);
54
+ return EXIT.usage;
55
+ }
56
+ if (err instanceof MethodologyError) {
57
+ io.stderr(`Methodology error: ${err.message}\n`);
58
+ return EXIT.methodology;
59
+ }
60
+ throw err;
61
+ }
62
+ }
@@ -0,0 +1,125 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { parseArgs } from 'node:util';
5
+ import { readRun, RunReadError } from '../run/read.js';
6
+ import { EMAIL_PATTERN, MAX_EMAIL_CHARS, MAX_PAYLOAD_BYTES, MAX_SUMMARY_CHARS } from '../submit/constants.js';
7
+ import { buildPayload, PayloadError } from '../submit/payload.js';
8
+ import { repoIdentity, RepoIdentityError } from '../submit/repo-identity.js';
9
+ import { sendPayload } from '../submit/send.js';
10
+ import { EXIT } from './common.js';
11
+ export const SUBMIT_USAGE = 'Usage: vibecodedshit submit <run.json> --email <addr> [--confirm] [--newsletter] [--include-repo-name] [--summary-file <path>]\n';
12
+ export const defaultGit = (repoPath, args) => new Promise((resolve) => {
13
+ execFile('git', ['-C', repoPath, ...args], { encoding: 'utf8' }, (err, stdout) => resolve(err ? null : stdout));
14
+ });
15
+ export async function runSubmit(argv, io, deps = {}) {
16
+ let parsed;
17
+ try {
18
+ parsed = parseArgs({
19
+ args: argv,
20
+ allowPositionals: true,
21
+ options: {
22
+ confirm: { type: 'boolean' },
23
+ newsletter: { type: 'boolean' },
24
+ 'include-repo-name': { type: 'boolean' },
25
+ email: { type: 'string' },
26
+ 'summary-file': { type: 'string' },
27
+ },
28
+ });
29
+ }
30
+ catch (err) {
31
+ io.stderr(`${err instanceof Error ? err.message : String(err)}\n${SUBMIT_USAGE}`);
32
+ return EXIT.usage;
33
+ }
34
+ const [runFile] = parsed.positionals;
35
+ if (!runFile || parsed.positionals.length > 1) {
36
+ io.stderr(SUBMIT_USAGE);
37
+ return EXIT.usage;
38
+ }
39
+ const refuse = (message) => {
40
+ io.stderr(`${message}\n`);
41
+ return EXIT.usage;
42
+ };
43
+ const email = parsed.values.email;
44
+ if (email === undefined)
45
+ return refuse('--email is required.');
46
+ if (email.length > MAX_EMAIL_CHARS || !EMAIL_PATTERN.test(email))
47
+ return refuse('--email is not a valid address.');
48
+ let run;
49
+ try {
50
+ run = readRun(path.resolve(io.cwd, runFile));
51
+ }
52
+ catch (err) {
53
+ if (err instanceof RunReadError)
54
+ return refuse(err.message);
55
+ throw err;
56
+ }
57
+ if (run.mode !== 'full') {
58
+ return refuse('Preview runs are not comparable and cannot be submitted. Run a full scan (Docker required) and submit that run.');
59
+ }
60
+ if (!run.valid)
61
+ return refuse(`run.json is invalid: ${run.invalidReason ?? 'no reason given'}.`);
62
+ if (!run.comparable)
63
+ return refuse(`This run is not comparable: ${run.comparableReason ?? 'no reason given'}.`);
64
+ let summary;
65
+ const summaryFile = parsed.values['summary-file'];
66
+ if (summaryFile !== undefined) {
67
+ try {
68
+ summary = readFileSync(path.resolve(io.cwd, summaryFile), 'utf8').trim();
69
+ }
70
+ catch (err) {
71
+ return refuse(`Cannot read the summary file: ${err instanceof Error ? err.message : String(err)}`);
72
+ }
73
+ if (summary === '')
74
+ return refuse('The summary file is empty.');
75
+ if (summary.length > MAX_SUMMARY_CHARS)
76
+ return refuse(`The summary is longer than ${MAX_SUMMARY_CHARS} characters, the server limit.`);
77
+ const paths = [run.repo.path, ...run.parameters.flatMap((p) => (p.units ?? []).map((u) => u.package))].filter((p) => p !== '' && p !== '.');
78
+ if (paths.some((p) => summary?.includes(p)))
79
+ return refuse('The summary contains a file path. Remove it and try again.');
80
+ }
81
+ let identity;
82
+ try {
83
+ identity = await repoIdentity(run.repo.path, deps.git ?? defaultGit);
84
+ }
85
+ catch (err) {
86
+ if (err instanceof RepoIdentityError)
87
+ return refuse(err.message);
88
+ throw err;
89
+ }
90
+ let payload;
91
+ try {
92
+ payload = buildPayload({
93
+ run,
94
+ repoHash: identity.hash,
95
+ repoName: parsed.values['include-repo-name'] ? identity.name : undefined,
96
+ summary,
97
+ email,
98
+ newsletter: parsed.values.newsletter,
99
+ });
100
+ }
101
+ catch (err) {
102
+ if (err instanceof PayloadError)
103
+ return refuse(err.message);
104
+ throw err;
105
+ }
106
+ const body = JSON.stringify(payload, null, 2);
107
+ if (Buffer.byteLength(JSON.stringify(payload), 'utf8') > MAX_PAYLOAD_BYTES)
108
+ return refuse('Payload is larger than 64 KB, the server limit.');
109
+ io.stdout(`${body}\n`);
110
+ if (!parsed.values.confirm) {
111
+ io.stdout('Not sent. Add --confirm to send.\n');
112
+ return EXIT.ok;
113
+ }
114
+ const result = await sendPayload(payload, deps.fetch ?? fetch);
115
+ if (!result.ok) {
116
+ io.stderr(`${result.message}\n`);
117
+ return EXIT.sendFailed;
118
+ }
119
+ io.stdout('Submitted. Thank you.\n');
120
+ if (result.rank) {
121
+ const { rank, total, top10Threshold } = result.rank;
122
+ io.stdout(`Rank ${rank} of ${total}.${top10Threshold === null ? '' : ` Top 10 starts at ${top10Threshold}.`}\n`);
123
+ }
124
+ return EXIT.ok;
125
+ }
@@ -0,0 +1,77 @@
1
+ function round1(value) {
2
+ return (Math.round(value * 10) / 10).toFixed(1);
3
+ }
4
+ /** Whole numbers print bare (0 findings, 178 points), anything else with one decimal (3.1 seconds). */
5
+ function formatNumber(value) {
6
+ return Number.isInteger(value) ? String(value) : round1(value);
7
+ }
8
+ /** One raw value as text: scale-100 ratios in %, scale-1000 ratios and numbers with the YAML unit. Null when it has no value. */
9
+ export function formatRaw(raw, unit) {
10
+ switch (raw.kind) {
11
+ case 'ratio': {
12
+ if (raw.denominator === 0)
13
+ return null;
14
+ const value = round1((raw.numerator / raw.denominator) * raw.scale);
15
+ return raw.scale === 100 ? `${value}%` : `${value} ${unit}`;
16
+ }
17
+ case 'number':
18
+ return unit === 'count' ? formatNumber(raw.value) : `${formatNumber(raw.value)} ${unit}`;
19
+ case 'level':
20
+ return raw.level;
21
+ case 'failed':
22
+ return 'install failed';
23
+ case 'components':
24
+ return Object.entries(raw.components)
25
+ .map(([name, component]) => `${name}=${formatRaw(component, unit) ?? 'n/a'}`)
26
+ .join(', ');
27
+ }
28
+ }
29
+ /**
30
+ * The raw value printed for a measured parameter: the pooled value when there is one, otherwise the unit values
31
+ * (one unit alone, several as `package/group: value` joined with `; `). Levels list the distinct levels, as before.
32
+ */
33
+ export function shownRaw(param, unit) {
34
+ if (param.raw) {
35
+ const pooled = formatRaw(param.raw, unit);
36
+ if (pooled !== null)
37
+ return pooled;
38
+ }
39
+ const units = param.units ?? [];
40
+ if (units.length > 0 && units.every((u) => u.raw.kind === 'level')) {
41
+ return [...new Set(units.map((u) => u.raw.level))].join(', ');
42
+ }
43
+ const shown = units.flatMap((u) => {
44
+ const value = formatRaw(u.raw, unit);
45
+ return value === null ? [] : [{ label: `${u.package}/${u.group}`, value }];
46
+ });
47
+ if (shown.length === 0)
48
+ return 'n/a';
49
+ if (shown.length === 1)
50
+ return shown[0]?.value ?? 'n/a';
51
+ return shown.map((s) => `${s.label}: ${s.value}`).join('; ');
52
+ }
53
+ export function summarize(run, methodology) {
54
+ const lines = [];
55
+ if (run.health) {
56
+ const band = methodology.bands.find((b) => b.key === run.health?.band);
57
+ lines.push(`Health ${run.health.displayed} ${band?.name ?? run.health.band} (${run.mode === 'preview' ? 'preview, ' : ''}${run.comparable ? 'comparable' : 'not comparable'})`);
58
+ }
59
+ for (const dimension of run.dimensions) {
60
+ if (dimension.score === null)
61
+ continue;
62
+ const def = methodology.dimensions.find((d) => d.id === dimension.id);
63
+ lines.push(`Dimension ${dimension.id} ${def?.name ?? dimension.key}: ${Math.floor(dimension.score + 0.5 + 1e-9)}`);
64
+ }
65
+ const defs = new Map(methodology.dimensions.flatMap((d) => d.parameters).map((p) => [p.id, p]));
66
+ for (const param of run.parameters) {
67
+ const def = defs.get(param.id);
68
+ if (param.status === 'not_measured' && param.reason && !/^(execution layer|not implemented)/.test(param.reason)) {
69
+ lines.push(`${param.id} ${def?.name ?? ''}: not measured (${param.reason})`);
70
+ continue;
71
+ }
72
+ if (param.status !== 'measured')
73
+ continue;
74
+ lines.push(`${param.id} ${def?.name ?? ''}: ${shownRaw(param, def?.raw.unit ?? '')} -> ${Math.floor((param.score ?? 0) + 0.5 + 1e-9)}`);
75
+ }
76
+ return `${lines.join('\n')}\n`;
77
+ }
@@ -0,0 +1,73 @@
1
+ import { roundHalfUp } from '../scoring/aggregate.js';
2
+ import { conditionRefusals, notComparableRefusals } from './conditions.js';
3
+ /** methodology.yaml `regression.flag_when`: a dimension drops by this much or more, unrounded. */
4
+ const REGRESSION_DROP = 3.0;
5
+ const TOLERANCE = 1e-9;
6
+ export class DiffInputError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = 'DiffInputError';
10
+ }
11
+ }
12
+ function side(run, file, methodology) {
13
+ const health = run.health;
14
+ if (!health)
15
+ throw new DiffInputError(`${file}: run has no health score`);
16
+ const band = methodology.bands.find((b) => b.key === health.band);
17
+ if (!band)
18
+ throw new DiffInputError(`${file}: unknown band "${health.band}" for methodology ${methodology.version}`);
19
+ return {
20
+ path: run.repo.path,
21
+ headCommit: run.repo.headCommit,
22
+ treeHash: run.repo.treeHash,
23
+ health: { score: health.score, displayed: health.displayed, band: band.key, bandName: band.name },
24
+ };
25
+ }
26
+ function dimensionScore(run, id) {
27
+ return run.dimensions.find((d) => d.id === id)?.score ?? null;
28
+ }
29
+ /** Refuse when the five conditions differ or a run cannot be compared; otherwise compare the stored scores. */
30
+ export function compareRuns(a, b, methodology) {
31
+ const refusals = conditionRefusals(a.run, b.run);
32
+ if (refusals.length === 0)
33
+ refusals.push(...notComparableRefusals(a.run, b.run));
34
+ if (refusals.length > 0)
35
+ return { schemaVersion: 1, compared: false, refusals };
36
+ const before = side(a.run, a.file, methodology);
37
+ const after = side(b.run, b.file, methodology);
38
+ const rank = (key) => methodology.bands.findIndex((band) => band.key === key);
39
+ const bandDrop = rank(after.health.band) < rank(before.health.band);
40
+ const dimensions = methodology.dimensions.map((def) => {
41
+ const from = dimensionScore(a.run, def.id);
42
+ const to = dimensionScore(b.run, def.id);
43
+ const both = from !== null && to !== null;
44
+ return {
45
+ id: def.id,
46
+ key: def.key,
47
+ name: def.name,
48
+ before: from === null ? null : roundHalfUp(from),
49
+ after: to === null ? null : roundHalfUp(to),
50
+ delta: both ? roundHalfUp(to - from) : null,
51
+ regressed: both && from - to >= REGRESSION_DROP - TOLERANCE,
52
+ };
53
+ });
54
+ const regressed = dimensions.filter((d) => d.regressed).map((d) => d.id);
55
+ return {
56
+ schemaVersion: 1,
57
+ compared: true,
58
+ methodologyVersion: methodology.version,
59
+ asOf: a.run.asOf,
60
+ before,
61
+ after,
62
+ healthDelta: roundHalfUp(after.health.score - before.health.score, 1),
63
+ bandChange: {
64
+ from: before.health.band,
65
+ to: after.health.band,
66
+ fromName: before.health.bandName,
67
+ toName: after.health.bandName,
68
+ dropped: bandDrop,
69
+ },
70
+ dimensions,
71
+ regression: { flag: regressed.length > 0 || bandDrop, dimensions: regressed, bandDrop },
72
+ };
73
+ }
@@ -0,0 +1,87 @@
1
+ const NONE = '(none)';
2
+ /** Layers as written, for the message. */
3
+ function layerText(run) {
4
+ return [...new Set(run.layers)].join('+') || NONE;
5
+ }
6
+ /** Layers as a set, for the comparison. */
7
+ function layerKey(run) {
8
+ return [...new Set(run.layers)].sort().join('+');
9
+ }
10
+ /** Flatten nested objects to dotted keys, so a differing leaf is named exactly. */
11
+ function flatten(value, prefix, out) {
12
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
13
+ for (const [key, inner] of Object.entries(value))
14
+ flatten(inner, `${prefix}.${key}`, out);
15
+ return;
16
+ }
17
+ out.set(prefix, JSON.stringify(value));
18
+ }
19
+ function shown(json) {
20
+ if (json === undefined)
21
+ return NONE;
22
+ try {
23
+ const value = JSON.parse(json);
24
+ return typeof value === 'string' ? value : json;
25
+ }
26
+ catch {
27
+ return json;
28
+ }
29
+ }
30
+ function toolDifferences(a, b) {
31
+ const left = new Map();
32
+ const right = new Map();
33
+ flatten(a.tools, 'tools', left);
34
+ flatten(b.tools, 'tools', right);
35
+ const keys = [...new Set([...left.keys(), ...right.keys()])].sort();
36
+ return keys.filter((k) => left.get(k) !== right.get(k)).map((k) => `${k} a=${shown(left.get(k))} b=${shown(right.get(k))}`);
37
+ }
38
+ function ignoreDifferences(a, b) {
39
+ const out = [];
40
+ if (a.ignoreSet.healthignoreSha256 !== b.ignoreSet.healthignoreSha256)
41
+ out.push('.healthignore hash');
42
+ if (a.ignoreSet.gitleaksignoreSha256 !== b.ignoreSet.gitleaksignoreSha256)
43
+ out.push('.gitleaksignore hash');
44
+ return out;
45
+ }
46
+ /** The five `delta.conditions` of methodology.yaml, in order. Every mismatch is reported. */
47
+ export function conditionRefusals(a, b) {
48
+ const refusals = [];
49
+ if (layerKey(a) !== layerKey(b)) {
50
+ refusals.push({ condition: 'layers', message: `layers differ (a: ${layerText(a)}, b: ${layerText(b)})` });
51
+ }
52
+ const tools = toolDifferences(a, b);
53
+ if (tools.length > 0)
54
+ refusals.push({ condition: 'tool_versions', message: `tool versions differ: ${tools.join(', ')}` });
55
+ if (a.methodology.version !== b.methodology.version) {
56
+ refusals.push({
57
+ condition: 'methodology_version',
58
+ message: `methodology version differs (a: ${a.methodology.version}, b: ${b.methodology.version}); runs from different versions are never compared`,
59
+ });
60
+ }
61
+ if (a.asOf !== b.asOf) {
62
+ refusals.push({ condition: 'as_of_date', message: `as-of date differs (a: ${a.asOf}, b: ${b.asOf})` });
63
+ }
64
+ const ignore = ignoreDifferences(a, b);
65
+ if (ignore.length > 0)
66
+ refusals.push({ condition: 'ignore_set', message: `ignore set differs: ${ignore.join(', ')}` });
67
+ return refusals;
68
+ }
69
+ /** A run that cannot be compared at all: invalid, preview or without a health score. */
70
+ export function notComparableRefusals(before, after) {
71
+ const refusals = [];
72
+ for (const [side, run] of [
73
+ ['before', before],
74
+ ['after', after],
75
+ ]) {
76
+ let reason = null;
77
+ if (!run.valid)
78
+ reason = run.invalidReason ?? 'run is invalid';
79
+ else if (!run.comparable)
80
+ reason = run.comparableReason ?? 'run is not comparable';
81
+ else if (run.health === null)
82
+ reason = 'run has no health score';
83
+ if (reason !== null)
84
+ refusals.push({ condition: 'run_not_comparable', message: `${side} run is not comparable: ${reason}` });
85
+ }
86
+ return refusals;
87
+ }
@@ -0,0 +1,23 @@
1
+ function signed(value, digits) {
2
+ const text = value.toFixed(digits);
3
+ return value >= 0 ? `+${text}` : text;
4
+ }
5
+ /** Human output: health line, one line per changed dimension, regression line. */
6
+ export function formatDiff(result) {
7
+ const lines = [`Health ${result.after.health.displayed} ${result.after.health.bandName} (${signed(result.healthDelta, 1)})`];
8
+ for (const d of result.dimensions) {
9
+ if (d.delta === null || d.delta === 0)
10
+ continue;
11
+ lines.push(`Dimension ${d.id} ${d.name}: ${signed(d.delta, 0)}`);
12
+ }
13
+ const regressions = result.dimensions
14
+ .filter((d) => d.regressed)
15
+ .map((d) => `${d.name} ${signed(d.delta ?? 0, 0)}`);
16
+ if (result.regression.bandDrop)
17
+ regressions.push(`band ${result.bandChange.fromName} -> ${result.bandChange.toName}`);
18
+ lines.push(`Regression: ${regressions.length === 0 ? 'none' : regressions.join(', ')}`);
19
+ return `${lines.join('\n')}\n`;
20
+ }
21
+ export function formatRefusal(result) {
22
+ return `${result.refusals.map((r) => `Refused: ${r.message}`).join('\n')}\n`;
23
+ }