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.
- package/LICENSE +95 -0
- package/README.md +64 -2
- package/assets/fonts/NotoEmoji-OFL.txt +93 -0
- package/assets/fonts/NotoEmoji-Poop.ttf +0 -0
- package/assets/fonts/SpaceGrotesk-Bold.ttf +0 -0
- package/assets/fonts/SpaceGrotesk-OFL.txt +93 -0
- package/dist/bin.js +8 -0
- package/dist/card/previous.js +38 -0
- package/dist/card/render.js +71 -0
- package/dist/cli.js +121 -0
- package/dist/commands/card.js +69 -0
- package/dist/commands/common.js +1 -0
- package/dist/commands/diff.js +54 -0
- package/dist/commands/footer.js +25 -0
- package/dist/commands/report.js +49 -0
- package/dist/commands/route.js +62 -0
- package/dist/commands/submit.js +125 -0
- package/dist/commands/summary.js +77 -0
- package/dist/diff/compare.js +73 -0
- package/dist/diff/conditions.js +87 -0
- package/dist/diff/format.js +23 -0
- package/dist/exec/docker.js +158 -0
- package/dist/exec/findings.js +73 -0
- package/dist/exec/plan.js +50 -0
- package/dist/exec/reports.js +90 -0
- package/dist/exec/roots.js +49 -0
- package/dist/exec/run.js +182 -0
- package/dist/exec/steps/build.js +45 -0
- package/dist/exec/steps/coverage-install.js +12 -0
- package/dist/exec/steps/install.js +41 -0
- package/dist/exec/steps/knip.js +28 -0
- package/dist/exec/steps/lint.js +25 -0
- package/dist/exec/steps/lockfiles.js +25 -0
- package/dist/exec/steps/test.js +17 -0
- package/dist/exec/steps/typecheck.js +40 -0
- package/dist/exec/test-reports/coverage-py.js +23 -0
- package/dist/exec/test-reports/coverage-ranges.js +65 -0
- package/dist/exec/test-reports/coverage-summary.js +23 -0
- package/dist/exec/test-reports/jest-json.js +31 -0
- package/dist/exec/test-reports/junit.js +24 -0
- package/dist/exec/test-reports/mocha-json.js +18 -0
- package/dist/exec/test-reports/vitest-json.js +5 -0
- package/dist/exec/test-runners/detect.js +99 -0
- package/dist/exec/test-runners/index.js +6 -0
- package/dist/exec/test-runners/jest.js +27 -0
- package/dist/exec/test-runners/mocha.js +28 -0
- package/dist/exec/test-runners/node-test.js +24 -0
- package/dist/exec/test-runners/pytest.js +18 -0
- package/dist/exec/test-runners/script.js +48 -0
- package/dist/exec/test-runners/spec.js +1 -0
- package/dist/exec/test-runners/vitest.js +28 -0
- package/dist/exec/tests.js +120 -0
- package/dist/exec/types.js +1 -0
- package/dist/manifest/package-json.js +36 -0
- package/dist/manifest/python.js +106 -0
- package/dist/measure/d3/index.js +45 -0
- package/dist/measure/d3/presets.js +100 -0
- package/dist/measure/d3/py/adapter.js +67 -0
- package/dist/measure/d3/py/config.js +80 -0
- package/dist/measure/d3/py/contracts.js +57 -0
- package/dist/measure/d3/py/domain.js +57 -0
- package/dist/measure/d3/py/graph.js +214 -0
- package/dist/measure/d3/py/interpreter.js +31 -0
- package/dist/measure/d3/py/layers.js +120 -0
- package/dist/measure/d3/py/roots.js +58 -0
- package/dist/measure/d3/py/types.js +1 -0
- package/dist/measure/d3/scc.js +64 -0
- package/dist/measure/d3/ts/adapter.js +371 -0
- package/dist/measure/d3/ts/classify.js +107 -0
- package/dist/measure/d3/ts/config.js +318 -0
- package/dist/measure/d3/ts/domain.js +23 -0
- package/dist/measure/d3/ts/rules.js +65 -0
- package/dist/measure/d3/types.js +5 -0
- package/dist/measure/deps/cvss.js +66 -0
- package/dist/measure/deps/declared.js +195 -0
- package/dist/measure/deps/files.js +40 -0
- package/dist/measure/deps/lockfiles.js +308 -0
- package/dist/measure/deps/osv.js +122 -0
- package/dist/measure/deps/registry.js +142 -0
- package/dist/measure/deps/resolved.js +78 -0
- package/dist/measure/deps/units.js +5 -0
- package/dist/measure/eslint/health-plugin.js +19 -0
- package/dist/measure/ops/docs-commands.js +73 -0
- package/dist/measure/ops/docs-resolve.js +217 -0
- package/dist/measure/ops/entry-points.js +49 -0
- package/dist/measure/ops/env-example.js +20 -0
- package/dist/measure/ops/env-reads.js +46 -0
- package/dist/measure/ops/runtime.js +73 -0
- package/dist/measure/p1-1.js +8 -0
- package/dist/measure/p1-2.js +8 -0
- package/dist/measure/p1-3.js +8 -0
- package/dist/measure/p1-4.js +41 -0
- package/dist/measure/p1-5.js +59 -0
- package/dist/measure/p2-1.js +8 -0
- package/dist/measure/p2-2.js +44 -0
- package/dist/measure/p2-3.js +81 -0
- package/dist/measure/p2-4.js +288 -0
- package/dist/measure/p2-5.js +8 -0
- package/dist/measure/p3-1.js +16 -0
- package/dist/measure/p3-2.js +26 -0
- package/dist/measure/p3-3.js +37 -0
- package/dist/measure/p3-4.js +26 -0
- package/dist/measure/p3-5.js +26 -0
- package/dist/measure/p4-1.js +184 -0
- package/dist/measure/p4-2.js +27 -0
- package/dist/measure/p4-3.js +106 -0
- package/dist/measure/p4-4.js +309 -0
- package/dist/measure/p5-1.js +117 -0
- package/dist/measure/p5-2.js +80 -0
- package/dist/measure/p5-3.js +21 -0
- package/dist/measure/p5-4.js +161 -0
- package/dist/measure/p5-5.js +158 -0
- package/dist/measure/p6-1.js +20 -0
- package/dist/measure/p6-2.js +20 -0
- package/dist/measure/p6-3.js +40 -0
- package/dist/measure/p6-4.js +31 -0
- package/dist/measure/p6-5.js +60 -0
- package/dist/measure/p6-6.js +54 -0
- package/dist/measure/py/typecheck-config.js +95 -0
- package/dist/measure/registry.js +31 -0
- package/dist/measure/safety/py-tests.js +124 -0
- package/dist/measure/safety/ts-barrels.js +68 -0
- package/dist/measure/safety/ts-cases.js +111 -0
- package/dist/measure/safety/ts-credits.js +101 -0
- package/dist/measure/safety/ts-imports.js +63 -0
- package/dist/measure/safety/ts-resolve.js +43 -0
- package/dist/measure/ts/directives.js +37 -0
- package/dist/measure/ts/exports.js +60 -0
- package/dist/measure/ts/tsconfig.js +179 -0
- package/dist/measure/types.js +1 -0
- package/dist/measure/units.js +41 -0
- package/dist/methodology/load.js +159 -0
- package/dist/methodology/schema.js +105 -0
- package/dist/python/deadcode.js +27 -0
- package/dist/python/env.js +13 -0
- package/dist/python/helper.js +24 -0
- package/dist/python/imports.js +40 -0
- package/dist/python/nox.js +16 -0
- package/dist/python/scope.js +35 -0
- package/dist/python/tests.js +19 -0
- package/dist/python/typing.js +27 -0
- package/dist/report/escape.js +5 -0
- package/dist/report/format.js +75 -0
- package/dist/report/render.js +213 -0
- package/dist/report/roadmap.js +58 -0
- package/dist/report/scares.js +83 -0
- package/dist/report/style.js +27 -0
- package/dist/report-only/hotspots.js +48 -0
- package/dist/route/check.js +37 -0
- package/dist/route/files.js +74 -0
- package/dist/route/order.js +22 -0
- package/dist/route/select.js +114 -0
- package/dist/route/write.js +13 -0
- package/dist/run/read.js +89 -0
- package/dist/run/recompute.js +19 -0
- package/dist/run/scan.js +196 -0
- package/dist/run/schema.js +1 -0
- package/dist/run/score-run.js +42 -0
- package/dist/run/write.js +4 -0
- package/dist/scope/build.js +173 -0
- package/dist/scope/packages.js +87 -0
- package/dist/scope/rules.js +21 -0
- package/dist/scope/tracked.js +34 -0
- package/dist/scope/ts-lines.js +63 -0
- package/dist/scope/types.js +7 -0
- package/dist/scoring/aggregate.js +38 -0
- package/dist/scoring/pool.js +65 -0
- package/dist/scoring/score.js +81 -0
- package/dist/scoring/what-if.js +38 -0
- package/dist/scripts/checks.js +53 -0
- package/dist/scripts/ci.js +139 -0
- package/dist/scripts/entry-points/find.js +31 -0
- package/dist/scripts/entry-points/index.js +18 -0
- package/dist/scripts/entry-points/justfile.js +62 -0
- package/dist/scripts/entry-points/makefile.js +66 -0
- package/dist/scripts/entry-points/nox.js +29 -0
- package/dist/scripts/entry-points/package-json.js +21 -0
- package/dist/scripts/entry-points/pyproject.js +73 -0
- package/dist/scripts/entry-points/tox.js +130 -0
- package/dist/scripts/expand.js +300 -0
- package/dist/scripts/triggers.js +151 -0
- package/dist/scripts/types.js +1 -0
- package/dist/submit/constants.js +9 -0
- package/dist/submit/payload.js +42 -0
- package/dist/submit/repo-identity.js +46 -0
- package/dist/submit/send.js +29 -0
- package/dist/tools/catalog.js +46 -0
- package/dist/tools/image.js +180 -0
- package/dist/tools/kit.js +71 -0
- package/dist/tools/knip.js +169 -0
- package/dist/tools/outcome.js +21 -0
- package/dist/tools/resolve.js +66 -0
- package/dist/tools/workcopy.js +27 -0
- package/dist/util/ini.js +37 -0
- package/dist/util/pep440.js +163 -0
- package/dist/util/toml.js +27 -0
- package/docker/Dockerfile +142 -0
- package/docker/bin/health-capture +5 -0
- package/docker/bin/health-coverage-install +5 -0
- package/docker/bin/health-eslint +66 -0
- package/docker/bin/health-pytest +10 -0
- package/docker/bin/health-tsc +10 -0
- package/docker/bin/health-venv +11 -0
- package/docker/config/eslint.config.mjs +4 -0
- package/docker/config/eslint.rules.mjs +65 -0
- package/docker/config/eslint.untyped.config.mjs +4 -0
- package/docker/config/knip.json +1 -0
- package/docker/config/ruff.toml +7 -0
- package/methodology/LICENSE +396 -0
- package/methodology/v1.0/METHODOLOGY.md +1176 -0
- package/methodology/v1.0/dimensions/1-safety-net.yaml +240 -0
- package/methodology/v1.0/dimensions/2-type-static-rigor.yaml +289 -0
- package/methodology/v1.0/dimensions/3-architecture-boundaries.yaml +266 -0
- package/methodology/v1.0/dimensions/4-complexity-maintainability.yaml +192 -0
- package/methodology/v1.0/dimensions/5-dependency-security-hygiene.yaml +223 -0
- package/methodology/v1.0/dimensions/6-operability-feedback-loop.yaml +279 -0
- package/methodology/v1.0/methodology.yaml +242 -0
- package/methodology/v1.0/presets/django.yaml +43 -0
- package/methodology/v1.0/presets/express.yaml +47 -0
- package/methodology/v1.0/presets/fastapi.yaml +52 -0
- package/methodology/v1.0/presets/generic.yaml +41 -0
- package/methodology/v1.0/presets/nextjs.yaml +50 -0
- package/methodology/v1.0/scope.yaml +115 -0
- package/package.json +61 -4
- package/python/dead_code_helper.py +265 -0
- package/python/env_helper.py +71 -0
- package/python/graph_helper.py +277 -0
- package/python/imports_helper.py +132 -0
- package/python/nox_helper.py +79 -0
- package/python/scope_helper.py +96 -0
- package/python/tests_helper.py +171 -0
- package/python/typing_helper.py +235 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
preset:
|
|
2
|
+
key: generic
|
|
3
|
+
name: Generic
|
|
4
|
+
languages: [typescript, javascript, python]
|
|
5
|
+
detect: Used when no other preset matches.
|
|
6
|
+
|
|
7
|
+
conventions: >-
|
|
8
|
+
Globs are relative to the package root and match with or without a src/
|
|
9
|
+
prefix (TypeScript, JavaScript) or a top-level package directory (Python).
|
|
10
|
+
When a file matches several layer globs, the innermost matching directory
|
|
11
|
+
decides its layer.
|
|
12
|
+
|
|
13
|
+
layers:
|
|
14
|
+
shared: ["shared/**", "common/**", "lib/**", "utils/**", "helpers/**"]
|
|
15
|
+
features:
|
|
16
|
+
- "features/**"
|
|
17
|
+
- "modules/**"
|
|
18
|
+
- "routes/**"
|
|
19
|
+
- "pages/**"
|
|
20
|
+
- "app/**"
|
|
21
|
+
- "components/**"
|
|
22
|
+
- "views/**"
|
|
23
|
+
- "controllers/**"
|
|
24
|
+
- "api/**"
|
|
25
|
+
|
|
26
|
+
modules:
|
|
27
|
+
roots: ["features/*", "modules/*", "packages/*"]
|
|
28
|
+
public_entry:
|
|
29
|
+
typescript_javascript: "index.{ts,tsx,js,jsx,mts,mjs,cjs}"
|
|
30
|
+
python: __init__.py
|
|
31
|
+
|
|
32
|
+
rules:
|
|
33
|
+
- name: shared-does-not-import-features
|
|
34
|
+
description: Shared code does not depend on feature, route or UI code.
|
|
35
|
+
from: { in: [shared] }
|
|
36
|
+
to: { in: [features] }
|
|
37
|
+
severity: error
|
|
38
|
+
type_only_imports: included
|
|
39
|
+
|
|
40
|
+
sources:
|
|
41
|
+
- "Martin, R. C. Clean Architecture. Prentice Hall, 2017 (the Dependency Rule)."
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
preset:
|
|
2
|
+
key: nextjs
|
|
3
|
+
name: Next.js
|
|
4
|
+
languages: [typescript, javascript]
|
|
5
|
+
detect: The package declares next as a dependency.
|
|
6
|
+
|
|
7
|
+
conventions: >-
|
|
8
|
+
Globs are relative to the package root and match with or without a src/
|
|
9
|
+
prefix. When a file matches several layer globs, the innermost matching
|
|
10
|
+
directory decides its layer.
|
|
11
|
+
|
|
12
|
+
layers:
|
|
13
|
+
routes: ["app/**", "pages/**"]
|
|
14
|
+
components: ["components/**"]
|
|
15
|
+
lib: ["lib/**", "utils/**"]
|
|
16
|
+
server_only: >-
|
|
17
|
+
Files that import the server-only package, files whose name contains
|
|
18
|
+
.server., and files under server/**.
|
|
19
|
+
client: Files whose first statement is the "use client" directive.
|
|
20
|
+
|
|
21
|
+
modules:
|
|
22
|
+
roots: ["features/*", "modules/*", "packages/*"]
|
|
23
|
+
public_entry: "index.{ts,tsx,js,jsx,mts,mjs}"
|
|
24
|
+
|
|
25
|
+
rules:
|
|
26
|
+
- name: routes-are-leaves
|
|
27
|
+
description: Nothing outside the route directories imports from them.
|
|
28
|
+
from: { not_in: [routes] }
|
|
29
|
+
to: { in: [routes] }
|
|
30
|
+
severity: error
|
|
31
|
+
type_only_imports: included
|
|
32
|
+
- name: lib-does-not-import-ui
|
|
33
|
+
description: Shared code does not depend on components or routes.
|
|
34
|
+
from: { in: [lib] }
|
|
35
|
+
to: { in: [components, routes] }
|
|
36
|
+
severity: error
|
|
37
|
+
type_only_imports: included
|
|
38
|
+
- name: client-does-not-import-server
|
|
39
|
+
description: >-
|
|
40
|
+
A client component does not directly import a server-only file or a
|
|
41
|
+
database package.
|
|
42
|
+
from: { in: [client] }
|
|
43
|
+
to:
|
|
44
|
+
in: [server_only]
|
|
45
|
+
packages: ["@prisma/client", drizzle-orm, pg, mysql2, mongoose, redis, ioredis, server-only]
|
|
46
|
+
severity: error
|
|
47
|
+
type_only_imports: excluded
|
|
48
|
+
|
|
49
|
+
sources:
|
|
50
|
+
- "Next.js: Server and Client Components, keeping server-only code out of the client environment. https://nextjs.org/docs/app/getting-started/server-and-client-components"
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Which files count, and how lines are counted.
|
|
2
|
+
|
|
3
|
+
source_files:
|
|
4
|
+
start_from: git ls-files
|
|
5
|
+
extensions:
|
|
6
|
+
typescript: [.ts, .tsx, .mts, .cts]
|
|
7
|
+
javascript: [.js, .jsx, .mjs, .cjs]
|
|
8
|
+
python: [.py]
|
|
9
|
+
|
|
10
|
+
exclusions:
|
|
11
|
+
# Fixed by the methodology. Applied before anything else.
|
|
12
|
+
vendored:
|
|
13
|
+
- "**/node_modules/**"
|
|
14
|
+
- "**/.venv/**"
|
|
15
|
+
- "**/venv/**"
|
|
16
|
+
- "**/vendor/**"
|
|
17
|
+
- "**/third_party/**"
|
|
18
|
+
build_output:
|
|
19
|
+
- "**/dist/**"
|
|
20
|
+
- "**/build/**"
|
|
21
|
+
- "**/.next/**"
|
|
22
|
+
- "**/out/**"
|
|
23
|
+
- "**/__pycache__/**"
|
|
24
|
+
- "**/coverage/**"
|
|
25
|
+
generated:
|
|
26
|
+
- files whose first 5 lines contain "@generated" or "Code generated"
|
|
27
|
+
- "**/*_pb2.py"
|
|
28
|
+
- "**/*_pb2_grpc.py"
|
|
29
|
+
- "**/node_modules/.prisma/**"
|
|
30
|
+
- "**/prisma/generated/**"
|
|
31
|
+
- "**/generated/prisma/**"
|
|
32
|
+
minified:
|
|
33
|
+
- "**/*.min.js"
|
|
34
|
+
- files whose average line length is over 300 characters
|
|
35
|
+
migrations:
|
|
36
|
+
- "**/prisma/migrations/**"
|
|
37
|
+
- "**/alembic/versions/**"
|
|
38
|
+
- "**/migrations/*.py"
|
|
39
|
+
- "**/drizzle/**/*.sql"
|
|
40
|
+
declarations:
|
|
41
|
+
- "**/*.d.ts"
|
|
42
|
+
test_data:
|
|
43
|
+
- "**/__fixtures__/**"
|
|
44
|
+
- "**/__snapshots__/**"
|
|
45
|
+
- "**/testdata/**"
|
|
46
|
+
- "**/test-data/**"
|
|
47
|
+
- "**/test/**/fixtures/**"
|
|
48
|
+
- "**/tests/**/fixtures/**"
|
|
49
|
+
- "**/__tests__/**/fixtures/**"
|
|
50
|
+
- "**/e2e/**/fixtures/**"
|
|
51
|
+
|
|
52
|
+
file_roles:
|
|
53
|
+
test:
|
|
54
|
+
typescript_javascript:
|
|
55
|
+
- "**/*.test.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"
|
|
56
|
+
- "**/*.spec.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"
|
|
57
|
+
- "**/__tests__/**"
|
|
58
|
+
- "**/__mocks__/**"
|
|
59
|
+
- "**/test/**"
|
|
60
|
+
- "**/tests/**"
|
|
61
|
+
- "**/e2e/**"
|
|
62
|
+
python:
|
|
63
|
+
- "**/test_*.py"
|
|
64
|
+
- "**/*_test.py"
|
|
65
|
+
- "**/test/**"
|
|
66
|
+
- "**/tests/**"
|
|
67
|
+
- "**/conftest.py"
|
|
68
|
+
config:
|
|
69
|
+
- "**/*.config.{ts,mts,cts,js,mjs,cjs}"
|
|
70
|
+
- "**/*.config.*.{ts,mts,cts,js,mjs,cjs}"
|
|
71
|
+
- "**/.eslintrc.{js,cjs}"
|
|
72
|
+
- "**/setup.py"
|
|
73
|
+
- "**/noxfile.py"
|
|
74
|
+
logic: >-
|
|
75
|
+
An in-scope source file that is neither a test file nor a config file and
|
|
76
|
+
defines at least one function, method or class. Files that only re-export
|
|
77
|
+
(barrels), only declare types, or only declare constants are not logic
|
|
78
|
+
files.
|
|
79
|
+
|
|
80
|
+
how_tests_are_used:
|
|
81
|
+
dimension_1: Test files are the subject of dimension 1.
|
|
82
|
+
typecheck_and_lint: >-
|
|
83
|
+
Test files are included in 2.1 (typecheck errors) and 2.5 (baseline lint
|
|
84
|
+
findings), because broken tests are a real problem.
|
|
85
|
+
other_static_metrics: >-
|
|
86
|
+
Test files are excluded from every other static metric in dimensions 2
|
|
87
|
+
to 4: escape hatches, typed surface, boundaries, cycles, complexity, file
|
|
88
|
+
size, duplication and dead code.
|
|
89
|
+
|
|
90
|
+
line_counts:
|
|
91
|
+
logic_lines: >-
|
|
92
|
+
Non-blank lines that are not comment-only, in logic files. Python
|
|
93
|
+
docstrings count as comments. Used by 2.3, 4.2 and for unit_weighted
|
|
94
|
+
pooling.
|
|
95
|
+
source_lines: >-
|
|
96
|
+
Non-blank lines that are not comment-only, in logic files and test files.
|
|
97
|
+
Used by 2.1 and 2.5.
|
|
98
|
+
|
|
99
|
+
healthignore:
|
|
100
|
+
file: .healthignore
|
|
101
|
+
syntax: gitignore patterns, relative to the repository root
|
|
102
|
+
allowed: true
|
|
103
|
+
safeguards:
|
|
104
|
+
- The report shows the percentage of lines excluded by .healthignore.
|
|
105
|
+
- health diff refuses to compare runs whose .healthignore contents differ.
|
|
106
|
+
does_not_apply_to: >-
|
|
107
|
+
The secret scan (5.4). Secret scanning covers every tracked file and only
|
|
108
|
+
honours .gitleaksignore, which is part of the ignore set.
|
|
109
|
+
|
|
110
|
+
workspaces:
|
|
111
|
+
detection:
|
|
112
|
+
typescript_javascript: package.json "workspaces", pnpm-workspace.yaml
|
|
113
|
+
python: uv workspace members in pyproject.toml, or each directory with its own pyproject.toml
|
|
114
|
+
scoring: >-
|
|
115
|
+
One headline score for the repository. See pooling in methodology.yaml.
|
package/package.json
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibecodedshit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Codebase Health benchmark CLI",
|
|
5
|
+
"license": "Elastic-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"vibecodedshit": "dist/bin.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"methodology",
|
|
16
|
+
"python",
|
|
17
|
+
"assets",
|
|
18
|
+
"docker",
|
|
19
|
+
"LICENSE",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"lint": "eslint .",
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"coverage": "vitest run --coverage --coverage.provider=v8 --coverage.reporter=json-summary --coverage.reporter=text-summary --coverage.include=src/**",
|
|
30
|
+
"test:tools": "HEALTH_TOOLS=1 vitest run test/tools",
|
|
31
|
+
"test:docker": "HEALTH_DOCKER=1 vitest run test/docker",
|
|
32
|
+
"image:build": "docker build -t health:1.0 -f docker/Dockerfile docker",
|
|
33
|
+
"verify": "npm run typecheck && npm run lint && npm run test",
|
|
34
|
+
"build": "tsc -p tsconfig.build.json",
|
|
35
|
+
"prepublishOnly": "npm run build && npm run verify",
|
|
36
|
+
"health": "tsx src/bin.ts"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@resvg/resvg-wasm": "2.6.2",
|
|
40
|
+
"@typescript-eslint/parser": "8.71.0",
|
|
41
|
+
"dependency-cruiser": "18.5.0",
|
|
42
|
+
"eslint": "9.39.5",
|
|
43
|
+
"eslint-plugin-sonarjs": "4.2.2",
|
|
44
|
+
"ignore": "^7.0.12",
|
|
45
|
+
"jscpd": "5.4.0",
|
|
46
|
+
"knip": "6.39.0",
|
|
47
|
+
"picomatch": "^4.0.7",
|
|
48
|
+
"semver": "7.8.5",
|
|
49
|
+
"smol-toml": "1.9.0",
|
|
50
|
+
"typescript": "^5.9.3",
|
|
51
|
+
"typescript-eslint": "8.71.0",
|
|
52
|
+
"yaml": "^2.9.1",
|
|
53
|
+
"zod": "^4.6.7"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "^26.6.4",
|
|
57
|
+
"@types/picomatch": "^4.0.3",
|
|
58
|
+
"@types/semver": "^7.8.0",
|
|
59
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
60
|
+
"tsx": "^4.23.15",
|
|
61
|
+
"vitest": "^4.1.11"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""Symbols, __all__, imports and class bases of Python files for the 4.4 dead-code parameter. Standard library only.
|
|
2
|
+
|
|
3
|
+
Reads {"files": [relative paths]} as JSON on stdin and prints, per path,
|
|
4
|
+
{"ok": bool,
|
|
5
|
+
"symbols": [{"name", "qualname", "kind": "function"|"class"|"method", "line", "startLine", "decorated"}],
|
|
6
|
+
"allNames": [str] | null,
|
|
7
|
+
"imports": [{"module": str, "level": int, "names": [str]}],
|
|
8
|
+
"classes": {class name: {"bases": [{"name": str, "file": str | null, "class": str | null}]}}}
|
|
9
|
+
as JSON on stdout. "line" is the def/class line, "startLine" the first decorator line (or "line").
|
|
10
|
+
A base with "file" and "class" resolves to a class defined in one of the scanned files; a base with
|
|
11
|
+
null "file" is defined outside them (standard library, third party, or not resolvable). `object` and
|
|
12
|
+
`Generic[...]` are left out. A file that cannot be read or parsed gives {"ok": false}.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import ast
|
|
16
|
+
import json
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
FUNCTIONS = (ast.FunctionDef, ast.AsyncFunctionDef)
|
|
20
|
+
BLOCKS = (ast.If, ast.Try, ast.With, ast.AsyncWith)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def start_line(node):
|
|
24
|
+
lines = [node.lineno] + [d.lineno for d in node.decorator_list]
|
|
25
|
+
return min(lines)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def symbol(node, kind, qualname):
|
|
29
|
+
return {
|
|
30
|
+
"name": node.name,
|
|
31
|
+
"qualname": qualname,
|
|
32
|
+
"kind": kind,
|
|
33
|
+
"line": node.lineno,
|
|
34
|
+
"startLine": start_line(node),
|
|
35
|
+
"decorated": bool(node.decorator_list),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def child_bodies(node):
|
|
40
|
+
if isinstance(node, ast.If):
|
|
41
|
+
return [node.body, node.orelse]
|
|
42
|
+
if isinstance(node, ast.Try):
|
|
43
|
+
bodies = [node.body, node.orelse, node.finalbody]
|
|
44
|
+
bodies.extend(handler.body for handler in node.handlers)
|
|
45
|
+
return bodies
|
|
46
|
+
return [node.body]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def collect(statements, out):
|
|
50
|
+
"""Module-level functions and classes, also inside top-level if/try/with, plus methods of those classes."""
|
|
51
|
+
for node in statements:
|
|
52
|
+
if isinstance(node, FUNCTIONS):
|
|
53
|
+
out.append(symbol(node, "function", node.name))
|
|
54
|
+
elif isinstance(node, ast.ClassDef):
|
|
55
|
+
out.append(symbol(node, "class", node.name))
|
|
56
|
+
for member in node.body:
|
|
57
|
+
if isinstance(member, FUNCTIONS):
|
|
58
|
+
out.append(symbol(member, "method", node.name + "." + member.name))
|
|
59
|
+
elif isinstance(node, BLOCKS):
|
|
60
|
+
for body in child_bodies(node):
|
|
61
|
+
collect(body, out)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def string_items(value):
|
|
65
|
+
if isinstance(value, (ast.List, ast.Tuple)):
|
|
66
|
+
return [e.value for e in value.elts if isinstance(e, ast.Constant) and isinstance(e.value, str)]
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def all_names(tree):
|
|
71
|
+
names = None
|
|
72
|
+
for node in tree.body:
|
|
73
|
+
targets = []
|
|
74
|
+
value = None
|
|
75
|
+
if isinstance(node, ast.Assign):
|
|
76
|
+
targets, value = node.targets, node.value
|
|
77
|
+
elif isinstance(node, ast.AnnAssign) and node.value is not None:
|
|
78
|
+
targets, value = [node.target], node.value
|
|
79
|
+
elif isinstance(node, ast.AugAssign):
|
|
80
|
+
targets, value = [node.target], node.value
|
|
81
|
+
if not any(isinstance(t, ast.Name) and t.id == "__all__" for t in targets):
|
|
82
|
+
continue
|
|
83
|
+
items = string_items(value)
|
|
84
|
+
if items is not None:
|
|
85
|
+
names = (names or []) + items
|
|
86
|
+
return names
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def imports(tree):
|
|
90
|
+
found = []
|
|
91
|
+
for node in ast.walk(tree):
|
|
92
|
+
if isinstance(node, ast.Import):
|
|
93
|
+
for alias in node.names:
|
|
94
|
+
found.append({"module": alias.name, "level": 0, "names": []})
|
|
95
|
+
elif isinstance(node, ast.ImportFrom):
|
|
96
|
+
found.append(
|
|
97
|
+
{
|
|
98
|
+
"module": node.module or "",
|
|
99
|
+
"level": node.level,
|
|
100
|
+
"names": [alias.name for alias in node.names],
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
return found
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ---- class bases ------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
IGNORED_BASES = {"object", "Generic"}
|
|
109
|
+
MAX_HOPS = 5
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def class_nodes(statements, out):
|
|
113
|
+
"""Module-level classes, also inside top-level if/try/with (the same ones `collect` reports)."""
|
|
114
|
+
for node in statements:
|
|
115
|
+
if isinstance(node, ast.ClassDef):
|
|
116
|
+
out.append(node)
|
|
117
|
+
elif isinstance(node, BLOCKS):
|
|
118
|
+
for body in child_bodies(node):
|
|
119
|
+
class_nodes(body, out)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def dotted(node):
|
|
123
|
+
"""`a.b.C` as ["a", "b", "C"]; `Generic[T]` as its value; None for anything else (a call, ...)."""
|
|
124
|
+
if isinstance(node, ast.Subscript):
|
|
125
|
+
return dotted(node.value)
|
|
126
|
+
if isinstance(node, ast.Name):
|
|
127
|
+
return [node.id]
|
|
128
|
+
if isinstance(node, ast.Attribute):
|
|
129
|
+
head = dotted(node.value)
|
|
130
|
+
return None if head is None else head + [node.attr]
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def bindings(tree):
|
|
135
|
+
"""Module-level names bound by imports: name -> ("module", level, parts) or ("from", level, parts, name)."""
|
|
136
|
+
bound = {}
|
|
137
|
+
for node in ast.walk(tree):
|
|
138
|
+
if isinstance(node, ast.Import):
|
|
139
|
+
for alias in node.names:
|
|
140
|
+
parts = alias.name.split(".")
|
|
141
|
+
if alias.asname:
|
|
142
|
+
bound[alias.asname] = ("module", 0, parts)
|
|
143
|
+
else:
|
|
144
|
+
bound[parts[0]] = ("module", 0, parts[:1])
|
|
145
|
+
elif isinstance(node, ast.ImportFrom):
|
|
146
|
+
parts = node.module.split(".") if node.module else []
|
|
147
|
+
for alias in node.names:
|
|
148
|
+
if alias.name != "*":
|
|
149
|
+
bound[alias.asname or alias.name] = ("from", node.level, parts, alias.name)
|
|
150
|
+
return bound
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class Index:
|
|
154
|
+
"""Classes and import bindings of every scanned file, and module lookup by path."""
|
|
155
|
+
|
|
156
|
+
def __init__(self, trees):
|
|
157
|
+
self.trees = trees
|
|
158
|
+
self.classes = {}
|
|
159
|
+
self.bound = {}
|
|
160
|
+
for path, tree in trees.items():
|
|
161
|
+
nodes = []
|
|
162
|
+
class_nodes(tree.body, nodes)
|
|
163
|
+
self.classes[path] = {node.name: node for node in nodes}
|
|
164
|
+
self.bound[path] = bindings(tree)
|
|
165
|
+
|
|
166
|
+
def module_file(self, importer, level, parts):
|
|
167
|
+
"""The scanned file of a module, absolute (path suffix) or relative to `importer`."""
|
|
168
|
+
if level > 0:
|
|
169
|
+
base = importer.split("/")[:-1]
|
|
170
|
+
if level - 1 > len(base):
|
|
171
|
+
return None
|
|
172
|
+
base = base[: len(base) - (level - 1)]
|
|
173
|
+
stem = "/".join(base + parts)
|
|
174
|
+
for candidate in (stem + ".py", stem + "/__init__.py"):
|
|
175
|
+
if candidate in self.trees:
|
|
176
|
+
return candidate
|
|
177
|
+
return None
|
|
178
|
+
if not parts:
|
|
179
|
+
return None
|
|
180
|
+
stem = "/".join(parts)
|
|
181
|
+
for candidate in sorted(self.trees):
|
|
182
|
+
for suffix in (stem + ".py", stem + "/__init__.py"):
|
|
183
|
+
if candidate == suffix or candidate.endswith("/" + suffix):
|
|
184
|
+
return candidate
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
def class_in(self, path, name, hops):
|
|
188
|
+
"""(file, class) for `name` as seen from module `path`, following re-exports."""
|
|
189
|
+
if path is None or hops > MAX_HOPS:
|
|
190
|
+
return None
|
|
191
|
+
if name in self.classes.get(path, {}):
|
|
192
|
+
return (path, name)
|
|
193
|
+
binding = self.bound.get(path, {}).get(name)
|
|
194
|
+
if binding is None or binding[0] != "from":
|
|
195
|
+
return None
|
|
196
|
+
_, level, parts, imported = binding
|
|
197
|
+
return self.class_in(self.module_file(path, level, parts), imported, hops + 1)
|
|
198
|
+
|
|
199
|
+
def resolve(self, path, parts):
|
|
200
|
+
"""Resolve base `parts` in module `path` to (file, class), or None."""
|
|
201
|
+
head, rest = parts[0], parts[1:]
|
|
202
|
+
if not rest and head in self.classes.get(path, {}):
|
|
203
|
+
return (path, head)
|
|
204
|
+
binding = self.bound.get(path, {}).get(head)
|
|
205
|
+
if binding is None:
|
|
206
|
+
return None
|
|
207
|
+
if binding[0] == "module":
|
|
208
|
+
_, level, mod = binding
|
|
209
|
+
module_parts = mod + rest[:-1]
|
|
210
|
+
if not rest:
|
|
211
|
+
return None
|
|
212
|
+
return self.class_in(self.module_file(path, level, module_parts), rest[-1], 0)
|
|
213
|
+
_, level, mod, imported = binding
|
|
214
|
+
if not rest:
|
|
215
|
+
return self.class_in(self.module_file(path, level, mod), imported, 0)
|
|
216
|
+
# `from pkg import module` then `module.Class`.
|
|
217
|
+
module = self.module_file(path, level, mod + [imported] + rest[:-1])
|
|
218
|
+
return self.class_in(module, rest[-1], 0)
|
|
219
|
+
|
|
220
|
+
def bases(self, path):
|
|
221
|
+
out = {}
|
|
222
|
+
for name, node in self.classes[path].items():
|
|
223
|
+
entries = []
|
|
224
|
+
for base in node.bases:
|
|
225
|
+
parts = dotted(base)
|
|
226
|
+
if parts is not None and parts[-1] in IGNORED_BASES:
|
|
227
|
+
continue
|
|
228
|
+
found = self.resolve(path, parts) if parts is not None else None
|
|
229
|
+
text = ".".join(parts) if parts is not None else ast.dump(base)[:80]
|
|
230
|
+
entries.append({"name": text, "file": found[0] if found else None, "class": found[1] if found else None})
|
|
231
|
+
out[name] = {"bases": entries}
|
|
232
|
+
return out
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def parse(path):
|
|
236
|
+
try:
|
|
237
|
+
with open(path, "rb") as handle:
|
|
238
|
+
return ast.parse(handle.read(), filename=path)
|
|
239
|
+
except (OSError, SyntaxError, ValueError, RecursionError):
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def analyze(path, tree, index):
|
|
244
|
+
if tree is None:
|
|
245
|
+
return {"ok": False}
|
|
246
|
+
symbols = []
|
|
247
|
+
collect(tree.body, symbols)
|
|
248
|
+
return {
|
|
249
|
+
"ok": True,
|
|
250
|
+
"symbols": symbols,
|
|
251
|
+
"allNames": all_names(tree),
|
|
252
|
+
"imports": imports(tree),
|
|
253
|
+
"classes": index.bases(path),
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def main():
|
|
258
|
+
request = json.load(sys.stdin)
|
|
259
|
+
trees = {path: parse(path) for path in request["files"]}
|
|
260
|
+
index = Index({path: tree for path, tree in trees.items() if tree is not None})
|
|
261
|
+
json.dump({path: analyze(path, tree, index) for path, tree in trees.items()}, sys.stdout)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
if __name__ == "__main__":
|
|
265
|
+
main()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Environment variable names read by Python files (6.5). Standard library only.
|
|
2
|
+
|
|
3
|
+
Reads {"files": [relative paths]} as JSON on stdin and prints {path: {"ok": false} | {"ok": true, "names": [str]}}.
|
|
4
|
+
Reads: os.environ["X"] (Load context only), os.environ.get("X"), os.getenv("X"), also through
|
|
5
|
+
"from os import environ, getenv" and "import os as <alias>". Only literal string names count.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import ast
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def literal(node):
|
|
14
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
15
|
+
return node.value
|
|
16
|
+
return None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def analyze(path):
|
|
20
|
+
with open(path, "rb") as handle:
|
|
21
|
+
tree = ast.parse(handle.read().decode("utf-8", errors="replace"), filename=path)
|
|
22
|
+
os_names, environ_names, getenv_names = set(), set(), set()
|
|
23
|
+
for node in ast.walk(tree):
|
|
24
|
+
if isinstance(node, ast.Import):
|
|
25
|
+
for alias in node.names:
|
|
26
|
+
if alias.name == "os":
|
|
27
|
+
os_names.add(alias.asname or "os")
|
|
28
|
+
elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module == "os":
|
|
29
|
+
for alias in node.names:
|
|
30
|
+
if alias.name == "environ":
|
|
31
|
+
environ_names.add(alias.asname or "environ")
|
|
32
|
+
elif alias.name == "getenv":
|
|
33
|
+
getenv_names.add(alias.asname or "getenv")
|
|
34
|
+
|
|
35
|
+
def is_environ(node):
|
|
36
|
+
if isinstance(node, ast.Name):
|
|
37
|
+
return node.id in environ_names
|
|
38
|
+
return isinstance(node, ast.Attribute) and node.attr == "environ" and isinstance(node.value, ast.Name) and node.value.id in os_names
|
|
39
|
+
|
|
40
|
+
names = set()
|
|
41
|
+
for node in ast.walk(tree):
|
|
42
|
+
if isinstance(node, ast.Subscript) and isinstance(node.ctx, ast.Load) and is_environ(node.value):
|
|
43
|
+
name = literal(node.slice)
|
|
44
|
+
if name is not None:
|
|
45
|
+
names.add(name)
|
|
46
|
+
elif isinstance(node, ast.Call) and node.args:
|
|
47
|
+
func = node.func
|
|
48
|
+
name = literal(node.args[0])
|
|
49
|
+
if name is None:
|
|
50
|
+
continue
|
|
51
|
+
if isinstance(func, ast.Attribute) and func.attr == "get" and is_environ(func.value):
|
|
52
|
+
names.add(name)
|
|
53
|
+
elif isinstance(func, ast.Attribute) and func.attr == "getenv" and isinstance(func.value, ast.Name) and func.value.id in os_names:
|
|
54
|
+
names.add(name)
|
|
55
|
+
elif isinstance(func, ast.Name) and func.id in getenv_names:
|
|
56
|
+
names.add(name)
|
|
57
|
+
return sorted(names)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def main():
|
|
61
|
+
request = json.load(sys.stdin)
|
|
62
|
+
out = {}
|
|
63
|
+
for path in request["files"]:
|
|
64
|
+
try:
|
|
65
|
+
out[path] = {"ok": True, "names": analyze(path)}
|
|
66
|
+
except (SyntaxError, ValueError, OSError, RecursionError):
|
|
67
|
+
out[path] = {"ok": False}
|
|
68
|
+
json.dump(out, sys.stdout)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
main()
|