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,40 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { trackedPaths } from '../../scope/tracked.js';
4
+ const sets = new Map();
5
+ export function trackedSet(repoPath) {
6
+ const key = path.resolve(repoPath);
7
+ const cached = sets.get(key);
8
+ if (cached)
9
+ return cached;
10
+ const made = new Set(trackedPaths(key));
11
+ sets.set(key, made);
12
+ return made;
13
+ }
14
+ export function joinRel(...parts) {
15
+ return parts.filter((p) => p !== '' && p !== '.').join('/');
16
+ }
17
+ /** Text of a tracked file of any size, or null when it is not tracked or unreadable. */
18
+ export function readTrackedText(repoPath, rel) {
19
+ if (!trackedSet(repoPath).has(rel))
20
+ return null;
21
+ try {
22
+ return readFileSync(path.join(path.resolve(repoPath), rel), 'utf8');
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ /** Tracked files that sit directly in `dir` ('' for the repository root). */
29
+ export function trackedIn(repoPath, dir) {
30
+ const prefix = dir === '' ? '' : `${dir}/`;
31
+ const found = [];
32
+ for (const file of trackedSet(repoPath)) {
33
+ if (!file.startsWith(prefix))
34
+ continue;
35
+ if (file.slice(prefix.length).includes('/'))
36
+ continue;
37
+ found.push(file);
38
+ }
39
+ return found.sort();
40
+ }
@@ -0,0 +1,308 @@
1
+ import path from 'node:path';
2
+ import semver from 'semver';
3
+ import ts from 'typescript';
4
+ import { parse as parseYaml } from 'yaml';
5
+ import { normalizeName, satisfies as pySatisfies } from '../../util/pep440.js';
6
+ import { readToml } from '../../util/toml.js';
7
+ import { requirementLines } from './declared.js';
8
+ import { readTrackedText, trackedIn } from './files.js';
9
+ export function kindGroup(kind) {
10
+ return ['uv', 'poetry', 'pdm', 'pipfile', 'requirements'].includes(kind) ? 'python' : 'typescript_javascript';
11
+ }
12
+ const NAMED = {
13
+ 'package-lock.json': 'package-lock',
14
+ 'npm-shrinkwrap.json': 'npm-shrinkwrap',
15
+ 'pnpm-lock.yaml': 'pnpm',
16
+ 'yarn.lock': 'yarn',
17
+ 'bun.lock': 'bun',
18
+ 'uv.lock': 'uv',
19
+ 'poetry.lock': 'poetry',
20
+ 'pdm.lock': 'pdm',
21
+ 'Pipfile.lock': 'pipfile',
22
+ };
23
+ /**
24
+ * JS lockfiles in the install step's preference order. With several in one directory, the first one
25
+ * present is the one that gets installed, so D5 reads that one too. `bun.lockb` is binary: install
26
+ * uses it, D5 cannot read it.
27
+ */
28
+ const JS_LOCK_ORDER = ['pnpm-lock.yaml', 'yarn.lock', 'bun.lock', 'bun.lockb', 'npm-shrinkwrap.json', 'package-lock.json'];
29
+ /** The JS lockfile names among `names`, in preference order. */
30
+ export function jsLockfiles(names) {
31
+ const present = new Set(names);
32
+ return JS_LOCK_ORDER.filter((name) => present.has(name));
33
+ }
34
+ /** The one JS lockfile the install step and D5 use, among the file names of one directory. */
35
+ export function chooseJsLockfile(names) {
36
+ return jsLockfiles(names)[0];
37
+ }
38
+ const MISSING = { present: false, versions: [], specifierOk: true };
39
+ function record(value) {
40
+ return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
41
+ }
42
+ function str(value) {
43
+ return typeof value === 'string' ? value : undefined;
44
+ }
45
+ function found(version, specifierOk = true) {
46
+ return { present: true, versions: version === undefined ? [] : [version], specifierOk };
47
+ }
48
+ // ---- npm --------------------------------------------------------------------------------
49
+ function npmReader(json) {
50
+ const packages = record(json.packages);
51
+ const tree = record(json.dependencies);
52
+ return {
53
+ lookup(dep, pkgDir) {
54
+ if (packages) {
55
+ let dir = pkgDir;
56
+ for (;;) {
57
+ const entry = record(packages[dir === '' ? `node_modules/${dep.name}` : `${dir}/node_modules/${dep.name}`]);
58
+ if (entry) {
59
+ const target = entry.link === true ? record(packages[str(entry.resolved) ?? '']) : undefined;
60
+ return found(str(target?.version) ?? str(entry.version));
61
+ }
62
+ if (dir === '')
63
+ break;
64
+ dir = dir.includes('/') ? dir.slice(0, dir.lastIndexOf('/')) : '';
65
+ }
66
+ return MISSING;
67
+ }
68
+ const entry = record(tree?.[dep.name]);
69
+ return entry ? found(str(entry.version)) : MISSING;
70
+ },
71
+ };
72
+ }
73
+ // ---- pnpm -------------------------------------------------------------------------------
74
+ function pnpmVersion(raw) {
75
+ if (raw.startsWith('link:') || raw.startsWith('file:'))
76
+ return undefined;
77
+ return raw.replace(/\(.*$/, '').replace(/_.*$/, '');
78
+ }
79
+ function pnpmReader(doc) {
80
+ const importers = record(doc.importers);
81
+ return {
82
+ lookup(dep, pkgDir) {
83
+ const section = importers ? record(importers[pkgDir === '' ? '.' : pkgDir]) : doc;
84
+ if (!section)
85
+ return MISSING;
86
+ const specifiers = record(section.specifiers);
87
+ for (const key of ['dependencies', 'devDependencies', 'optionalDependencies']) {
88
+ const entry = record(section[key])?.[dep.name];
89
+ if (entry === undefined)
90
+ continue;
91
+ if (typeof entry === 'string')
92
+ return found(pnpmVersion(entry), str(specifiers?.[dep.name]) === dep.spec);
93
+ const table = record(entry);
94
+ const version = str(table?.version);
95
+ return found(version === undefined ? undefined : pnpmVersion(version), str(table?.specifier) === dep.spec);
96
+ }
97
+ return MISSING;
98
+ },
99
+ };
100
+ }
101
+ // ---- yarn -------------------------------------------------------------------------------
102
+ function yarnReader(text) {
103
+ const entries = new Map();
104
+ const names = new Set();
105
+ let keys = [];
106
+ for (const line of text.split(/\r?\n/)) {
107
+ if (line === '' || line.startsWith('#'))
108
+ continue;
109
+ if (!/^\s/.test(line)) {
110
+ keys = [];
111
+ if (!line.endsWith(':') || line.startsWith('__metadata'))
112
+ continue;
113
+ for (const part of line.slice(0, -1).split(',')) {
114
+ const key = part.trim().replace(/^"|"$/g, '');
115
+ const at = key.indexOf('@', 1);
116
+ if (at === -1)
117
+ continue;
118
+ const name = key.slice(0, at);
119
+ const range = key.slice(at + 1).replace(/^npm:/, '');
120
+ names.add(name);
121
+ keys.push(`${name}@${range}`);
122
+ entries.set(`${name}@${range}`, undefined);
123
+ }
124
+ continue;
125
+ }
126
+ const m = /^\s+version:?\s+"?([^"\s]+)"?\s*$/.exec(line);
127
+ if (m?.[1])
128
+ for (const key of keys)
129
+ entries.set(key, m[1]);
130
+ }
131
+ return {
132
+ lookup(dep) {
133
+ if (!dep.registry)
134
+ return names.has(dep.name) ? found(undefined) : MISSING;
135
+ const key = `${dep.name}@${dep.spec}`;
136
+ return entries.has(key) ? found(entries.get(key)) : MISSING;
137
+ },
138
+ };
139
+ }
140
+ // ---- bun --------------------------------------------------------------------------------
141
+ function bunReader(doc) {
142
+ const workspaces = record(doc.workspaces);
143
+ const packages = record(doc.packages);
144
+ return {
145
+ lookup(dep, pkgDir) {
146
+ const workspace = record(workspaces?.[pkgDir]);
147
+ let specifier;
148
+ for (const key of ['dependencies', 'devDependencies', 'optionalDependencies']) {
149
+ specifier ??= str(record(workspace?.[key])?.[dep.name]);
150
+ }
151
+ const tuple = packages?.[dep.name];
152
+ if (specifier === undefined && tuple === undefined)
153
+ return MISSING;
154
+ const id = Array.isArray(tuple) ? str(tuple[0]) : undefined;
155
+ const version = id !== undefined && id.lastIndexOf('@') > 0 ? id.slice(id.lastIndexOf('@') + 1) : undefined;
156
+ return found(version, specifier === undefined || specifier === dep.spec);
157
+ },
158
+ };
159
+ }
160
+ // ---- Python -----------------------------------------------------------------------------
161
+ function tomlReader(absPath) {
162
+ const doc = readToml(absPath);
163
+ if (!doc || !Array.isArray(doc.package))
164
+ return undefined;
165
+ const byName = new Map();
166
+ for (const entry of doc.package) {
167
+ const table = record(entry);
168
+ const name = str(table?.name);
169
+ if (name === undefined)
170
+ continue;
171
+ const version = str(table?.version);
172
+ const list = byName.get(normalizeName(name)) ?? [];
173
+ if (version !== undefined)
174
+ list.push(version);
175
+ byName.set(normalizeName(name), list);
176
+ }
177
+ return {
178
+ lookup(dep) {
179
+ const versions = byName.get(dep.name);
180
+ return versions === undefined ? MISSING : { present: true, versions, specifierOk: true };
181
+ },
182
+ };
183
+ }
184
+ function pipfileLockReader(json) {
185
+ return {
186
+ lookup(dep) {
187
+ for (const section of ['default', 'develop']) {
188
+ for (const [rawName, entry] of Object.entries(record(json[section]) ?? {})) {
189
+ if (normalizeName(rawName) !== dep.name)
190
+ continue;
191
+ const version = str(record(entry)?.version)?.replace(/^==/, '');
192
+ return found(version);
193
+ }
194
+ }
195
+ return MISSING;
196
+ },
197
+ };
198
+ }
199
+ /** `name==version` for every requirement of a hashed, fully pinned requirements file, else undefined. */
200
+ function pinnedRequirements(text) {
201
+ const lines = requirementLines(text);
202
+ if (lines.length === 0)
203
+ return undefined;
204
+ const pinned = new Map();
205
+ for (const { line, hashed } of lines) {
206
+ const m = /^([A-Za-z0-9][A-Za-z0-9._-]*)\s*(?:\[[^\]]*\])?\s*==\s*([^\s;=!<>~,]+)\s*(?:;.*)?$/.exec(line);
207
+ if (!hashed || !m?.[1] || !m[2] || m[2].endsWith('.*'))
208
+ return undefined;
209
+ pinned.set(normalizeName(m[1]), m[2]);
210
+ }
211
+ return pinned;
212
+ }
213
+ function requirementsReader(pinned) {
214
+ return {
215
+ lookup(dep) {
216
+ const version = pinned.get(dep.name);
217
+ return version === undefined ? MISSING : found(version);
218
+ },
219
+ };
220
+ }
221
+ // ---- search and parse -------------------------------------------------------------------
222
+ /**
223
+ * Lockfiles of the unit's group in the nearest directory (the package directory, then each
224
+ * ancestor) that holds any. Every kind found in that directory is a candidate.
225
+ */
226
+ export function findLockfiles(repoPath, pkg, group) {
227
+ let dir = pkg === '.' ? '' : pkg;
228
+ for (;;) {
229
+ const candidates = [];
230
+ for (const file of trackedIn(repoPath, dir)) {
231
+ const base = path.posix.basename(file);
232
+ const kind = NAMED[base];
233
+ if (kind !== undefined && kindGroup(kind) === group)
234
+ candidates.push({ path: file, dir, kind });
235
+ else if (group === 'python' && /^requirements.*\.txt$/.test(base)) {
236
+ const text = readTrackedText(repoPath, file);
237
+ if (text !== null && pinnedRequirements(text) !== undefined)
238
+ candidates.push({ path: file, dir, kind: 'requirements' });
239
+ }
240
+ }
241
+ if (candidates.length > 0)
242
+ return candidates;
243
+ if (dir === '')
244
+ return [];
245
+ dir = dir.includes('/') ? dir.slice(0, dir.lastIndexOf('/')) : '';
246
+ }
247
+ }
248
+ function parseJson(text) {
249
+ try {
250
+ return record(JSON.parse(text));
251
+ }
252
+ catch {
253
+ return undefined;
254
+ }
255
+ }
256
+ /** Parse a lockfile. Undefined when it cannot be read or parsed (it then counts as out of sync). */
257
+ export function readLockfile(repoPath, ref) {
258
+ const text = readTrackedText(repoPath, ref.path);
259
+ if (text === null)
260
+ return undefined;
261
+ const abs = path.join(path.resolve(repoPath), ref.path);
262
+ switch (ref.kind) {
263
+ case 'package-lock':
264
+ case 'npm-shrinkwrap': {
265
+ const json = parseJson(text);
266
+ return json ? npmReader(json) : undefined;
267
+ }
268
+ case 'pnpm': {
269
+ try {
270
+ const doc = record(parseYaml(text));
271
+ return doc ? pnpmReader(doc) : undefined;
272
+ }
273
+ catch {
274
+ return undefined;
275
+ }
276
+ }
277
+ case 'yarn':
278
+ return yarnReader(text);
279
+ case 'bun': {
280
+ const parsed = ts.parseConfigFileTextToJson(ref.path, text);
281
+ const doc = record(parsed.config);
282
+ return doc ? bunReader(doc) : undefined;
283
+ }
284
+ case 'uv':
285
+ case 'poetry':
286
+ case 'pdm':
287
+ return tomlReader(abs);
288
+ case 'pipfile': {
289
+ const json = parseJson(text);
290
+ return json ? pipfileLockReader(json) : undefined;
291
+ }
292
+ case 'requirements': {
293
+ const pinned = pinnedRequirements(text);
294
+ return pinned ? requirementsReader(pinned) : undefined;
295
+ }
296
+ }
297
+ }
298
+ export function entrySatisfies(dep, entry, group) {
299
+ if (!entry.present)
300
+ return false;
301
+ if (!dep.registry)
302
+ return true;
303
+ if (!entry.specifierOk)
304
+ return false;
305
+ if (group === 'python')
306
+ return entry.versions.some((v) => pySatisfies(v, dep.spec));
307
+ return entry.versions.some((v) => semver.satisfies(v, dep.spec, { includePrerelease: true }));
308
+ }
@@ -0,0 +1,122 @@
1
+ import { band, cvss3BaseScore } from './cvss.js';
2
+ const POINTS = { critical: 10, high: 5, moderate: 2, low: 0.5, none: 0 };
3
+ const LABELS = {
4
+ CRITICAL: 'critical',
5
+ HIGH: 'high',
6
+ MODERATE: 'moderate',
7
+ MEDIUM: 'moderate',
8
+ LOW: 'low',
9
+ };
10
+ export function endOfDay(asOf) {
11
+ return Date.parse(`${asOf}T23:59:59.999Z`);
12
+ }
13
+ /** Published at or before the end of the as-of day (no `published` is kept) and not withdrawn. */
14
+ function counts(record, asOf) {
15
+ if (record.withdrawn !== undefined && record.withdrawn !== '')
16
+ return false;
17
+ if (record.published === undefined)
18
+ return true;
19
+ const published = Date.parse(record.published);
20
+ return Number.isNaN(published) || published <= endOfDay(asOf);
21
+ }
22
+ const ORDER = ['none', 'low', 'moderate', 'high', 'critical'];
23
+ function highest(values) {
24
+ return values.reduce((top, v) => (top === undefined || ORDER.indexOf(v) > ORDER.indexOf(top) ? v : top), undefined);
25
+ }
26
+ /** Methodology 5.1 order: GHSA label, then CVSS v3.x base score band, then moderate. */
27
+ function groupSeverity(records) {
28
+ const labelled = records
29
+ .map((r) => LABELS[(r.database_specific?.severity ?? '').toUpperCase()])
30
+ .filter((s) => s !== undefined);
31
+ const label = highest(labelled);
32
+ if (label !== undefined)
33
+ return label;
34
+ const scored = [];
35
+ for (const record of records) {
36
+ for (const entry of record.severity ?? []) {
37
+ if (entry.type !== 'CVSS_V3' || !entry.score?.startsWith('CVSS:3.'))
38
+ continue;
39
+ const score = cvss3BaseScore(entry.score);
40
+ if (score !== undefined)
41
+ scored.push(band(score));
42
+ }
43
+ }
44
+ return highest(scored) ?? 'moderate';
45
+ }
46
+ /** Group the records of one package into advisories by alias closure. */
47
+ function advisories(records) {
48
+ const parent = new Map();
49
+ const find = (x) => {
50
+ let root = parent.get(x) ?? x;
51
+ while ((parent.get(root) ?? root) !== root)
52
+ root = parent.get(root) ?? root;
53
+ parent.set(x, root);
54
+ return root;
55
+ };
56
+ const union = (a, b) => {
57
+ const ra = find(a);
58
+ const rb = find(b);
59
+ if (ra !== rb)
60
+ parent.set(ra, rb);
61
+ };
62
+ for (const r of records) {
63
+ find(r.id);
64
+ for (const alias of r.aliases ?? [])
65
+ union(r.id, alias);
66
+ }
67
+ const groups = new Map();
68
+ for (const r of records) {
69
+ const root = find(r.id);
70
+ groups.set(root, [...(groups.get(root) ?? []), r]);
71
+ }
72
+ return [...groups.values()];
73
+ }
74
+ /** Severity points of the advisories that count at the as-of date. Each advisory counts once per package. */
75
+ export function severityPoints(packages, asOf) {
76
+ const found = [];
77
+ for (const pkg of packages) {
78
+ const records = pkg.vulnerabilities.filter((r) => counts(r, asOf));
79
+ for (const group of advisories(records)) {
80
+ const severity = groupSeverity(group);
81
+ const first = group[0];
82
+ if (!first)
83
+ continue;
84
+ found.push({ name: pkg.name, version: pkg.version, dev: pkg.dev, id: first.id, severity, points: POINTS[severity] });
85
+ }
86
+ }
87
+ return { points: found.reduce((sum, a) => sum + a.points, 0), advisories: found };
88
+ }
89
+ /** Packages of an osv-scanner JSON report. Undefined when the shape is not recognised. */
90
+ export function parseOsvReport(text) {
91
+ let json;
92
+ try {
93
+ json = JSON.parse(text);
94
+ }
95
+ catch {
96
+ return undefined;
97
+ }
98
+ const results = json?.results;
99
+ if (results === null || results === undefined)
100
+ return [];
101
+ if (!Array.isArray(results))
102
+ return undefined;
103
+ const found = [];
104
+ for (const result of results) {
105
+ if (!Array.isArray(result?.packages))
106
+ continue;
107
+ for (const entry of result.packages) {
108
+ const { name, version, ecosystem } = entry.package ?? {};
109
+ if (typeof name !== 'string' || typeof version !== 'string')
110
+ return undefined;
111
+ const groups = Array.isArray(entry.dependency_groups) ? entry.dependency_groups : [];
112
+ found.push({
113
+ name,
114
+ version,
115
+ ecosystem: typeof ecosystem === 'string' ? ecosystem : '',
116
+ dev: groups.includes('dev'),
117
+ vulnerabilities: Array.isArray(entry.vulnerabilities) ? entry.vulnerabilities : [],
118
+ });
119
+ }
120
+ }
121
+ return found;
122
+ }
@@ -0,0 +1,142 @@
1
+ import semver from 'semver';
2
+ import { compareVersions, isStable, normalizeName, parseVersion } from '../../util/pep440.js';
3
+ import { endOfDay } from './osv.js';
4
+ /** Anything but a 404: the registry could not be read. */
5
+ export class RegistryError extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = 'RegistryError';
9
+ }
10
+ }
11
+ const TIMEOUT_MS = 20_000;
12
+ export const CONCURRENCY = 8;
13
+ async function getJson(ctx, url) {
14
+ let lastError = 'unknown error';
15
+ for (let attempt = 0; attempt < 2; attempt += 1) {
16
+ try {
17
+ const response = await ctx.tools.fetch(url, { signal: AbortSignal.timeout(TIMEOUT_MS) });
18
+ if (response.status === 404)
19
+ return 'not_found';
20
+ if (!response.ok) {
21
+ lastError = `HTTP ${response.status}`;
22
+ continue;
23
+ }
24
+ return (await response.json());
25
+ }
26
+ catch (err) {
27
+ lastError = err instanceof Error ? err.message : String(err);
28
+ }
29
+ }
30
+ throw new RegistryError(`registry request ${url} failed: ${lastError}`);
31
+ }
32
+ function record(value) {
33
+ return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
34
+ }
35
+ function npmVersions(doc) {
36
+ const time = record(doc.time) ?? {};
37
+ const found = [];
38
+ for (const version of Object.keys(record(doc.versions) ?? {})) {
39
+ const published = typeof time[version] === 'string' ? Date.parse(time[version]) : Number.NaN;
40
+ if (!Number.isNaN(published))
41
+ found.push({ version, time: published, yanked: false });
42
+ }
43
+ return found;
44
+ }
45
+ function pypiVersions(doc) {
46
+ const found = [];
47
+ for (const [version, files] of Object.entries(record(doc.releases) ?? {})) {
48
+ if (!Array.isArray(files) || files.length === 0)
49
+ continue;
50
+ const entries = files.map((f) => record(f)).filter((f) => f !== undefined);
51
+ const times = entries
52
+ .map((f) => (typeof f.upload_time_iso_8601 === 'string' ? Date.parse(f.upload_time_iso_8601) : Number.NaN))
53
+ .filter((t) => !Number.isNaN(t));
54
+ if (times.length === 0)
55
+ continue;
56
+ found.push({ version, time: Math.min(...times), yanked: entries.every((f) => f.yanked === true) });
57
+ }
58
+ return found;
59
+ }
60
+ /** Registry data for one package, memoized per scan. `null` when the registry has no such package (404). */
61
+ export function fetchRegistry(ctx, group, name) {
62
+ const key = group === 'python' ? `pypi:${normalizeName(name)}` : `npm:${name}`;
63
+ return ctx.tools.memo(key, async () => {
64
+ const url = group === 'python'
65
+ ? `https://pypi.org/pypi/${encodeURIComponent(normalizeName(name))}/json`
66
+ : `https://registry.npmjs.org/${name.startsWith('@') ? name.replace('/', '%2F') : encodeURIComponent(name)}`;
67
+ const doc = await getJson(ctx, url);
68
+ if (doc === 'not_found')
69
+ return null;
70
+ if (!doc)
71
+ throw new RegistryError(`registry request ${url} returned no document`);
72
+ return { versions: group === 'python' ? pypiVersions(doc) : npmVersions(doc) };
73
+ });
74
+ }
75
+ /** Highest stable, non-yanked version published at or before the end of the as-of day. */
76
+ export function latestStable(data, group, asOf) {
77
+ const limit = endOfDay(asOf);
78
+ const candidates = data.versions.filter((v) => !v.yanked && v.time <= limit);
79
+ if (group === 'python') {
80
+ let top;
81
+ for (const { version } of candidates) {
82
+ const parsed = parseVersion(version);
83
+ if (!parsed || !isStable(parsed))
84
+ continue;
85
+ if (!top || compareVersions(parsed, top.parsed) > 0)
86
+ top = { text: version, parsed };
87
+ }
88
+ return top?.text;
89
+ }
90
+ return candidates
91
+ .map((v) => v.version)
92
+ .filter((v) => semver.valid(v) !== null && semver.prerelease(v) === null)
93
+ .sort(semver.compare)
94
+ .pop();
95
+ }
96
+ function parts(version, group) {
97
+ if (group === 'python') {
98
+ const parsed = parseVersion(version);
99
+ if (!parsed)
100
+ return undefined;
101
+ return {
102
+ release: parsed.release,
103
+ cmp: (other) => {
104
+ const o = parseVersion(other);
105
+ return o ? compareVersions(parsed, o) : 0;
106
+ },
107
+ };
108
+ }
109
+ const parsed = semver.parse(version);
110
+ if (!parsed)
111
+ return undefined;
112
+ return { release: [parsed.major, parsed.minor, parsed.patch], cmp: (other) => semver.compare(version, other) };
113
+ }
114
+ /** At least one major behind; for 0.x a minor bump counts as a major. A resolved version newer than latest is not behind. */
115
+ export function isMajorBehind(resolved, latest, group) {
116
+ const r = parts(resolved, group);
117
+ const l = parts(latest, group);
118
+ if (!r || !l)
119
+ return false;
120
+ if (r.cmp(latest) >= 0)
121
+ return false;
122
+ const [rMajor = 0, rMinor = 0] = r.release;
123
+ const [lMajor = 0, lMinor = 0] = l.release;
124
+ if (lMajor > rMajor)
125
+ return true;
126
+ return rMajor === 0 && lMajor === 0 && lMinor > rMinor;
127
+ }
128
+ /** Population and behind count of `direct` against one registry snapshot. Keys of `registryData`: `<group>:<name>`. */
129
+ export function majorsBehind(direct, registryData, asOf) {
130
+ let numerator = 0;
131
+ let denominator = 0;
132
+ for (const dep of direct) {
133
+ const data = registryData.get(`${dep.group}:${dep.group === 'python' ? normalizeName(dep.name) : dep.name}`);
134
+ if (!data)
135
+ continue;
136
+ denominator += 1;
137
+ const latest = latestStable(data, dep.group, asOf);
138
+ if (latest !== undefined && isMajorBehind(dep.version, latest, dep.group))
139
+ numerator += 1;
140
+ }
141
+ return { numerator, denominator };
142
+ }
@@ -0,0 +1,78 @@
1
+ import path from 'node:path';
2
+ import semver from 'semver';
3
+ import { compareVersions, parseVersion, satisfies as pySatisfies } from '../../util/pep440.js';
4
+ import { readManifest } from './declared.js';
5
+ import { trackedIn } from './files.js';
6
+ import { chooseJsLockfile, entrySatisfies, findLockfiles, jsLockfiles, readLockfile } from './lockfiles.js';
7
+ /** Manifest and lockfiles of one unit, read once per scan. */
8
+ export function unitDeps(ctx, pkg, group) {
9
+ return ctx.tools.memo(`deps:${pkg}\0${group}`, async () => {
10
+ const manifest = readManifest(ctx.repoPath, pkg, group);
11
+ const found = manifest ? findLockfiles(ctx.repoPath, pkg, group) : [];
12
+ const { refs, warnings } = group === 'typescript_javascript' ? installedLockfile(ctx.repoPath, found) : { refs: found, warnings: [] };
13
+ const lockfiles = refs.map((ref) => ({ ref, reader: readLockfile(ctx.repoPath, ref) }));
14
+ return { package: pkg, group, manifest, lockfiles, warnings };
15
+ });
16
+ }
17
+ /**
18
+ * With several JS lockfiles in the lockfile directory, keep only the one the install step uses
19
+ * (`chooseJsLockfile`) and warn. A binary `bun.lockb` cannot be read, so then every readable candidate stays.
20
+ */
21
+ function installedLockfile(repoPath, refs) {
22
+ const dir = refs[0]?.dir;
23
+ if (dir === undefined)
24
+ return { refs, warnings: [] };
25
+ const names = jsLockfiles(trackedIn(repoPath, dir).map((file) => path.posix.basename(file)));
26
+ if (names.length < 2)
27
+ return { refs, warnings: [] };
28
+ const chosen = chooseJsLockfile(names);
29
+ const where = dir === '' ? 'the repository root' : dir;
30
+ const picked = refs.filter((ref) => path.posix.basename(ref.path) === chosen);
31
+ if (picked.length === 0) {
32
+ return { refs, warnings: [`5.3: ${where} has several JS lockfiles (${names.join(', ')}); install uses ${chosen}, which is binary, so 5.3 checks ${refs.map((r) => path.posix.basename(r.path)).join(', ')}`] };
33
+ }
34
+ return { refs: picked, warnings: [`5.3: ${where} has several JS lockfiles (${names.join(', ')}); install and 5.3 use ${chosen}`] };
35
+ }
36
+ /** The unit's package directory relative to a lockfile's directory ('' when they are the same). */
37
+ function relToLock(pkg, lockDir) {
38
+ const dir = pkg === '.' ? '' : pkg;
39
+ if (lockDir === '')
40
+ return dir;
41
+ return dir === lockDir ? '' : path.posix.relative(lockDir, dir);
42
+ }
43
+ export function lockLevel(unit) {
44
+ if (!unit.manifest || unit.lockfiles.length === 0)
45
+ return 'none';
46
+ const inSync = unit.lockfiles.some(({ ref, reader }) => {
47
+ if (!reader)
48
+ return false;
49
+ const rel = relToLock(unit.package, ref.dir);
50
+ return (unit.manifest?.deps ?? []).every((dep) => entrySatisfies(dep, reader.lookup(dep, rel), unit.group));
51
+ });
52
+ return inSync ? 'in_sync' : 'out_of_sync';
53
+ }
54
+ function best(versions, dep, group) {
55
+ const fits = versions.filter((v) => group === 'python' ? pySatisfies(v, dep.spec) : semver.satisfies(v, dep.spec, { includePrerelease: true }));
56
+ const pool = fits.length > 0 ? fits : versions;
57
+ if (group === 'python') {
58
+ return [...pool].sort((a, b) => {
59
+ const pa = parseVersion(a);
60
+ const pb = parseVersion(b);
61
+ return pa && pb ? compareVersions(pb, pa) : 0;
62
+ })[0];
63
+ }
64
+ return pool[0];
65
+ }
66
+ /** Declared direct dependencies with the version the first lockfile holding them resolves. */
67
+ export function resolveDirect(unit) {
68
+ return (unit.manifest?.deps ?? []).map((dep) => {
69
+ for (const { ref, reader } of unit.lockfiles) {
70
+ if (!reader)
71
+ continue;
72
+ const entry = reader.lookup(dep, relToLock(unit.package, ref.dir));
73
+ if (entry.present)
74
+ return { name: dep.name, version: best(entry.versions, dep, unit.group), dev: dep.dev, registry: dep.registry };
75
+ }
76
+ return { name: dep.name, version: undefined, dev: dep.dev, registry: dep.registry };
77
+ });
78
+ }