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,73 @@
1
+ import path from 'node:path';
2
+ import { readPackageJson } from '../../manifest/package-json.js';
3
+ import { readTracked } from '../../scope/tracked.js';
4
+ import { parseTomlText } from '../../util/toml.js';
5
+ import { dirsUpward } from './entry-points.js';
6
+ const isObj = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
7
+ const nonEmpty = (value) => (typeof value === 'string' && value.trim() !== '') || (Array.isArray(value) && value.some(nonEmpty));
8
+ const fileHasContent = (name, group) => ({
9
+ group,
10
+ check: (repoPath, dir) => (readTracked(repoPath, path.posix.join(dir, name))?.trim() ?? '') !== '',
11
+ });
12
+ function toml(repoPath, file, warnings) {
13
+ const text = readTracked(repoPath, file);
14
+ if (text === null)
15
+ return undefined;
16
+ const doc = parseTomlText(text);
17
+ if (!doc)
18
+ warnings.push(`6.5: ${file} is not valid TOML, so it does not pin a runtime`);
19
+ return doc;
20
+ }
21
+ const toolVersions = (tools, group) => ({
22
+ group,
23
+ check: (repoPath, dir) => (readTracked(repoPath, path.posix.join(dir, '.tool-versions')) ?? '')
24
+ .split('\n')
25
+ .some((line) => {
26
+ const [tool, version] = line.trim().split(/\s+/);
27
+ return tool !== undefined && tools.includes(tool) && version !== undefined && !version.startsWith('#');
28
+ }),
29
+ });
30
+ const mise = (tools, group) => ({
31
+ group,
32
+ check(repoPath, dir, warnings) {
33
+ const doc = toml(repoPath, path.posix.join(dir, 'mise.toml'), warnings);
34
+ const table = doc && isObj(doc.tools) ? doc.tools : {};
35
+ return tools.some((tool) => nonEmpty(table[tool]));
36
+ },
37
+ });
38
+ /** The pin files of the 6.5 `runtime_pinned` measure, by the words the YAML uses; a word the YAML adds that is not here pins nothing. */
39
+ const PROBES = {
40
+ '.nvmrc': [fileHasContent('.nvmrc', 'typescript_javascript')],
41
+ '.node-version': [fileHasContent('.node-version', 'typescript_javascript')],
42
+ '.tool-versions': [toolVersions(['node', 'nodejs'], 'typescript_javascript'), toolVersions(['python'], 'python')],
43
+ 'mise.toml': [mise(['node', 'nodejs'], 'typescript_javascript'), mise(['python'], 'python')],
44
+ 'engines.node in package.json': [
45
+ { group: 'typescript_javascript', check: (repoPath, dir) => nonEmpty(readPackageJson(repoPath, dir)?.engines.node) },
46
+ ],
47
+ 'volta in package.json': [
48
+ { group: 'typescript_javascript', check: (repoPath, dir) => nonEmpty(readPackageJson(repoPath, dir)?.volta?.node) },
49
+ ],
50
+ '.python-version': [fileHasContent('.python-version', 'python')],
51
+ 'requires-python in pyproject.toml': [
52
+ {
53
+ group: 'python',
54
+ check(repoPath, dir, warnings) {
55
+ const doc = toml(repoPath, path.posix.join(dir, 'pyproject.toml'), warnings);
56
+ return doc !== undefined && isObj(doc.project) && nonEmpty(doc.project['requires-python']);
57
+ },
58
+ },
59
+ ],
60
+ 'runtime.txt': [fileHasContent('runtime.txt', 'python')],
61
+ };
62
+ /** Looked up at the unit's package directory, then each ancestor up to the repository root. Poetry's `python = "^3.12"` dependency does not count. */
63
+ export function runtimePinned(repoPath, dir, group, pinFiles, warnings) {
64
+ for (const d of dirsUpward(dir)) {
65
+ for (const word of pinFiles) {
66
+ for (const probe of PROBES[word] ?? []) {
67
+ if (probe.group === group && probe.check(repoPath, d, warnings))
68
+ return true;
69
+ }
70
+ }
71
+ }
72
+ return false;
73
+ }
@@ -0,0 +1,8 @@
1
+ import { measurePassRate } from '../exec/tests.js';
2
+ export const p11 = {
3
+ id: '1.1',
4
+ layer: 'execution',
5
+ async measure(ctx) {
6
+ return measurePassRate(ctx);
7
+ },
8
+ };
@@ -0,0 +1,8 @@
1
+ import { measureCoverage } from '../exec/tests.js';
2
+ export const p12 = {
3
+ id: '1.2',
4
+ layer: 'execution',
5
+ async measure(ctx) {
6
+ return measureCoverage(ctx, 'lines');
7
+ },
8
+ };
@@ -0,0 +1,8 @@
1
+ import { measureCoverage } from '../exec/tests.js';
2
+ export const p13 = {
3
+ id: '1.3',
4
+ layer: 'execution',
5
+ async measure(ctx) {
6
+ return measureCoverage(ctx, 'branches');
7
+ },
8
+ };
@@ -0,0 +1,41 @@
1
+ import { PyImportGraph } from './safety/py-tests.js';
2
+ import { TsCredits } from './safety/ts-credits.js';
3
+ import { createTsResolver } from './safety/ts-resolve.js';
4
+ import { logicUnits } from './units.js';
5
+ export const p14 = {
6
+ id: '1.4',
7
+ layer: 'static',
8
+ async measure({ scope, repoPath }) {
9
+ const units = logicUnits(scope);
10
+ const warnings = [];
11
+ const credited = new Set();
12
+ const tsTests = scope.files.filter((f) => f.role === 'test' && f.group === 'typescript_javascript');
13
+ if (tsTests.length > 0 && units.some((u) => u.group === 'typescript_javascript')) {
14
+ const credits = new TsCredits(repoPath, scope, createTsResolver(repoPath));
15
+ for (const test of tsTests)
16
+ for (const file of credits.credits(test.path))
17
+ credited.add(file);
18
+ }
19
+ const pyFiles = scope.files.filter((f) => f.group === 'python');
20
+ const pyTests = pyFiles.filter((f) => f.role === 'test');
21
+ if (pyTests.length > 0 && units.some((u) => u.group === 'python')) {
22
+ const graph = new PyImportGraph(repoPath, pyFiles);
23
+ for (const test of pyTests) {
24
+ if (graph.factsOf(test.path)?.ok === false)
25
+ warnings.push(`1.4: ${test.path}: syntax error, its imports are not counted`);
26
+ for (const file of graph.credits(test.path))
27
+ credited.add(file);
28
+ }
29
+ }
30
+ return {
31
+ status: 'measured',
32
+ units: units.map((unit) => ({
33
+ package: unit.package,
34
+ group: unit.group,
35
+ weight: unit.weight,
36
+ raw: { kind: 'ratio', numerator: unit.files.filter((f) => credited.has(f.path)).length, denominator: unit.files.length, scale: 100 },
37
+ })),
38
+ ...(warnings.length === 0 ? {} : { warnings }),
39
+ };
40
+ },
41
+ };
@@ -0,0 +1,59 @@
1
+ import { pythonTestFacts } from '../python/tests.js';
2
+ import { testCases } from './safety/ts-cases.js';
3
+ import { TsCredits } from './safety/ts-credits.js';
4
+ import { createTsResolver } from './safety/ts-resolve.js';
5
+ import { logicUnits, unitId } from './units.js';
6
+ export const p15 = {
7
+ id: '1.5',
8
+ layer: 'static',
9
+ async measure({ scope, repoPath }) {
10
+ const units = logicUnits(scope);
11
+ const tallies = new Map(units.map((u) => [unitId(u), { numerator: 0, denominator: 0 }]));
12
+ const warnings = [];
13
+ const tsTests = scope.files.filter((f) => f.role === 'test' && f.group === 'typescript_javascript' && tallies.has(unitId(f)));
14
+ if (tsTests.length > 0) {
15
+ const credits = new TsCredits(repoPath, scope, createTsResolver(repoPath));
16
+ for (const test of tsTests) {
17
+ const sf = credits.source(test.path);
18
+ const tally = tallies.get(unitId(test));
19
+ if (!sf || !tally)
20
+ continue;
21
+ for (const found of testCases(sf)) {
22
+ tally.denominator += 1;
23
+ if (found.asserts)
24
+ tally.numerator += 1;
25
+ }
26
+ }
27
+ }
28
+ const pyFiles = scope.files.filter((f) => f.group === 'python');
29
+ const pyTests = pyFiles.filter((f) => f.role === 'test' && tallies.has(unitId(f)));
30
+ if (pyTests.length > 0) {
31
+ const facts = pythonTestFacts(repoPath);
32
+ for (const test of pyTests) {
33
+ const info = facts[test.path];
34
+ const tally = tallies.get(unitId(test));
35
+ if (!tally)
36
+ continue;
37
+ if (!info?.ok) {
38
+ warnings.push(`1.5: ${test.path}: syntax error, its test cases are not counted`);
39
+ continue;
40
+ }
41
+ for (const found of info.cases) {
42
+ tally.denominator += 1;
43
+ if (found.asserts)
44
+ tally.numerator += 1;
45
+ }
46
+ }
47
+ }
48
+ return {
49
+ status: 'measured',
50
+ units: units.map((unit) => ({
51
+ package: unit.package,
52
+ group: unit.group,
53
+ weight: unit.weight,
54
+ raw: { kind: 'ratio', ...tallies.get(unitId(unit)), scale: 100 },
55
+ })),
56
+ ...(warnings.length === 0 ? {} : { warnings }),
57
+ };
58
+ },
59
+ };
@@ -0,0 +1,8 @@
1
+ import { measureFindings } from '../exec/findings.js';
2
+ export const p21 = {
3
+ id: '2.1',
4
+ layer: 'execution',
5
+ async measure(ctx) {
6
+ return measureFindings(ctx, (exec) => exec.typecheck);
7
+ },
8
+ };
@@ -0,0 +1,44 @@
1
+ import path from 'node:path';
2
+ import { pythonLevel } from './py/typecheck-config.js';
3
+ import { levelOf, lowestLevel, unitConfigs } from './ts/tsconfig.js';
4
+ import { emptyTallies, logicFiles } from './units.js';
5
+ const TS_SOURCE = /\.(?:ts|tsx|mts|cts)$/i;
6
+ export const p22 = {
7
+ id: '2.2',
8
+ layer: 'static',
9
+ async measure({ scope, repoPath }) {
10
+ const warnings = [];
11
+ const cache = new Map();
12
+ const warned = new Set();
13
+ const units = [];
14
+ const tsFiles = logicFiles(scope, 'typescript_javascript');
15
+ for (const tally of emptyTallies(tsFiles).values()) {
16
+ const unitFiles = tsFiles.filter((f) => f.package === tally.package).map((f) => f.path);
17
+ const { configs, read } = unitConfigs(repoPath, tally.package, unitFiles, cache);
18
+ for (const config of read) {
19
+ for (const missing of config.missingExtends) {
20
+ const message = `2.2: ${path.posix.relative(repoPath.split(path.sep).join('/'), config.file)} extends ${missing}, which could not be resolved; level computed without it`;
21
+ if (!warned.has(message)) {
22
+ warned.add(message);
23
+ warnings.push(message);
24
+ }
25
+ }
26
+ }
27
+ const hasTypeScript = unitFiles.some((f) => TS_SOURCE.test(f));
28
+ const levels = configs.map((c) => (!hasTypeScript && c.options.checkJs !== true ? 'none' : levelOf(c.options)));
29
+ const level = levels.length === 0 ? 'none' : lowestLevel(levels);
30
+ units.push({ package: tally.package, group: tally.group, weight: tally.weight, raw: { kind: 'level', level } });
31
+ }
32
+ const pyFiles = logicFiles(scope, 'python');
33
+ for (const tally of emptyTallies(pyFiles).values()) {
34
+ units.push({
35
+ package: tally.package,
36
+ group: tally.group,
37
+ weight: tally.weight,
38
+ raw: { kind: 'level', level: pythonLevel(repoPath, tally.package, warnings) },
39
+ });
40
+ }
41
+ units.sort((a, b) => a.package.localeCompare(b.package) || a.group.localeCompare(b.group));
42
+ return { status: 'measured', units, warnings: [...new Set(warnings)] };
43
+ },
44
+ };
@@ -0,0 +1,81 @@
1
+ import ts from 'typescript';
2
+ import { analyzePythonTyping } from '../python/typing.js';
3
+ import { countSuppressionComments, hasLeadingDirective } from './ts/directives.js';
4
+ import { emptyTallies, logicFiles, unitId } from './units.js';
5
+ const FILE_LEVEL_OPT_OUT = 10;
6
+ function isConstType(type) {
7
+ return ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName) && type.typeName.text === 'const';
8
+ }
9
+ /** Escape-hatch points of one TypeScript or JavaScript file (methodology 2.3). */
10
+ function countTsEscapeHatches(sourceFile) {
11
+ let points = 0;
12
+ const visit = (node) => {
13
+ switch (node.kind) {
14
+ case ts.SyntaxKind.AnyKeyword:
15
+ case ts.SyntaxKind.NonNullExpression:
16
+ points += 1;
17
+ break;
18
+ case ts.SyntaxKind.AsExpression:
19
+ case ts.SyntaxKind.TypeAssertionExpression:
20
+ if (!isConstType(node.type))
21
+ points += 1;
22
+ break;
23
+ case ts.SyntaxKind.PropertyDeclaration:
24
+ case ts.SyntaxKind.VariableDeclaration:
25
+ if (node.exclamationToken)
26
+ points += 1;
27
+ break;
28
+ }
29
+ ts.forEachChild(node, visit);
30
+ };
31
+ ts.forEachChild(sourceFile, visit);
32
+ points += countSuppressionComments(sourceFile);
33
+ if (hasLeadingDirective(sourceFile, 'ts-nocheck'))
34
+ points += FILE_LEVEL_OPT_OUT;
35
+ return points;
36
+ }
37
+ export const p23 = {
38
+ id: '2.3',
39
+ layer: 'static',
40
+ async measure({ scope, repoPath }) {
41
+ const tsFiles = logicFiles(scope, 'typescript_javascript');
42
+ const pyFiles = logicFiles(scope, 'python');
43
+ const tallies = emptyTallies([...tsFiles, ...pyFiles]);
44
+ for (const file of tsFiles) {
45
+ const tally = tallies.get(unitId(file));
46
+ if (!tally)
47
+ continue;
48
+ tally.numerator += countTsEscapeHatches(scope.getTsSourceFile(file.path));
49
+ tally.denominator += file.logicLines;
50
+ }
51
+ const warnings = [];
52
+ const info = analyzePythonTyping(repoPath, pyFiles.map((f) => f.path));
53
+ let unparsed = 0;
54
+ for (const file of pyFiles) {
55
+ const tally = tallies.get(unitId(file));
56
+ const facts = info[file.path];
57
+ if (!tally)
58
+ continue;
59
+ if (!facts?.ok) {
60
+ unparsed += 1;
61
+ continue;
62
+ }
63
+ const p = facts.points;
64
+ tally.numerator +=
65
+ p.anyCount + p.castCount + p.typeIgnoreLine + p.pyrightIgnore +
66
+ FILE_LEVEL_OPT_OUT * (p.typeIgnoreHeader + (p.mypyIgnoreErrors ? 1 : 0));
67
+ tally.denominator += file.logicLines;
68
+ }
69
+ if (unparsed > 0)
70
+ warnings.push(`2.3/2.4: ${unparsed} Python files could not be parsed`);
71
+ const units = [...tallies.values()]
72
+ .sort((a, b) => a.package.localeCompare(b.package) || a.group.localeCompare(b.group))
73
+ .map((t) => ({
74
+ package: t.package,
75
+ group: t.group,
76
+ weight: t.weight,
77
+ raw: { kind: 'ratio', numerator: t.numerator, denominator: t.denominator, scale: 1000 },
78
+ }));
79
+ return { status: 'measured', units, warnings };
80
+ },
81
+ };
@@ -0,0 +1,288 @@
1
+ import path from 'node:path';
2
+ import ts from 'typescript';
3
+ import { analyzePythonTyping } from '../python/typing.js';
4
+ import { hasLeadingDirective } from './ts/directives.js';
5
+ import { unitConfigs } from './ts/tsconfig.js';
6
+ import { emptyTallies, logicFiles, unitId } from './units.js';
7
+ function hasModifier(node, kind) {
8
+ return ts.canHaveModifiers(node) && (ts.getModifiers(node)?.some((m) => m.kind === kind) ?? false);
9
+ }
10
+ /** A function or arrow function behind parentheses, `satisfies T` and `as T`. */
11
+ function functionOperand(expression) {
12
+ let contextual = false;
13
+ let current = expression;
14
+ for (;;) {
15
+ if (ts.isParenthesizedExpression(current))
16
+ current = current.expression;
17
+ else if (ts.isSatisfiesExpression(current)) {
18
+ contextual = true;
19
+ current = current.expression;
20
+ }
21
+ else if (ts.isAsExpression(current)) {
22
+ if (!(ts.isTypeReferenceNode(current.type) && current.type.getText() === 'const'))
23
+ contextual = true;
24
+ current = current.expression;
25
+ }
26
+ else
27
+ break;
28
+ }
29
+ return ts.isArrowFunction(current) || ts.isFunctionExpression(current) ? { node: current, contextual } : undefined;
30
+ }
31
+ /** Overload signatures with an implementation count as the implementation; without one, every signature counts. */
32
+ function groupOverloads(nodes, keyOf) {
33
+ const byKey = new Map();
34
+ for (const node of nodes) {
35
+ const key = keyOf(node);
36
+ byKey.set(key, [...(byKey.get(key) ?? []), node]);
37
+ }
38
+ return [...byKey.values()].flatMap((group) => {
39
+ const impls = group.filter((n) => n.body !== undefined);
40
+ return impls.length > 0 ? impls : group;
41
+ });
42
+ }
43
+ function classSurface(node) {
44
+ const candidates = [];
45
+ const setters = [];
46
+ for (const member of node.members) {
47
+ if (ts.isMethodDeclaration(member) || ts.isConstructorDeclaration(member))
48
+ candidates.push(member);
49
+ else if (ts.isSetAccessorDeclaration(member))
50
+ setters.push(member);
51
+ else
52
+ continue;
53
+ }
54
+ const isPublic = (member) => !hasModifier(member, ts.SyntaxKind.PrivateKeyword) &&
55
+ !hasModifier(member, ts.SyntaxKind.ProtectedKeyword) &&
56
+ !(member.name !== undefined && ts.isPrivateIdentifier(member.name));
57
+ const methods = groupOverloads(candidates.filter(isPublic), (m) => `${hasModifier(m, ts.SyntaxKind.StaticKeyword) ? 'static ' : ''}${ts.isConstructorDeclaration(m) ? 'constructor' : m.name.getText()}`);
58
+ return [...methods, ...setters.filter(isPublic)].map((fn) => ({ node: fn, contextual: false }));
59
+ }
60
+ function isLocalFunctionValue(expression) {
61
+ return expression ? functionOperand(expression) : undefined;
62
+ }
63
+ /** Functions and function-valued variables declared at the top level of a file, by name. */
64
+ function localFunctions(sourceFile) {
65
+ const byName = new Map();
66
+ const declarations = [];
67
+ for (const statement of sourceFile.statements) {
68
+ if (ts.isFunctionDeclaration(statement) && statement.name)
69
+ declarations.push(statement);
70
+ else if (ts.isVariableStatement(statement)) {
71
+ for (const declaration of statement.declarationList.declarations) {
72
+ if (!ts.isIdentifier(declaration.name))
73
+ continue;
74
+ const surface = isLocalFunctionValue(declaration.initializer);
75
+ if (surface)
76
+ byName.set(declaration.name.text, [{ ...surface, contextual: surface.contextual || declaration.type !== undefined }]);
77
+ }
78
+ }
79
+ }
80
+ for (const fn of groupOverloads(declarations, (d) => d.name?.text ?? '')) {
81
+ const name = fn.name?.text ?? '';
82
+ byName.set(name, [...(byName.get(name) ?? []), { node: fn, contextual: false }]);
83
+ }
84
+ return byName;
85
+ }
86
+ function publicSurface(sourceFile) {
87
+ const surface = new Map();
88
+ const add = (items) => {
89
+ for (const item of items)
90
+ surface.set(item.node, item);
91
+ };
92
+ const locals = localFunctions(sourceFile);
93
+ const exportedDeclarations = [];
94
+ for (const statement of sourceFile.statements) {
95
+ const exported = hasModifier(statement, ts.SyntaxKind.ExportKeyword);
96
+ if (ts.isFunctionDeclaration(statement) && exported)
97
+ exportedDeclarations.push(statement);
98
+ else if (ts.isClassDeclaration(statement) && exported)
99
+ add(classSurface(statement));
100
+ else if (ts.isVariableStatement(statement) && exported) {
101
+ for (const declaration of statement.declarationList.declarations) {
102
+ const fn = isLocalFunctionValue(declaration.initializer);
103
+ if (fn)
104
+ add([{ ...fn, contextual: fn.contextual || declaration.type !== undefined }]);
105
+ }
106
+ }
107
+ else if (ts.isExportAssignment(statement) && !statement.isExportEquals) {
108
+ const fn = functionOperand(statement.expression);
109
+ if (fn)
110
+ add([fn]);
111
+ else if (ts.isIdentifier(statement.expression))
112
+ add(locals.get(statement.expression.text) ?? []);
113
+ }
114
+ else if (ts.isExportDeclaration(statement) && !statement.moduleSpecifier && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
115
+ for (const specifier of statement.exportClause.elements)
116
+ add(locals.get((specifier.propertyName ?? specifier.name).text) ?? []);
117
+ }
118
+ else if (ts.isExpressionStatement(statement)) {
119
+ add(commonJsSurface(statement.expression, locals));
120
+ }
121
+ }
122
+ add(groupOverloads(exportedDeclarations, (d) => d.name?.text ?? 'default').map((node) => ({ node, contextual: false })));
123
+ return [...surface.values()];
124
+ }
125
+ function commonJsSurface(expression, locals) {
126
+ if (!ts.isBinaryExpression(expression) || expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
127
+ return [];
128
+ const left = expression.left;
129
+ const right = expression.right;
130
+ const isModuleExports = (e) => ts.isPropertyAccessExpression(e) && ts.isIdentifier(e.expression) && e.expression.text === 'module' && e.name.text === 'exports';
131
+ if (isModuleExports(left)) {
132
+ const fn = functionOperand(right);
133
+ if (fn)
134
+ return [fn];
135
+ if (ts.isClassExpression(right))
136
+ return classSurface(right);
137
+ if (ts.isObjectLiteralExpression(right)) {
138
+ const out = [];
139
+ for (const property of right.properties) {
140
+ if (ts.isMethodDeclaration(property))
141
+ out.push({ node: property, contextual: false });
142
+ else if (ts.isPropertyAssignment(property)) {
143
+ const value = functionOperand(property.initializer);
144
+ if (value)
145
+ out.push(value);
146
+ }
147
+ else if (ts.isShorthandPropertyAssignment(property))
148
+ out.push(...(locals.get(property.name.text) ?? []));
149
+ }
150
+ return out;
151
+ }
152
+ return [];
153
+ }
154
+ if (ts.isPropertyAccessExpression(left) &&
155
+ ((ts.isIdentifier(left.expression) && left.expression.text === 'exports') || isModuleExports(left.expression))) {
156
+ const fn = functionOperand(right);
157
+ return fn ? [fn] : [];
158
+ }
159
+ return [];
160
+ }
161
+ function countTsSlots(sourceFile, checkedAsJs) {
162
+ const slots = { typed: 0, total: 0 };
163
+ for (const { node, contextual } of publicSurface(sourceFile)) {
164
+ for (const parameter of node.parameters) {
165
+ if (ts.isIdentifier(parameter.name) && parameter.name.text === 'this')
166
+ continue;
167
+ slots.total += 1;
168
+ const jsdoc = checkedAsJs && ts.getJSDocParameterTags(parameter).some((tag) => tag.typeExpression !== undefined);
169
+ if (parameter.type !== undefined || parameter.initializer !== undefined || contextual || jsdoc)
170
+ slots.typed += 1;
171
+ }
172
+ }
173
+ return slots;
174
+ }
175
+ // ---- Python -------------------------------------------------------------------------------
176
+ /** Dotted import path parts of a module, with a leading `src` directory ignored. */
177
+ function moduleParts(file) {
178
+ const relative = file.package === '.' ? file.path : file.path.slice(file.package.length + 1);
179
+ const parts = relative.split('/');
180
+ if (parts[0] === 'src' && parts.length > 1)
181
+ parts.shift();
182
+ const base = path.posix.basename(relative);
183
+ const stem = base.replace(/\.[^.]+$/, '');
184
+ parts.pop();
185
+ if (stem !== '__init__')
186
+ parts.push(stem);
187
+ return parts;
188
+ }
189
+ const isPublicName = (name) => !name.startsWith('_');
190
+ const isPublicMethod = (name) => !name.startsWith('_') || (name.startsWith('__') && name.endsWith('__') && name.length > 4);
191
+ const isOverload = (fn) => fn.decorators.some((d) => d === 'overload' || d.endsWith('.overload'));
192
+ /** Overload stubs are skipped when a non-overload definition of the same name exists in the same scope. */
193
+ function withoutOverloads(functions) {
194
+ const implemented = new Set(functions.filter((f) => !isOverload(f)).map((f) => f.name));
195
+ return functions.filter((f) => !(isOverload(f) && implemented.has(f.name)));
196
+ }
197
+ function countFunctionSlots(fn, isMethod, slots) {
198
+ let params = fn.params;
199
+ const isStatic = fn.decorators.includes('staticmethod');
200
+ const first = params[0];
201
+ if (isMethod && (!isStatic || fn.name === '__new__') && first?.kind === 'positional')
202
+ params = params.slice(1);
203
+ for (const param of params) {
204
+ slots.total += 1;
205
+ if (param.typed)
206
+ slots.typed += 1;
207
+ }
208
+ if (fn.name !== '__init__') {
209
+ slots.total += 1;
210
+ if (fn.returns)
211
+ slots.typed += 1;
212
+ }
213
+ }
214
+ function countPythonSlots(defs, allNames) {
215
+ const slots = { typed: 0, total: 0 };
216
+ const listed = allNames === null ? null : new Set(allNames);
217
+ const isPublicModuleName = (name) => (listed === null ? isPublicName(name) : listed.has(name));
218
+ const functions = withoutOverloads(defs.filter((d) => d.kind === 'function').map((d) => d.info));
219
+ for (const fn of functions)
220
+ if (isPublicModuleName(fn.name))
221
+ countFunctionSlots(fn, false, slots);
222
+ for (const def of defs) {
223
+ if (def.kind !== 'class' || !isPublicModuleName(def.name))
224
+ continue;
225
+ for (const method of withoutOverloads(def.methods))
226
+ if (isPublicMethod(method.name))
227
+ countFunctionSlots(method, true, slots);
228
+ }
229
+ return slots;
230
+ }
231
+ // ---- Measurer -------------------------------------------------------------------------------
232
+ export const p24 = {
233
+ id: '2.4',
234
+ layer: 'static',
235
+ async measure({ scope, repoPath }) {
236
+ const tsFiles = logicFiles(scope, 'typescript_javascript');
237
+ const pyFiles = logicFiles(scope, 'python');
238
+ const tallies = emptyTallies([...tsFiles, ...pyFiles]);
239
+ const cache = new Map();
240
+ const unitConfigsByPackage = new Map();
241
+ const configsFor = (pkg) => {
242
+ const existing = unitConfigsByPackage.get(pkg);
243
+ if (existing)
244
+ return existing;
245
+ const made = unitConfigs(repoPath, pkg, tsFiles.filter((f) => f.package === pkg).map((f) => f.path), cache);
246
+ unitConfigsByPackage.set(pkg, made);
247
+ return made;
248
+ };
249
+ for (const file of tsFiles) {
250
+ const tally = tallies.get(unitId(file));
251
+ if (!tally)
252
+ continue;
253
+ const sourceFile = scope.getTsSourceFile(file.path);
254
+ let checkedAsJs = false;
255
+ if (file.language === 'javascript') {
256
+ const absolute = path.resolve(repoPath, file.path).split(path.sep).join('/');
257
+ const byConfig = configsFor(file.package).configs.some((c) => c.options.checkJs === true && c.fileNames.has(absolute));
258
+ const byComment = hasLeadingDirective(sourceFile, 'ts-check') && !hasLeadingDirective(sourceFile, 'ts-nocheck');
259
+ checkedAsJs = byConfig || byComment;
260
+ }
261
+ const slots = countTsSlots(sourceFile, checkedAsJs);
262
+ tally.numerator += slots.typed;
263
+ tally.denominator += slots.total;
264
+ }
265
+ // Files that cannot be parsed are reported once, by 2.3.
266
+ const info = analyzePythonTyping(repoPath, pyFiles.map((f) => f.path));
267
+ for (const file of pyFiles) {
268
+ const tally = tallies.get(unitId(file));
269
+ const facts = info[file.path];
270
+ if (!tally || !facts?.ok)
271
+ continue;
272
+ if (moduleParts(file).some((part) => part.startsWith('_')))
273
+ continue;
274
+ const slots = countPythonSlots(facts.defs, facts.allNames);
275
+ tally.numerator += slots.typed;
276
+ tally.denominator += slots.total;
277
+ }
278
+ const units = [...tallies.values()]
279
+ .sort((a, b) => a.package.localeCompare(b.package) || a.group.localeCompare(b.group))
280
+ .map((t) => ({
281
+ package: t.package,
282
+ group: t.group,
283
+ weight: t.weight,
284
+ raw: { kind: 'ratio', numerator: t.numerator, denominator: t.denominator, scale: 100 },
285
+ }));
286
+ return { status: 'measured', units };
287
+ },
288
+ };
@@ -0,0 +1,8 @@
1
+ import { measureFindings } from '../exec/findings.js';
2
+ export const p25 = {
3
+ id: '2.5',
4
+ layer: 'execution',
5
+ async measure(ctx) {
6
+ return measureFindings(ctx, (exec) => exec.lint);
7
+ },
8
+ };
@@ -0,0 +1,16 @@
1
+ import { d3Gap, getD3Rules } from './d3/index.js';
2
+ export const p31 = {
3
+ id: '3.1',
4
+ layer: 'static',
5
+ async measure(ctx) {
6
+ const gap = d3Gap(ctx);
7
+ if (gap !== undefined)
8
+ return { status: 'not_measured', reason: gap };
9
+ const { units, warnings } = await getD3Rules(ctx);
10
+ return {
11
+ status: 'measured',
12
+ units: units.map((unit) => ({ package: unit.package, group: unit.group, weight: unit.weight, raw: { kind: 'level', level: unit.level31 } })),
13
+ ...(warnings.length === 0 ? {} : { warnings: warnings.map((w) => `3.1: ${w}`) }),
14
+ };
15
+ },
16
+ };