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,195 @@
1
+ import path from 'node:path';
2
+ import semver from 'semver';
3
+ import { isValidSpecifier, normalizeName, poetryToPep440 } from '../../util/pep440.js';
4
+ import { readToml } from '../../util/toml.js';
5
+ import { joinRel, readTrackedText, trackedIn, trackedSet } from './files.js';
6
+ function record(value) {
7
+ return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
8
+ }
9
+ // ---- npm --------------------------------------------------------------------------------
10
+ function npmManifest(repoPath, pkg) {
11
+ const file = joinRel(pkg, 'package.json');
12
+ const text = readTrackedText(repoPath, file);
13
+ if (text === null)
14
+ return undefined;
15
+ const warnings = [];
16
+ let json;
17
+ try {
18
+ json = record(JSON.parse(text));
19
+ }
20
+ catch {
21
+ warnings.push(`${file}: invalid JSON, ignored`);
22
+ }
23
+ const deps = [];
24
+ const sections = [
25
+ ['dependencies', false],
26
+ ['devDependencies', true],
27
+ ['optionalDependencies', false],
28
+ ];
29
+ for (const [section, dev] of sections) {
30
+ const optional = section === 'optionalDependencies';
31
+ for (const [name, spec] of Object.entries(record(json?.[section]) ?? {})) {
32
+ if (typeof spec !== 'string')
33
+ continue;
34
+ deps.push({ name, spec, dev, registry: semver.validRange(spec) !== null, ...(optional ? { optional } : {}) });
35
+ }
36
+ }
37
+ return { file, kind: 'package.json', deps, warnings };
38
+ }
39
+ // ---- Python -----------------------------------------------------------------------------
40
+ /** One PEP 508 requirement string, or undefined when it is not a requirement. */
41
+ function parseRequirement(line, dev) {
42
+ const withoutMarker = (line.split(';')[0] ?? '').trim();
43
+ const m = /^([A-Za-z0-9][A-Za-z0-9._-]*)\s*(?:\[[^\]]*\])?\s*(.*)$/.exec(withoutMarker);
44
+ if (!m?.[1])
45
+ return undefined;
46
+ let rest = (m[2] ?? '').trim();
47
+ if (rest.startsWith('@'))
48
+ return { name: normalizeName(m[1]), spec: '', dev, registry: false };
49
+ if (rest.startsWith('(') && rest.endsWith(')'))
50
+ rest = rest.slice(1, -1).trim();
51
+ rest = rest.replace(/\s+/g, '');
52
+ if (!isValidSpecifier(rest))
53
+ return undefined;
54
+ return { name: normalizeName(m[1]), spec: rest, dev, registry: true };
55
+ }
56
+ function requirementList(value, dev) {
57
+ if (!Array.isArray(value))
58
+ return [];
59
+ const found = [];
60
+ for (const entry of value) {
61
+ if (typeof entry !== 'string')
62
+ continue; // include-group tables are skipped
63
+ const dep = parseRequirement(entry, dev);
64
+ if (dep)
65
+ found.push(dep);
66
+ }
67
+ return found;
68
+ }
69
+ function groupedLists(value, dev) {
70
+ return Object.values(record(value) ?? {}).flatMap((list) => requirementList(list, dev));
71
+ }
72
+ function poetryTable(value, dev) {
73
+ const found = [];
74
+ for (const [rawName, entry] of Object.entries(record(value) ?? {})) {
75
+ if (rawName.toLowerCase() === 'python')
76
+ continue;
77
+ const name = normalizeName(rawName);
78
+ const first = Array.isArray(entry) ? entry[0] : entry;
79
+ if (typeof first === 'string') {
80
+ found.push({ name, spec: poetryToPep440(first), dev, registry: true });
81
+ continue;
82
+ }
83
+ const table = record(first);
84
+ const version = table?.version;
85
+ const nonRegistry = table === undefined || table.git !== undefined || table.path !== undefined || table.url !== undefined;
86
+ if (nonRegistry || typeof version !== 'string')
87
+ found.push({ name, spec: '', dev, registry: false });
88
+ else
89
+ found.push({ name, spec: poetryToPep440(version), dev, registry: true });
90
+ }
91
+ return found;
92
+ }
93
+ function pyprojectDeps(doc) {
94
+ const project = record(doc.project);
95
+ const tool = record(doc.tool);
96
+ const poetry = record(tool?.poetry);
97
+ const uv = record(tool?.uv);
98
+ const pdm = record(tool?.pdm);
99
+ const sections = [];
100
+ let present = false;
101
+ const take = (value, make) => {
102
+ if (value !== undefined) {
103
+ present = true;
104
+ sections.push(make());
105
+ }
106
+ };
107
+ take(project?.dependencies, () => requirementList(project?.dependencies, false));
108
+ take(project?.['optional-dependencies'], () => groupedLists(project?.['optional-dependencies'], true));
109
+ take(doc['dependency-groups'], () => groupedLists(doc['dependency-groups'], true));
110
+ take(uv?.['dev-dependencies'], () => requirementList(uv?.['dev-dependencies'], true));
111
+ take(poetry?.dependencies, () => poetryTable(poetry?.dependencies, false));
112
+ take(poetry?.['dev-dependencies'], () => poetryTable(poetry?.['dev-dependencies'], true));
113
+ for (const group of Object.values(record(poetry?.group) ?? {})) {
114
+ take(record(group)?.dependencies, () => poetryTable(record(group)?.dependencies, true));
115
+ }
116
+ take(pdm?.['dev-dependencies'], () => groupedLists(pdm?.['dev-dependencies'], true));
117
+ return present ? sections.flat() : undefined;
118
+ }
119
+ function pipfileDeps(doc) {
120
+ const found = [];
121
+ for (const [section, dev] of [['packages', false], ['dev-packages', true]]) {
122
+ for (const [rawName, entry] of Object.entries(record(doc[section]) ?? {})) {
123
+ const name = normalizeName(rawName);
124
+ if (typeof entry === 'string') {
125
+ found.push({ name, spec: entry === '*' ? '' : entry.replace(/\s+/g, ''), dev, registry: isValidSpecifier(entry === '*' ? '' : entry.replace(/\s+/g, '')) });
126
+ continue;
127
+ }
128
+ const version = record(entry)?.version;
129
+ if (typeof version === 'string') {
130
+ const spec = version === '*' ? '' : version.replace(/\s+/g, '');
131
+ found.push({ name, spec, dev, registry: isValidSpecifier(spec) });
132
+ }
133
+ else
134
+ found.push({ name, spec: '', dev, registry: false });
135
+ }
136
+ }
137
+ return found;
138
+ }
139
+ /** Requirement lines of a requirements file: comments, options and hash continuations removed. */
140
+ export function requirementLines(text) {
141
+ const joined = text.replace(/\\\r?\n/g, ' ');
142
+ const lines = [];
143
+ for (const raw of joined.split(/\r?\n/)) {
144
+ const line = raw.replace(/(^|\s)#.*$/, '').trim();
145
+ if (line === '' || line.startsWith('-'))
146
+ continue;
147
+ const hashed = /--hash[=\s]/.test(line);
148
+ const clean = line.replace(/\s--\S+(?:[= ]\S+)?/g, '').trim();
149
+ lines.push({ line: clean, hashed });
150
+ }
151
+ return lines;
152
+ }
153
+ export function requirementFiles(repoPath, pkg) {
154
+ return trackedIn(repoPath, pkg === '.' ? '' : pkg).filter((f) => /^requirements.*\.txt$/.test(path.posix.basename(f)));
155
+ }
156
+ function pythonManifest(repoPath, pkg) {
157
+ const warnings = [];
158
+ const dir = pkg === '.' ? '' : pkg;
159
+ const pyprojectFile = joinRel(dir, 'pyproject.toml');
160
+ const requirements = () => {
161
+ const found = new Map();
162
+ for (const file of requirementFiles(repoPath, pkg)) {
163
+ const text = readTrackedText(repoPath, file) ?? '';
164
+ for (const { line } of requirementLines(text)) {
165
+ const dep = parseRequirement(line, false);
166
+ if (dep && !found.has(dep.name))
167
+ found.set(dep.name, dep);
168
+ }
169
+ }
170
+ return [...found.values()];
171
+ };
172
+ if (trackedSet(repoPath).has(pyprojectFile)) {
173
+ const doc = readToml(path.join(path.resolve(repoPath), pyprojectFile));
174
+ if (doc === undefined)
175
+ warnings.push(`${pyprojectFile}: invalid TOML, ignored`);
176
+ const deps = doc ? pyprojectDeps(doc) : undefined;
177
+ return { file: pyprojectFile, kind: 'pyproject.toml', deps: deps ?? requirements(), warnings };
178
+ }
179
+ const pipfile = joinRel(dir, 'Pipfile');
180
+ if (trackedSet(repoPath).has(pipfile)) {
181
+ const doc = readToml(path.join(path.resolve(repoPath), pipfile));
182
+ if (doc === undefined)
183
+ warnings.push(`${pipfile}: invalid TOML, ignored`);
184
+ return { file: pipfile, kind: 'Pipfile', deps: pipfileDeps(doc ?? {}), warnings };
185
+ }
186
+ const files = requirementFiles(repoPath, pkg);
187
+ const first = files[0];
188
+ if (first === undefined)
189
+ return undefined;
190
+ return { file: first, kind: 'requirements', deps: requirements(), warnings };
191
+ }
192
+ /** The unit's declared direct dependencies, or undefined when its package directory has no manifest. */
193
+ export function readManifest(repoPath, pkg, group) {
194
+ return group === 'python' ? pythonManifest(repoPath, pkg) : npmManifest(repoPath, pkg);
195
+ }
@@ -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
+ }