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,309 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { analyzePythonDeadCode } from '../python/deadcode.js';
4
+ import { isOutcome, knipFailed, runKnip } from '../tools/knip.js';
5
+ import { failureReason, unavailable } from '../tools/outcome.js';
6
+ import { createWorkCopy } from '../tools/workcopy.js';
7
+ import { readToml } from '../util/toml.js';
8
+ import { countExportedSymbols } from './ts/exports.js';
9
+ import { emptyTallies, logicFiles, tallyUnits, unitId } from './units.js';
10
+ /** methodology 4.4: vulture min-confidence 60. */
11
+ const VULTURE_MIN_CONFIDENCE = '60';
12
+ const VULTURE_LINE = /^(.+?):(\d+): unused (function|class|method) '(.+)' \((\d+)% confidence\)$/;
13
+ /** Module basenames that are entry points: never reported as unused modules (methodology 4.4). */
14
+ const ENTRY_NAMES = new Set([
15
+ '__main__', 'manage', 'wsgi', 'asgi', 'conftest', 'settings',
16
+ 'models', 'admin', 'apps', 'urls', 'views', // Django conventions
17
+ 'main', // FastAPI
18
+ ]);
19
+ function asRecord(value) {
20
+ return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
21
+ }
22
+ function pkgDirOf(pkg) {
23
+ return pkg === '.' ? '' : pkg;
24
+ }
25
+ function joinRel(...parts) {
26
+ return parts.filter((p) => p !== '').join('/');
27
+ }
28
+ // ---- TypeScript and JavaScript ----------------------------------------------------------
29
+ /** Fills the TS/JS tallies. `failed` collects the units beneath a knip root whose install and knip both failed. */
30
+ async function measureTypeScript(ctx, tallies, warnings, failed) {
31
+ const files = logicFiles(ctx.scope, 'typescript_javascript');
32
+ if (files.length === 0)
33
+ return null;
34
+ const knip = await runKnip(ctx);
35
+ if (!isOutcome(knip))
36
+ return knip;
37
+ warnings.push(...knip.warnings.map((w) => `4.4: ${w}`));
38
+ const perUnit = new Map();
39
+ for (const file of files) {
40
+ if (knipFailed(knip, file.path)) {
41
+ failed.add(unitId(file));
42
+ continue;
43
+ }
44
+ const exported = countExportedSymbols(ctx.scope.getTsSourceFile(file.path));
45
+ const issues = knip.files.get(file.path);
46
+ const unused = issues?.unusedFile ? exported : (issues?.exports ?? 0);
47
+ const key = unitId(file);
48
+ const unit = perUnit.get(key) ?? { numerator: 0, denominator: 0 };
49
+ unit.denominator += exported;
50
+ unit.numerator += unused;
51
+ perUnit.set(key, unit);
52
+ }
53
+ for (const [key, unit] of perUnit) {
54
+ const tally = tallies.get(key);
55
+ if (!tally)
56
+ continue;
57
+ if (unit.numerator > unit.denominator) {
58
+ warnings.push(`4.4: ${tally.package} (${tally.group}): knip reported ${unit.numerator} unused exports for ${unit.denominator} exported symbols, clamped`);
59
+ unit.numerator = unit.denominator;
60
+ }
61
+ tally.numerator += unit.numerator;
62
+ tally.denominator += unit.denominator;
63
+ }
64
+ return null;
65
+ }
66
+ function importRoots(pkg) {
67
+ const dir = pkgDirOf(pkg);
68
+ return [...new Set([dir, joinRel(dir, 'src'), ''])];
69
+ }
70
+ /** Module parts of `file` under import root `root`, or undefined when the file is not under it. */
71
+ function modulePartsUnder(file, root) {
72
+ if (root !== '' && !file.startsWith(`${root}/`))
73
+ return undefined;
74
+ const rel = root === '' ? file : file.slice(root.length + 1);
75
+ const parts = rel.replace(/\.py$/, '').split('/');
76
+ if (parts[parts.length - 1] === '__init__')
77
+ parts.pop();
78
+ return parts;
79
+ }
80
+ function isDistributedPackage(repoPath, pkg, pyproject) {
81
+ if (pyproject && (asRecord(pyproject.project) || asRecord(asRecord(pyproject.tool)?.poetry)))
82
+ return true;
83
+ const dir = path.join(repoPath, pkgDirOf(pkg));
84
+ return existsSync(path.join(dir, 'setup.py')) || existsSync(path.join(dir, 'setup.cfg'));
85
+ }
86
+ function scriptTargets(pyproject) {
87
+ const targets = { modules: new Set(), objects: new Set() };
88
+ const add = (scripts) => {
89
+ for (const value of Object.values(asRecord(scripts) ?? {})) {
90
+ const text = typeof value === 'string' ? value : typeof asRecord(value)?.reference === 'string' ? String(asRecord(value)?.reference) : '';
91
+ const [modulePart, objectPart] = text.split(':');
92
+ const module = (modulePart ?? '').trim();
93
+ if (module === '')
94
+ continue;
95
+ targets.modules.add(module);
96
+ const object = (objectPart ?? '').trim();
97
+ if (object !== '')
98
+ targets.objects.add(`${module}:${object}`);
99
+ }
100
+ };
101
+ add(asRecord(pyproject?.project)?.scripts);
102
+ add(asRecord(asRecord(pyproject?.tool)?.poetry)?.scripts);
103
+ return targets;
104
+ }
105
+ function isEntryModule(mod, scripts) {
106
+ const segments = mod.file.path.replace(/\.py$/, '').split('/');
107
+ const base = segments[segments.length - 1] ?? '';
108
+ if (ENTRY_NAMES.has(base))
109
+ return true;
110
+ if (segments.slice(0, -1).includes('settings'))
111
+ return true;
112
+ return [...mod.names].some((name) => scripts.modules.has(name));
113
+ }
114
+ /** Imported dotted name to the files that import it. */
115
+ function importGraph(modules, tests, pkg) {
116
+ const importedBy = new Map();
117
+ const mark = (name, importer) => {
118
+ if (name === '')
119
+ return;
120
+ const set = importedBy.get(name) ?? new Set();
121
+ set.add(importer);
122
+ importedBy.set(name, set);
123
+ };
124
+ const markWithParents = (parts, importer) => {
125
+ for (let i = 1; i <= parts.length; i += 1)
126
+ mark(parts.slice(0, i).join('.'), importer);
127
+ };
128
+ for (const mod of [...modules, ...tests]) {
129
+ for (const root of importRoots(pkg)) {
130
+ const own = modulePartsUnder(mod.file.path, root);
131
+ if (!own)
132
+ continue;
133
+ const isInit = mod.file.path.endsWith('/__init__.py') || mod.file.path === '__init__.py';
134
+ const ownPackage = isInit ? own : own.slice(0, -1);
135
+ for (const imp of mod.info.imports) {
136
+ const moduleParts = imp.module === '' ? [] : imp.module.split('.');
137
+ let base;
138
+ if (imp.level === 0) {
139
+ base = moduleParts;
140
+ }
141
+ else {
142
+ const keep = ownPackage.length - (imp.level - 1);
143
+ if (keep < 0)
144
+ continue;
145
+ base = [...ownPackage.slice(0, keep), ...moduleParts];
146
+ }
147
+ markWithParents(base, mod.file.path);
148
+ if (imp.names.length > 0 && base.length > 0) {
149
+ for (const name of imp.names)
150
+ mark([...base, name].join('.'), mod.file.path);
151
+ }
152
+ else if (imp.names.length > 0) {
153
+ for (const name of imp.names)
154
+ mark(name, mod.file.path);
155
+ }
156
+ }
157
+ }
158
+ }
159
+ return importedBy;
160
+ }
161
+ /**
162
+ * A method vulture flags is used when it may be called through a base class (methodology 4.4): its class,
163
+ * directly or through base classes defined in the repository, has a base defined outside the scanned files
164
+ * (a framework or standard-library hook), or a base class defined in the repository has a method of the same name.
165
+ */
166
+ export function isOverride(infos, file, qualname) {
167
+ const dot = qualname.indexOf('.');
168
+ if (dot === -1)
169
+ return false;
170
+ const method = qualname.slice(dot + 1);
171
+ const seen = new Set([`${file}\0${qualname.slice(0, dot)}`]);
172
+ const queue = [[file, qualname.slice(0, dot)]];
173
+ for (let next = queue.shift(); next; next = queue.shift()) {
174
+ const [classFile, className] = next;
175
+ const info = infos[classFile];
176
+ const cls = info?.ok ? info.classes[className] : undefined;
177
+ for (const base of cls?.bases ?? []) {
178
+ if (base.file === null || base.class === null)
179
+ return true;
180
+ const key = `${base.file}\0${base.class}`;
181
+ if (seen.has(key))
182
+ continue;
183
+ seen.add(key);
184
+ const baseInfo = infos[base.file];
185
+ if (baseInfo?.ok && baseInfo.symbols.some((s) => s.kind === 'method' && s.qualname === `${base.class}.${method}`))
186
+ return true;
187
+ queue.push([base.file, base.class]);
188
+ }
189
+ }
190
+ return false;
191
+ }
192
+ function matchSymbol(symbols, name, line) {
193
+ return symbols.find((s) => s.name === name && s.startLine <= line && line <= s.line);
194
+ }
195
+ async function measurePython(ctx, tallies, warnings) {
196
+ const logic = logicFiles(ctx.scope, 'python');
197
+ if (logic.length === 0)
198
+ return null;
199
+ const tests = ctx.scope.files.filter((f) => f.role === 'test' && f.group === 'python');
200
+ const vulture = await ctx.tools.resolve('vulture');
201
+ if (!vulture)
202
+ return unavailable(ctx.tools.notFoundReason('vulture'));
203
+ const infos = analyzePythonDeadCode(ctx.repoPath, [...logic, ...tests].map((f) => f.path));
204
+ let unparsed = 0;
205
+ for (const file of [...logic, ...tests])
206
+ if (infos[file.path]?.ok !== true)
207
+ unparsed += 1;
208
+ if (unparsed > 0)
209
+ warnings.push(`4.4: ${unparsed} Python files could not be parsed`);
210
+ const toModule = (file) => {
211
+ const info = infos[file.path];
212
+ if (!info || !info.ok)
213
+ return undefined;
214
+ const names = new Set();
215
+ for (const root of importRoots(file.package)) {
216
+ const parts = modulePartsUnder(file.path, root);
217
+ if (parts && parts.length > 0)
218
+ names.add(parts.join('.'));
219
+ }
220
+ return { file, info, names };
221
+ };
222
+ const packages = [...new Set(logic.map((f) => f.package))];
223
+ for (const pkg of packages) {
224
+ const unitLogic = logic.filter((f) => f.package === pkg).map(toModule).filter((m) => m !== undefined);
225
+ const unitTests = tests.filter((f) => f.package === pkg).map(toModule).filter((m) => m !== undefined);
226
+ const tally = tallies.get(unitId({ package: pkg, group: 'python' }));
227
+ if (!tally || unitLogic.length === 0)
228
+ continue;
229
+ const pyprojectPath = path.join(ctx.repoPath, joinRel(pkgDirOf(pkg), 'pyproject.toml'));
230
+ const pyproject = readToml(pyprojectPath);
231
+ if (pyproject === undefined && existsSync(pyprojectPath)) {
232
+ warnings.push(`${joinRel(pkgDirOf(pkg), 'pyproject.toml')}: invalid TOML, ignored`);
233
+ }
234
+ const distributed = isDistributedPackage(ctx.repoPath, pkg, pyproject);
235
+ const scripts = scriptTargets(pyproject);
236
+ const importedBy = importGraph(unitLogic, unitTests, pkg);
237
+ const unusedModules = new Set();
238
+ for (const mod of unitLogic) {
239
+ if (isEntryModule(mod, scripts))
240
+ continue;
241
+ const used = [...mod.names].some((name) => [...(importedBy.get(name) ?? [])].some((importer) => importer !== mod.file.path));
242
+ if (!used)
243
+ unusedModules.add(mod.file.path);
244
+ }
245
+ const copy = await createWorkCopy(ctx.repoPath, [...unitLogic, ...unitTests].map((m) => m.file.path));
246
+ let hits;
247
+ try {
248
+ const result = await ctx.tools.run(vulture, ['.', '--min-confidence', VULTURE_MIN_CONFIDENCE], { cwd: copy.dir });
249
+ const failure = failureReason(vulture, result, [0, 3]);
250
+ if (failure)
251
+ return unavailable(failure);
252
+ hits = [];
253
+ for (const line of result.stdout.split('\n')) {
254
+ const match = VULTURE_LINE.exec(line.trim());
255
+ if (!match)
256
+ continue;
257
+ hits.push({
258
+ file: path.posix.normalize((match[1] ?? '').split(path.sep).join('/')).replace(/^\.\//, ''),
259
+ line: Number(match[2]),
260
+ name: match[4] ?? '',
261
+ });
262
+ }
263
+ }
264
+ finally {
265
+ await copy.cleanup();
266
+ }
267
+ const hitsByFile = new Map();
268
+ for (const hit of hits)
269
+ hitsByFile.set(hit.file, [...(hitsByFile.get(hit.file) ?? []), hit]);
270
+ for (const mod of unitLogic) {
271
+ tally.denominator += mod.info.symbols.length;
272
+ if (unusedModules.has(mod.file.path)) {
273
+ tally.numerator += mod.info.symbols.length;
274
+ continue;
275
+ }
276
+ const counted = new Set();
277
+ const allNames = new Set(distributed ? (mod.info.allNames ?? []) : []);
278
+ for (const hit of hitsByFile.get(mod.file.path) ?? []) {
279
+ const symbol = matchSymbol(mod.info.symbols, hit.name, hit.line);
280
+ if (!symbol || symbol.decorated || allNames.has(symbol.name))
281
+ continue;
282
+ if (symbol.kind === 'method' && isOverride(infos, mod.file.path, symbol.qualname))
283
+ continue;
284
+ if ([...mod.names].some((name) => scripts.objects.has(`${name}:${symbol.qualname}`)))
285
+ continue;
286
+ counted.add(symbol);
287
+ }
288
+ tally.numerator += counted.size;
289
+ }
290
+ }
291
+ return null;
292
+ }
293
+ export const p44 = {
294
+ id: '4.4',
295
+ layer: 'static',
296
+ async measure(ctx) {
297
+ const tallies = emptyTallies([...logicFiles(ctx.scope, 'typescript_javascript'), ...logicFiles(ctx.scope, 'python')]);
298
+ const warnings = [];
299
+ const failed = new Set();
300
+ const ts = await measureTypeScript(ctx, tallies, warnings, failed);
301
+ if (ts)
302
+ return ts;
303
+ const py = await measurePython(ctx, tallies, warnings);
304
+ if (py)
305
+ return py;
306
+ const units = tallyUnits(tallies).map((unit) => (failed.has(`${unit.package}\0${unit.group}`) ? { ...unit, raw: { kind: 'failed' } } : unit));
307
+ return { status: 'measured', units, warnings };
308
+ },
309
+ };
@@ -0,0 +1,117 @@
1
+ import { mkdtemp, readFile, rm } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import path from 'node:path';
4
+ import semver from 'semver';
5
+ import { packageOf } from '../scope/packages.js';
6
+ import { failureReason, unavailable } from '../tools/outcome.js';
7
+ import { joinRel } from './deps/files.js';
8
+ import { kindGroup } from './deps/lockfiles.js';
9
+ import { parseOsvReport, severityPoints } from './deps/osv.js';
10
+ import { unitDeps } from './deps/resolved.js';
11
+ import { scopeUnits } from './deps/units.js';
12
+ const OK_CODES = [0, 1, 128];
13
+ /** One osv-scanner run on one lockfile. Flags are written from the osv-scanner v2 documentation. */
14
+ async function scanLockfile(ctx, ref) {
15
+ const tool = await ctx.tools.resolve('osv-scanner');
16
+ if (!tool)
17
+ return unavailable(ctx.tools.notFoundReason('osv-scanner'));
18
+ const major = semver.coerce(tool.version)?.major;
19
+ if (major === undefined || major < 2)
20
+ return unavailable(`osv-scanner 2.x required, found ${tool.version}`);
21
+ const abs = path.join(ctx.repoPath, ref.path);
22
+ const lockArg = ref.kind === 'requirements' && path.posix.basename(ref.path) !== 'requirements.txt' ? `requirements.txt:${abs}` : abs;
23
+ const outDir = await mkdtemp(path.join(tmpdir(), 'health-osv-'));
24
+ try {
25
+ const report = path.join(outDir, 'osv.json');
26
+ const result = await ctx.tools.run(tool, ['scan', '-L', lockArg, '--format', 'json', '--output-file', report, '--all-packages'], { cwd: path.join(ctx.repoPath, ref.dir) });
27
+ const failure = failureReason(tool, result, OK_CODES);
28
+ if (failure)
29
+ return unavailable(failure);
30
+ let text;
31
+ try {
32
+ text = await readFile(report, 'utf8');
33
+ }
34
+ catch {
35
+ if (result.exitCode === 128)
36
+ return { ref, packages: [] };
37
+ return unavailable('osv-scanner wrote no report');
38
+ }
39
+ const packages = parseOsvReport(text);
40
+ if (!packages)
41
+ return unavailable('osv-scanner report has an unexpected shape');
42
+ return { ref, packages };
43
+ }
44
+ finally {
45
+ await rm(outDir, { recursive: true, force: true });
46
+ }
47
+ }
48
+ function isScanned(value) {
49
+ return 'ref' in value;
50
+ }
51
+ export const p51 = {
52
+ id: '5.1',
53
+ layer: 'static',
54
+ async measure(ctx) {
55
+ const warnings = [];
56
+ const lockfiles = new Map();
57
+ for (const unit of scopeUnits(ctx)) {
58
+ const deps = await unitDeps(ctx, unit.package, unit.group);
59
+ if (deps.lockfiles.length === 0) {
60
+ warnings.push(`5.1: ${unit.package} has no lockfile, its dependencies were not scanned`);
61
+ continue;
62
+ }
63
+ for (const { ref } of deps.lockfiles)
64
+ lockfiles.set(ref.path, ref);
65
+ }
66
+ const weights = new Map(scopeUnits(ctx).map((u) => [`${u.package}\0${u.group}`, u.weight]));
67
+ const perUnit = new Map();
68
+ const devAdvisories = [];
69
+ const productionAdvisories = [];
70
+ let productionPoints = 0;
71
+ let devPoints = 0;
72
+ const resolvedPackages = [];
73
+ for (const ref of [...lockfiles.values()].sort((a, b) => a.path.localeCompare(b.path))) {
74
+ const scanned = await scanLockfile(ctx, ref);
75
+ if (!isScanned(scanned))
76
+ return scanned;
77
+ const group = kindGroup(ref.kind);
78
+ const owner = packageOf(joinRel(ref.dir, 'x'), ctx.scope.packages);
79
+ const key = `${owner}\0${group}`;
80
+ const unit = perUnit.get(key) ?? { package: owner, group, points: 0 };
81
+ const production = scanned.packages.filter((p) => !p.dev);
82
+ const prod = severityPoints(production, ctx.asOf);
83
+ unit.points += prod.points;
84
+ productionPoints += prod.points;
85
+ for (const a of prod.advisories) {
86
+ productionAdvisories.push({ package: owner, name: a.name, version: a.version, id: a.id, severity: a.severity });
87
+ }
88
+ perUnit.set(key, unit);
89
+ const dev = severityPoints(scanned.packages.filter((p) => p.dev), ctx.asOf);
90
+ devPoints += dev.points;
91
+ for (const a of dev.advisories) {
92
+ devAdvisories.push({ package: owner, name: a.name, version: a.version, id: a.id, severity: a.severity });
93
+ }
94
+ for (const p of scanned.packages) {
95
+ resolvedPackages.push({ package: owner, group, ecosystem: p.ecosystem, name: p.name, version: p.version, dev: p.dev });
96
+ }
97
+ }
98
+ const units = [...perUnit.values()]
99
+ .sort((a, b) => a.package.localeCompare(b.package) || a.group.localeCompare(b.group))
100
+ .map((u) => ({
101
+ package: u.package,
102
+ group: u.group,
103
+ weight: weights.get(`${u.package}\0${u.group}`) ?? 0,
104
+ raw: { kind: 'number', value: u.points },
105
+ }));
106
+ return {
107
+ status: 'measured',
108
+ units,
109
+ warnings,
110
+ reportOnly: {
111
+ productionDependencyVulnerabilities: { points: productionPoints, advisories: productionAdvisories },
112
+ devDependencyVulnerabilities: { points: devPoints, advisories: devAdvisories },
113
+ resolvedPackages,
114
+ },
115
+ };
116
+ },
117
+ };
@@ -0,0 +1,80 @@
1
+ import { normalizeName } from '../util/pep440.js';
2
+ import { unavailable } from '../tools/outcome.js';
3
+ import { resolveDirect, unitDeps } from './deps/resolved.js';
4
+ import { CONCURRENCY, fetchRegistry, majorsBehind, RegistryError } from './deps/registry.js';
5
+ import { scopeUnits } from './deps/units.js';
6
+ async function inBatches(items, size, work) {
7
+ let next = 0;
8
+ const workers = Array.from({ length: Math.min(size, items.length) }, async () => {
9
+ while (next < items.length) {
10
+ const item = items[next];
11
+ next += 1;
12
+ if (item !== undefined)
13
+ await work(item);
14
+ }
15
+ });
16
+ await Promise.all(workers);
17
+ }
18
+ export const p52 = {
19
+ id: '5.2',
20
+ layer: 'static',
21
+ async measure(ctx) {
22
+ const warnings = [];
23
+ const checked = [];
24
+ for (const unit of scopeUnits(ctx)) {
25
+ const deps = await unitDeps(ctx, unit.package, unit.group);
26
+ if (deps.lockfiles.length === 0) {
27
+ warnings.push(`5.2: ${unit.package} has no lockfile, dependencies not checked`);
28
+ continue;
29
+ }
30
+ const seen = new Set();
31
+ for (const dep of resolveDirect(deps)) {
32
+ if (!dep.registry || dep.version === undefined)
33
+ continue;
34
+ const key = unit.group === 'python' ? normalizeName(dep.name) : dep.name;
35
+ if (seen.has(key))
36
+ continue;
37
+ seen.add(key);
38
+ checked.push({ package: unit.package, group: unit.group, name: dep.name, version: dep.version, dev: dep.dev });
39
+ }
40
+ }
41
+ const wanted = new Map();
42
+ for (const dep of checked)
43
+ wanted.set(`${dep.group}:${dep.group === 'python' ? normalizeName(dep.name) : dep.name}`, { group: dep.group, name: dep.name });
44
+ const registryData = new Map();
45
+ try {
46
+ await inBatches([...wanted], CONCURRENCY, async ([key, { group, name }]) => {
47
+ registryData.set(key, await fetchRegistry(ctx, group, name));
48
+ });
49
+ }
50
+ catch (err) {
51
+ if (err instanceof RegistryError)
52
+ return unavailable(err.message);
53
+ throw err;
54
+ }
55
+ for (const [key, data] of [...registryData].sort(([a], [b]) => a.localeCompare(b))) {
56
+ if (data === null)
57
+ warnings.push(`5.2: ${key.slice(key.indexOf(':') + 1)} not found on the registry`);
58
+ }
59
+ const units = scopeUnits(ctx).map((unit) => {
60
+ const own = checked.filter((d) => d.package === unit.package && d.group === unit.group);
61
+ const { numerator, denominator } = majorsBehind(own, registryData, ctx.asOf);
62
+ return {
63
+ package: unit.package,
64
+ group: unit.group,
65
+ weight: unit.weight,
66
+ raw: { kind: 'ratio', numerator, denominator, scale: 100 },
67
+ };
68
+ });
69
+ return {
70
+ status: 'measured',
71
+ units,
72
+ warnings,
73
+ reportOnly: {
74
+ directDependencies: checked
75
+ .filter((d) => registryData.get(`${d.group}:${d.group === 'python' ? normalizeName(d.name) : d.name}`) !== null)
76
+ .map((d) => ({ package: d.package, group: d.group, name: d.name, version: d.version, dev: d.dev })),
77
+ },
78
+ };
79
+ },
80
+ };
@@ -0,0 +1,21 @@
1
+ import { unitDeps, lockLevel } from './deps/resolved.js';
2
+ import { scopeUnits } from './deps/units.js';
3
+ export const p53 = {
4
+ id: '5.3',
5
+ layer: 'static',
6
+ async measure(ctx) {
7
+ const units = [];
8
+ const warnings = [];
9
+ for (const unit of scopeUnits(ctx)) {
10
+ const deps = await unitDeps(ctx, unit.package, unit.group);
11
+ warnings.push(...(deps.manifest?.warnings ?? []), ...deps.warnings);
12
+ units.push({
13
+ package: unit.package,
14
+ group: unit.group,
15
+ weight: unit.weight,
16
+ raw: { kind: 'level', level: lockLevel(deps) },
17
+ });
18
+ }
19
+ return { status: 'measured', units, warnings: [...new Set(warnings)] };
20
+ },
21
+ };