vibecodedshit 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/LICENSE +95 -0
  2. package/README.md +64 -2
  3. package/assets/fonts/NotoEmoji-OFL.txt +93 -0
  4. package/assets/fonts/NotoEmoji-Poop.ttf +0 -0
  5. package/assets/fonts/SpaceGrotesk-Bold.ttf +0 -0
  6. package/assets/fonts/SpaceGrotesk-OFL.txt +93 -0
  7. package/dist/bin.js +8 -0
  8. package/dist/card/previous.js +38 -0
  9. package/dist/card/render.js +71 -0
  10. package/dist/cli.js +121 -0
  11. package/dist/commands/card.js +69 -0
  12. package/dist/commands/common.js +1 -0
  13. package/dist/commands/diff.js +54 -0
  14. package/dist/commands/footer.js +25 -0
  15. package/dist/commands/report.js +49 -0
  16. package/dist/commands/route.js +62 -0
  17. package/dist/commands/submit.js +125 -0
  18. package/dist/commands/summary.js +77 -0
  19. package/dist/diff/compare.js +73 -0
  20. package/dist/diff/conditions.js +87 -0
  21. package/dist/diff/format.js +23 -0
  22. package/dist/exec/docker.js +158 -0
  23. package/dist/exec/findings.js +73 -0
  24. package/dist/exec/plan.js +50 -0
  25. package/dist/exec/reports.js +90 -0
  26. package/dist/exec/roots.js +49 -0
  27. package/dist/exec/run.js +182 -0
  28. package/dist/exec/steps/build.js +45 -0
  29. package/dist/exec/steps/coverage-install.js +12 -0
  30. package/dist/exec/steps/install.js +41 -0
  31. package/dist/exec/steps/knip.js +28 -0
  32. package/dist/exec/steps/lint.js +25 -0
  33. package/dist/exec/steps/lockfiles.js +25 -0
  34. package/dist/exec/steps/test.js +17 -0
  35. package/dist/exec/steps/typecheck.js +40 -0
  36. package/dist/exec/test-reports/coverage-py.js +23 -0
  37. package/dist/exec/test-reports/coverage-ranges.js +65 -0
  38. package/dist/exec/test-reports/coverage-summary.js +23 -0
  39. package/dist/exec/test-reports/jest-json.js +31 -0
  40. package/dist/exec/test-reports/junit.js +24 -0
  41. package/dist/exec/test-reports/mocha-json.js +18 -0
  42. package/dist/exec/test-reports/vitest-json.js +5 -0
  43. package/dist/exec/test-runners/detect.js +99 -0
  44. package/dist/exec/test-runners/index.js +6 -0
  45. package/dist/exec/test-runners/jest.js +27 -0
  46. package/dist/exec/test-runners/mocha.js +28 -0
  47. package/dist/exec/test-runners/node-test.js +24 -0
  48. package/dist/exec/test-runners/pytest.js +18 -0
  49. package/dist/exec/test-runners/script.js +48 -0
  50. package/dist/exec/test-runners/spec.js +1 -0
  51. package/dist/exec/test-runners/vitest.js +28 -0
  52. package/dist/exec/tests.js +120 -0
  53. package/dist/exec/types.js +1 -0
  54. package/dist/manifest/package-json.js +36 -0
  55. package/dist/manifest/python.js +106 -0
  56. package/dist/measure/d3/index.js +45 -0
  57. package/dist/measure/d3/presets.js +100 -0
  58. package/dist/measure/d3/py/adapter.js +67 -0
  59. package/dist/measure/d3/py/config.js +80 -0
  60. package/dist/measure/d3/py/contracts.js +57 -0
  61. package/dist/measure/d3/py/domain.js +57 -0
  62. package/dist/measure/d3/py/graph.js +214 -0
  63. package/dist/measure/d3/py/interpreter.js +31 -0
  64. package/dist/measure/d3/py/layers.js +120 -0
  65. package/dist/measure/d3/py/roots.js +58 -0
  66. package/dist/measure/d3/py/types.js +1 -0
  67. package/dist/measure/d3/scc.js +64 -0
  68. package/dist/measure/d3/ts/adapter.js +371 -0
  69. package/dist/measure/d3/ts/classify.js +107 -0
  70. package/dist/measure/d3/ts/config.js +318 -0
  71. package/dist/measure/d3/ts/domain.js +23 -0
  72. package/dist/measure/d3/ts/rules.js +65 -0
  73. package/dist/measure/d3/types.js +5 -0
  74. package/dist/measure/deps/cvss.js +66 -0
  75. package/dist/measure/deps/declared.js +195 -0
  76. package/dist/measure/deps/files.js +40 -0
  77. package/dist/measure/deps/lockfiles.js +308 -0
  78. package/dist/measure/deps/osv.js +122 -0
  79. package/dist/measure/deps/registry.js +142 -0
  80. package/dist/measure/deps/resolved.js +78 -0
  81. package/dist/measure/deps/units.js +5 -0
  82. package/dist/measure/eslint/health-plugin.js +19 -0
  83. package/dist/measure/ops/docs-commands.js +73 -0
  84. package/dist/measure/ops/docs-resolve.js +217 -0
  85. package/dist/measure/ops/entry-points.js +49 -0
  86. package/dist/measure/ops/env-example.js +20 -0
  87. package/dist/measure/ops/env-reads.js +46 -0
  88. package/dist/measure/ops/runtime.js +73 -0
  89. package/dist/measure/p1-1.js +8 -0
  90. package/dist/measure/p1-2.js +8 -0
  91. package/dist/measure/p1-3.js +8 -0
  92. package/dist/measure/p1-4.js +41 -0
  93. package/dist/measure/p1-5.js +59 -0
  94. package/dist/measure/p2-1.js +8 -0
  95. package/dist/measure/p2-2.js +44 -0
  96. package/dist/measure/p2-3.js +81 -0
  97. package/dist/measure/p2-4.js +288 -0
  98. package/dist/measure/p2-5.js +8 -0
  99. package/dist/measure/p3-1.js +16 -0
  100. package/dist/measure/p3-2.js +26 -0
  101. package/dist/measure/p3-3.js +37 -0
  102. package/dist/measure/p3-4.js +26 -0
  103. package/dist/measure/p3-5.js +26 -0
  104. package/dist/measure/p4-1.js +184 -0
  105. package/dist/measure/p4-2.js +27 -0
  106. package/dist/measure/p4-3.js +106 -0
  107. package/dist/measure/p4-4.js +309 -0
  108. package/dist/measure/p5-1.js +117 -0
  109. package/dist/measure/p5-2.js +80 -0
  110. package/dist/measure/p5-3.js +21 -0
  111. package/dist/measure/p5-4.js +161 -0
  112. package/dist/measure/p5-5.js +158 -0
  113. package/dist/measure/p6-1.js +20 -0
  114. package/dist/measure/p6-2.js +20 -0
  115. package/dist/measure/p6-3.js +40 -0
  116. package/dist/measure/p6-4.js +31 -0
  117. package/dist/measure/p6-5.js +60 -0
  118. package/dist/measure/p6-6.js +54 -0
  119. package/dist/measure/py/typecheck-config.js +95 -0
  120. package/dist/measure/registry.js +31 -0
  121. package/dist/measure/safety/py-tests.js +124 -0
  122. package/dist/measure/safety/ts-barrels.js +68 -0
  123. package/dist/measure/safety/ts-cases.js +111 -0
  124. package/dist/measure/safety/ts-credits.js +101 -0
  125. package/dist/measure/safety/ts-imports.js +63 -0
  126. package/dist/measure/safety/ts-resolve.js +43 -0
  127. package/dist/measure/ts/directives.js +37 -0
  128. package/dist/measure/ts/exports.js +60 -0
  129. package/dist/measure/ts/tsconfig.js +179 -0
  130. package/dist/measure/types.js +1 -0
  131. package/dist/measure/units.js +41 -0
  132. package/dist/methodology/load.js +159 -0
  133. package/dist/methodology/schema.js +105 -0
  134. package/dist/python/deadcode.js +27 -0
  135. package/dist/python/env.js +13 -0
  136. package/dist/python/helper.js +24 -0
  137. package/dist/python/imports.js +40 -0
  138. package/dist/python/nox.js +16 -0
  139. package/dist/python/scope.js +35 -0
  140. package/dist/python/tests.js +19 -0
  141. package/dist/python/typing.js +27 -0
  142. package/dist/report/escape.js +5 -0
  143. package/dist/report/format.js +75 -0
  144. package/dist/report/render.js +213 -0
  145. package/dist/report/roadmap.js +58 -0
  146. package/dist/report/scares.js +83 -0
  147. package/dist/report/style.js +27 -0
  148. package/dist/report-only/hotspots.js +48 -0
  149. package/dist/route/check.js +37 -0
  150. package/dist/route/files.js +74 -0
  151. package/dist/route/order.js +22 -0
  152. package/dist/route/select.js +114 -0
  153. package/dist/route/write.js +13 -0
  154. package/dist/run/read.js +89 -0
  155. package/dist/run/recompute.js +19 -0
  156. package/dist/run/scan.js +196 -0
  157. package/dist/run/schema.js +1 -0
  158. package/dist/run/score-run.js +42 -0
  159. package/dist/run/write.js +4 -0
  160. package/dist/scope/build.js +173 -0
  161. package/dist/scope/packages.js +87 -0
  162. package/dist/scope/rules.js +21 -0
  163. package/dist/scope/tracked.js +34 -0
  164. package/dist/scope/ts-lines.js +63 -0
  165. package/dist/scope/types.js +7 -0
  166. package/dist/scoring/aggregate.js +38 -0
  167. package/dist/scoring/pool.js +65 -0
  168. package/dist/scoring/score.js +81 -0
  169. package/dist/scoring/what-if.js +38 -0
  170. package/dist/scripts/checks.js +53 -0
  171. package/dist/scripts/ci.js +139 -0
  172. package/dist/scripts/entry-points/find.js +31 -0
  173. package/dist/scripts/entry-points/index.js +18 -0
  174. package/dist/scripts/entry-points/justfile.js +62 -0
  175. package/dist/scripts/entry-points/makefile.js +66 -0
  176. package/dist/scripts/entry-points/nox.js +29 -0
  177. package/dist/scripts/entry-points/package-json.js +21 -0
  178. package/dist/scripts/entry-points/pyproject.js +73 -0
  179. package/dist/scripts/entry-points/tox.js +130 -0
  180. package/dist/scripts/expand.js +300 -0
  181. package/dist/scripts/triggers.js +151 -0
  182. package/dist/scripts/types.js +1 -0
  183. package/dist/submit/constants.js +9 -0
  184. package/dist/submit/payload.js +42 -0
  185. package/dist/submit/repo-identity.js +46 -0
  186. package/dist/submit/send.js +29 -0
  187. package/dist/tools/catalog.js +46 -0
  188. package/dist/tools/image.js +180 -0
  189. package/dist/tools/kit.js +71 -0
  190. package/dist/tools/knip.js +169 -0
  191. package/dist/tools/outcome.js +21 -0
  192. package/dist/tools/resolve.js +66 -0
  193. package/dist/tools/workcopy.js +27 -0
  194. package/dist/util/ini.js +37 -0
  195. package/dist/util/pep440.js +163 -0
  196. package/dist/util/toml.js +27 -0
  197. package/docker/Dockerfile +142 -0
  198. package/docker/bin/health-capture +5 -0
  199. package/docker/bin/health-coverage-install +5 -0
  200. package/docker/bin/health-eslint +66 -0
  201. package/docker/bin/health-pytest +10 -0
  202. package/docker/bin/health-tsc +10 -0
  203. package/docker/bin/health-venv +11 -0
  204. package/docker/config/eslint.config.mjs +4 -0
  205. package/docker/config/eslint.rules.mjs +65 -0
  206. package/docker/config/eslint.untyped.config.mjs +4 -0
  207. package/docker/config/knip.json +1 -0
  208. package/docker/config/ruff.toml +7 -0
  209. package/methodology/LICENSE +396 -0
  210. package/methodology/v1.0/METHODOLOGY.md +1176 -0
  211. package/methodology/v1.0/dimensions/1-safety-net.yaml +240 -0
  212. package/methodology/v1.0/dimensions/2-type-static-rigor.yaml +289 -0
  213. package/methodology/v1.0/dimensions/3-architecture-boundaries.yaml +266 -0
  214. package/methodology/v1.0/dimensions/4-complexity-maintainability.yaml +192 -0
  215. package/methodology/v1.0/dimensions/5-dependency-security-hygiene.yaml +223 -0
  216. package/methodology/v1.0/dimensions/6-operability-feedback-loop.yaml +279 -0
  217. package/methodology/v1.0/methodology.yaml +242 -0
  218. package/methodology/v1.0/presets/django.yaml +43 -0
  219. package/methodology/v1.0/presets/express.yaml +47 -0
  220. package/methodology/v1.0/presets/fastapi.yaml +52 -0
  221. package/methodology/v1.0/presets/generic.yaml +41 -0
  222. package/methodology/v1.0/presets/nextjs.yaml +50 -0
  223. package/methodology/v1.0/scope.yaml +115 -0
  224. package/package.json +61 -4
  225. package/python/dead_code_helper.py +265 -0
  226. package/python/env_helper.py +71 -0
  227. package/python/graph_helper.py +277 -0
  228. package/python/imports_helper.py +132 -0
  229. package/python/nox_helper.py +79 -0
  230. package/python/scope_helper.py +96 -0
  231. package/python/tests_helper.py +171 -0
  232. package/python/typing_helper.py +235 -0
@@ -0,0 +1,1176 @@
1
+ # Codebase Health Methodology, version 1.0
2
+
3
+ This document explains how a codebase health score is computed and why each rule exists. The YAML files in this folder are the source of truth; section 6 is generated from them. If this document and the YAML disagree, the YAML is correct.
4
+
5
+ | File | Contents |
6
+ |---|---|
7
+ | `methodology.yaml` | Aggregation, bands, execution rules, delta rules, regression flags |
8
+ | `scope.yaml` | Which files count and how lines are counted |
9
+ | `dimensions/*.yaml` | The 30 parameters: weights, anchors, measurement, rationale, sources |
10
+ | `presets/*.yaml` | Default import rules for Next.js, Express, Django, FastAPI and generic projects |
11
+
12
+ ## 1. Purpose
13
+
14
+ **Health is how safely and cheaply someone with no memory of this codebase, whether a contractor or an agent, can make a correct change.**
15
+
16
+ The methodology is built for three uses:
17
+
18
+ 1. **Audits.** A run gives a baseline score, six dimension scores and the 30 parameter values behind them.
19
+ 2. **Before and after.** A second run after the work shows what the fixes changed, parameter by parameter.
20
+ 3. **Sprint-loop gate.** Every automated change is scored against its base commit. Regressions are flagged and never block.
21
+
22
+ It covers TypeScript, JavaScript and Python. Every parameter is computed from the repository by a tool. There are no surveys, no manual judgements and no AI grading. One parameter (6.3, feedback loop time) depends on wall-clock time and can vary slightly between identical runs; every other parameter gives the same result for the same commit, tool versions and as-of date.
23
+
24
+ ## 2. How a score is computed
25
+
26
+ ### Parameter score
27
+
28
+ Each parameter has a **raw value** measured from the repository, such as "62% line coverage" or "3.1 escape hatches per 1,000 lines". A fixed rubric turns the raw value into a score from 0 to 100.
29
+
30
+ Most rubrics are **piecewise-linear**: 3 or 4 anchors, each with a raw value, a score and a written meaning. Between two anchors the score is interpolated linearly. Below the first anchor and above the last, the score is clamped. Yes/no facts and configuration choices use **levels** instead, where each named level has a fixed score.
31
+
32
+ Example, 1.2 line coverage at 52%:
33
+
34
+ | Raw | Score |
35
+ |---|---|
36
+ | 40% | 50 |
37
+ | 60% | 75 |
38
+
39
+ 52% is 12/20 of the way from 40% to 60%, so the score is 50 + 0.6 × 25 = **65**.
40
+
41
+ Anchors are fixed. They do not depend on other repositories, so a score means the same thing in every audit and every year until the methodology version changes.
42
+
43
+ ### Dimension score
44
+
45
+ A dimension score is the weighted mean of its parameter scores. Weights within each dimension add up to 100.
46
+
47
+ Example, D1 Safety net:
48
+
49
+ | Parameter | Raw | Score | Weight | Contribution |
50
+ |---|---|---|---|---|
51
+ | 1.1 Suite pass rate | 97% | 70 | 20% | 14.0 |
52
+ | 1.2 Line coverage | 52% | 65 | 25% | 16.3 |
53
+ | 1.3 Branch coverage | 38% | 60 | 15% | 9.0 |
54
+ | 1.4 Modules with tests | 55% | 62.5 | 25% | 15.6 |
55
+ | 1.5 Tests that assert something | 92% | 70 | 15% | 10.5 |
56
+ | **D1** | | | | **65** |
57
+
58
+ ### Not applicable
59
+
60
+ A parameter can be not applicable only when its precondition is a design choice the methodology does not score. In v1.0 there are two such cases:
61
+
62
+ - **3.4 Public-entry discipline**, when the repository has fewer than two modules.
63
+ - **3.5 Domain purity**, when the repository declares no domain layer.
64
+
65
+ The weight of a not-applicable parameter is spread over the other parameters in the same dimension, in proportion to their weights. With 3.5 not applicable, D3's weights become 17.6 / 35.3 / 29.4 / 17.6.
66
+
67
+ Missing tests, a failed install or a failed test run are never "not applicable". They score 0.
68
+
69
+ ### Health score
70
+
71
+ ```
72
+ health = min( mean(D1..D6), min(D1..D6) + 30 )
73
+ ```
74
+
75
+ The six dimensions have equal weight. The cap means one very weak dimension pulls the headline down, because a codebase with no tests is not made safe by clean dependencies.
76
+
77
+ | Example | D1 | D2 | D3 | D4 | D5 | D6 | Mean | Weakest + 30 | Health |
78
+ |---|---|---|---|---|---|---|---|---|---|
79
+ | Balanced | 72 | 65 | 58 | 81 | 90 | 60 | 71.0 | 88 | **71** Stable |
80
+ | No tests | 5 | 80 | 80 | 80 | 80 | 80 | 67.5 | 35 | **35** Vibe-coded |
81
+
82
+ ### Bands
83
+
84
+ | Health | Band | Meaning |
85
+ |---|---|---|
86
+ | 0–39 | Vibe-coded | Changes break things without anyone noticing. Nobody, human or agent, can work here safely. |
87
+ | 40–59 | Fragile | It works, but every change is a gamble. |
88
+ | 60–79 | Stable | Changes are safe with care and review. |
89
+ | 80–100 | Agent-ready | An agent with no memory of the codebase can make a correct change and verify it alone. |
90
+
91
+ Because of the cap, Agent-ready requires every dimension to score at least 49.5.
92
+
93
+ ### Precision
94
+
95
+ Scores are stored at full precision in `run.json`. The report shows health and dimension scores as integers (rounded half up) and the health delta with one decimal. The band comes from the displayed health score.
96
+
97
+ ## 3. Layers and execution
98
+
99
+ Every run has two layers.
100
+
101
+ | Layer | What it does | Parameters |
102
+ |---|---|---|
103
+ | Static | Reads tracked files, parses ASTs, builds import graphs. Never runs repository code. | 23 |
104
+ | Execution | Installs dependencies, then runs the build, the typechecker, a pinned lint ruleset and the test suite with coverage. | 7 (1.1, 1.2, 1.3, 2.1, 2.5, 6.1, 6.3) |
105
+
106
+ Both layers run on every scan. The execution layer runs in a pinned Docker image, `health:1.0`, with 4 CPUs and 8 GB of memory. The same image runs the client's code, so nothing from the repository runs on the host.
107
+
108
+ - **Network** is allowed while installing dependencies and while fetching the advisory and registry snapshots. It is off for the build, typecheck, lint and tests.
109
+ - **Environment.** Variables listed in the repository's env example file are loaded with their example values. No other variables or secrets are provided. External services such as databases, queues and browsers are not provided.
110
+ - **Timeouts.** Install 15 minutes, build 15 minutes, typecheck 10 minutes, lint 10 minutes, tests 10 minutes. The test suite runs once.
111
+
112
+ There are two kinds of failure:
113
+
114
+ | Failure | Cause | Result |
115
+ |---|---|---|
116
+ | Repository failure | Install, build, typecheck or tests fail or time out because of the repository | Every parameter that needs that step scores 0. An install that fails in one install root zeroes only the units beneath that root; the other roots are still measured. The run is valid. |
117
+ | Tool error | The scanner itself fails, for example a crash or coverage data missing after a test run that produced results | The run is invalid and produces no score. |
118
+
119
+ ## 4. Scope
120
+
121
+ Full rules are in `scope.yaml`.
122
+
123
+ - **Files.** The scan starts from `git ls-files` and keeps TypeScript, JavaScript and Python files.
124
+ - **Fixed exclusions.** Vendored code, build output, generated files, minified files, migrations, `.d.ts` files and test data (fixtures, snapshots, testdata) are excluded.
125
+ - **File roles.** Each remaining file is a test file, a config file or a logic file. Test directories (`test`, `tests`, `e2e`) are recognised at any depth. A logic file defines at least one function, method or class. Barrels, type-only files and constants-only files are not logic files.
126
+ - **Tests.** Test files are scored in D1, and also in 2.1 (typecheck errors) and 2.5 (lint findings), because broken tests are a real problem. They are excluded from every other static metric.
127
+ - **Line counts.** *Logic lines* are non-blank, non-comment lines in logic files; Python docstrings count as comments. *Source lines* add test files. Per-1,000-line parameters use one or the other, as each parameter states.
128
+ - **`.healthignore`.** A repository can exclude more paths with gitignore syntax. The report shows the percentage of lines excluded, and `health diff` refuses to compare runs with different ignore files. `.healthignore` does not apply to the secret scan (5.4).
129
+
130
+ ### Monorepos and mixed languages
131
+
132
+ A repository always gets one headline score.
133
+
134
+ - **Ratio parameters** (percentages, counts per 1,000 lines, pass rates) sum numerators and denominators across packages and languages, then score the pooled ratio once. A unit under an install root whose install failed takes no part in the sums; the pooled score is multiplied by the share of the logic lines that is not failed, so a broken package costs its share.
135
+ - **Configuration and level parameters** are scored per language or package and combined by logic-line weight. Example: 2.2 scores 100 for a TypeScript frontend with 8,000 logic lines and 0 for a Python backend with 2,000 logic lines. The pooled 2.2 score is 80.
136
+
137
+ The report breaks every dimension down per package and per language.
138
+
139
+ ## 5. Comparing runs and regression flags
140
+
141
+ ### The as-of date
142
+
143
+ Two parameters depend on outside data that changes over time: 5.1 (known vulnerabilities) and 5.2 (dependencies a major version behind). Every run has an **as-of date**. Advisories published on or before that date count, and "latest version" means the latest version published on or before that date.
144
+
145
+ Without this, a repository's score would drop when a new advisory is published, even if nobody changed the code.
146
+
147
+ ### Delta rule
148
+
149
+ `health diff before.json after.json` compares two runs only when all five conditions match. Otherwise it refuses and names the conditions that differ.
150
+
151
+ 1. **Layers.** Both runs attempted the same layers.
152
+ 2. **Tool versions.** Both runs used the same image digest.
153
+ 3. **Methodology version.** Both runs used the same methodology version.
154
+ 4. **As-of date.** Both runs were scored at the same as-of date. In a contract, this is the baseline's as-of date. The report also shows the newer run's score as of today.
155
+ 5. **Ignore set.** Both runs used the same `.healthignore` and `.gitleaksignore` contents.
156
+
157
+ `run.json` stores raw values and the resolved dependency list, so the newer run can be rescored at the baseline's as-of date without rescanning.
158
+
159
+ ### Sprint-loop regression flags
160
+
161
+ Each change is scored against its base commit, at the same as-of date. Base commit runs are cached by tree hash, methodology version and as-of date.
162
+
163
+ A change is flagged when **any dimension drops by 3.0 points or more** (unrounded), or when **the band drops**. A flagged pull request gets the `health-regression` label. Flags never block a merge.
164
+
165
+ Telegram summary format:
166
+
167
+ ```
168
+ Health 62 Stable (+1.4) · Safety net +6 · Complexity −1 · ⚠ none
169
+ ```
170
+
171
+ ## 6. The six dimensions
172
+
173
+ | # | Dimension | Question |
174
+ |---|---|---|
175
+ | D1 | Safety net | Will a change break something without anyone noticing? |
176
+ | D2 | Type & static rigor | Does the code tell you what it expects before you run it? |
177
+ | D3 | Architecture & boundaries | Can you change one part without reading the rest? |
178
+ | D4 | Complexity & maintainability | How much does someone have to read to make one change correctly? |
179
+ | D5 | Dependency & security hygiene | Is the code built on parts that are known, current and safe? |
180
+ | D6 | Operability & feedback loop | Can someone new set it up, check a change and know they are done? |
181
+
182
+ ### D1. Safety net
183
+
184
+ **Question.** Will a change break something without anyone noticing?
185
+
186
+ **Why it matters for agents.** An agent cannot remember how the system is supposed to behave. The test suite is the only record of that behaviour it can check its change against.
187
+
188
+ | # | Parameter | Layer | Weight |
189
+ |---|---|---|---|
190
+ | 1.1 | Suite pass rate | Execution | 20% |
191
+ | 1.2 | Line coverage | Execution | 25% |
192
+ | 1.3 | Branch coverage | Execution | 15% |
193
+ | 1.4 | Modules with tests | Static | 25% |
194
+ | 1.5 | Tests that assert something | Static | 15% |
195
+
196
+ #### 1.1 Suite pass rate
197
+
198
+ Weight 20%. Execution layer. Scores 0 if any of these steps fail: install, test run. Pooled as a ratio across languages and packages.
199
+
200
+ **Raw value** (percent). passed / (passed + failed + errored) test cases. Skipped and expected-failure tests are excluded from both numerator and denominator. A test file that fails to load or collect counts as one failed test case.
201
+
202
+ | Raw | Score | Meaning |
203
+ |---|---|---|
204
+ | 80 | 0 | The suite is red. Nobody can use it to check a change. |
205
+ | 95 | 50 | Mostly green, with known failures people have learned to ignore. |
206
+ | 100 | 100 | Green. A new failure means the change broke something. |
207
+
208
+ Scores are interpolated between anchors and clamped outside them. If there is nothing to count, the score is 0.
209
+
210
+ **How it is measured.**
211
+
212
+ - TypeScript / JavaScript: vitest, jest, mocha or node:test, detected from package.json dependencies and the test script. Run once with a JSON reporter. Playwright and Cypress suites are not run.
213
+ - Python: pytest (also runs unittest suites), with --continue-on-collection-errors and a JSON or JUnit report.
214
+
215
+ **Why.** A suite with known failures stops working as a check: a new failure looks the same as the old ones. The curve is steep on purpose. 95% of a 200-test suite is 10 failing tests, which is already enough for people to stop reading the output.
216
+
217
+ #### 1.2 Line coverage
218
+
219
+ Weight 25%. Execution layer. Scores 0 if any of these steps fail: install, test run. Pooled as a ratio across languages and packages.
220
+
221
+ **Raw value** (percent). Covered lines / coverable lines across all logic files, including logic files no test loads. A file no test loads counts as 0% covered. For TypeScript and JavaScript the coverable lines are health's code lines (lines with at least one token). A line counts as covered when the innermost statement or function containing it ran.
222
+
223
+ | Raw | Score | Meaning |
224
+ |---|---|---|
225
+ | 10 | 0 | No safety net. |
226
+ | 40 | 50 | Happy paths only. |
227
+ | 60 | 75 | Acceptable. |
228
+ | 80 | 100 | Well covered. No extra credit above this. |
229
+
230
+ Scores are interpolated between anchors and clamped outside them. If there is nothing to count, the score is 0.
231
+
232
+ **How it is measured.**
233
+
234
+ - TypeScript / JavaScript: v8 or istanbul coverage with the json-summary and json reporters (vitest coverage.include, jest collectCoverageFrom over logic files, c8 for mocha and node:test). Lines are counted by health from the json report's statement and function ranges, not taken from the tool, so the same code gives the same count under every runner. Repository coverage thresholds are disabled so they cannot fail the run.
235
+ - Python: coverage.py through pytest-cov, with --cov set to every first-party package so files no test imports are included.
236
+
237
+ **Why.** Coverage shows which code no test executes. It does not show that the executed code is checked, which is why 1.5 exists. The 60% anchor matches Google's "acceptable" level. The 100 anchor is 80%, which is the coverage SonarQube's default quality gate requires on new code and sits between Google's "commendable" (75%) and "exemplary" (90%). Above 80% the methodology gives no extra credit, because the remaining code is often glue that costs more to test than it returns.
238
+
239
+ **Sources.**
240
+
241
+ - Arguelles, Ivanković, Bender. Code Coverage Best Practices. Google Testing Blog, 2020. https://testing.googleblog.com/2020/08/code-coverage-best-practices.html
242
+ - SonarQube default quality gate (Sonar way): coverage on new code at least 80%.
243
+
244
+ #### 1.3 Branch coverage
245
+
246
+ Weight 15%. Execution layer. Scores 0 if any of these steps fail: install, test run. Pooled as a ratio across languages and packages.
247
+
248
+ **Raw value** (percent). Covered branches / total branches across all logic files, including files no test loads.
249
+
250
+ | Raw | Score | Meaning |
251
+ |---|---|---|
252
+ | 5 | 0 | Only one path through each decision is ever run. |
253
+ | 30 | 50 | Error and edge paths are mostly untested. |
254
+ | 50 | 75 | Main error paths are tested. |
255
+ | 70 | 100 | Most decisions are tested both ways. |
256
+
257
+ Scores are interpolated between anchors and clamped outside them. If there is nothing to count, the score is 0.
258
+
259
+ **How it is measured.**
260
+
261
+ - TypeScript / JavaScript: Same coverage run as 1.2; branch totals from json-summary. They come from the runner's coverage tool and can differ slightly between runners on the same code.
262
+ - Python: Same run as 1.2 with --cov-branch.
263
+
264
+ **Why.** Tests written quickly cover the happy path. Line coverage can be high while every error branch is untested. Branch coverage shows that gap. Branch percentages are lower than line percentages on the same code, so the anchors sit about 10 points lower than 1.2's.
265
+
266
+ #### 1.4 Modules with tests
267
+
268
+ Weight 25%. Static layer. Pooled as a ratio across languages and packages.
269
+
270
+ **Raw value** (percent). Logic files directly imported by at least one test file / all logic files. Imports are followed through barrel files and Python __init__.py re-exports to the file that defines the imported name. Transitive imports do not count.
271
+
272
+ | Raw | Score | Meaning |
273
+ |---|---|---|
274
+ | 10 | 0 | Almost nothing has a test. |
275
+ | 40 | 50 | Some modules have tests; most do not. |
276
+ | 70 | 75 | Most modules have a test. |
277
+ | 90 | 100 | Nearly every module has a test. |
278
+
279
+ Scores are interpolated between anchors and clamped outside them. If there is nothing to count, the score is 0.
280
+
281
+ **How it is measured.**
282
+
283
+ - TypeScript / JavaScript: AST import graph from test files (static imports, require and dynamic import with a literal path).
284
+ - Python: AST import graph from test files (import and from-import statements).
285
+
286
+ **Why.** This is the static counterpart to coverage. It still works when the suite does not run, so a repository with tests that are broken gets partial credit, and "make the existing tests run" shows up as a gain in 1.1 to 1.3. Only direct imports count; otherwise one test that imports the app entry point would mark every file as tested.
287
+
288
+ #### 1.5 Tests that assert something
289
+
290
+ Weight 15%. Static layer. Pooled as a ratio across languages and packages.
291
+
292
+ **Raw value** (percent). Test cases containing at least one assertion / all test cases. An assertion inside a helper function defined in the same file and called by the test counts (one level deep).
293
+
294
+ | Raw | Score | Meaning |
295
+ |---|---|---|
296
+ | 50 | 0 | Many tests run code without checking anything. |
297
+ | 80 | 50 | A noticeable share of tests check nothing. |
298
+ | 95 | 75 | A few tests check nothing. |
299
+ | 100 | 100 | Every test checks something. |
300
+
301
+ Scores are interpolated between anchors and clamped outside them. If there is nothing to count, the score is 0.
302
+
303
+ **How it is measured.**
304
+
305
+ - TypeScript / JavaScript: Test case: a call to it, test or their .each/.only/.concurrent variants. Assertion: expect(...), assert(...), assert.*, expectTypeOf(...), t.assert* / t.is / t.deepEqual (node:test and ava contexts).
306
+ - Python: Test case: a test_* function in a test module, or a test_* method of a unittest.TestCase subclass. Assertion: an assert statement, self.assert*, pytest.raises, pytest.warns, or a mock method call starting with assert_.
307
+
308
+ **Why.** A test without an assertion raises coverage and checks nothing. This is the cheapest way to game 1.2 and 1.3, and it is common in generated tests. The anchors are strict because writing a test without an assertion is almost never intentional.
309
+
310
+ ### D2. Type & static rigor
311
+
312
+ **Question.** Does the code tell you what it expects before you run it?
313
+
314
+ **Why it matters for agents.** Type errors and lint findings are the fastest feedback an agent gets. A strict typechecker catches a wrong change in seconds, before any test runs, and the types document what each function accepts and returns.
315
+
316
+ | # | Parameter | Layer | Weight |
317
+ |---|---|---|---|
318
+ | 2.1 | Typecheck errors | Execution | 25% |
319
+ | 2.2 | Declared strictness | Static | 20% |
320
+ | 2.3 | Escape hatches | Static | 20% |
321
+ | 2.4 | Typed public surface | Static | 15% |
322
+ | 2.5 | Baseline lint findings | Execution | 20% |
323
+
324
+ #### 2.1 Typecheck errors
325
+
326
+ Weight 25%. Execution layer. Scores 0 if any of these steps fail: install, typecheck run. Pooled as a ratio across languages and packages.
327
+
328
+ **Raw value** (errors per 1,000 source lines). Typechecker errors located in in-scope files (logic and test), divided by source lines / 1,000. Errors in excluded files and in dependencies are ignored. Warnings are ignored.
329
+
330
+ | Raw | Score | Meaning |
331
+ |---|---|---|
332
+ | 0 | 100 | Clean. A new error means the change introduced it. |
333
+ | 1 | 75 | A few known errors. |
334
+ | 5 | 50 | Errors are routine and people work around them. |
335
+ | 20 | 0 | The typechecker is ignored. |
336
+
337
+ Scores are interpolated between anchors and clamped outside them. If there is nothing to count, the score is 100.
338
+
339
+ **How it is measured.**
340
+
341
+ - TypeScript / JavaScript: tsc --noEmit with the repository's tsconfig.json (each tsconfig that a workspace package declares). JavaScript-only repositories without a tsconfig or jsconfig are checked with a default config: allowJs, checkJs, strict false.
342
+ - Python: pyright with the repository's configuration (pyrightconfig.json or [tool.pyright]). If only mypy is configured, mypy with its configuration. If both are configured, pyright. If neither, pyright in basic mode.
343
+
344
+ **Why.** The repository's own config is used so the count reflects what its developers see. A config error, or a checker that cannot start, is a repository failure and scores 0. Untyped code is checked with a default config on purpose, so "add types" is visible as a score change.
345
+
346
+ **Sources.**
347
+
348
+ - TypeScript: Type checking JavaScript files (checkJs). https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html
349
+ - Pyright configuration: typeCheckingMode. https://microsoft.github.io/pyright/#/configuration
350
+
351
+ #### 2.2 Declared strictness
352
+
353
+ Weight 20%. Static layer. Scored per language and package, then combined by logic-line weight.
354
+
355
+ **Raw value** (level). The strictness the repository's typechecker configuration declares, read from the config files without running anything.
356
+
357
+ TypeScript / JavaScript:
358
+
359
+ | Level | Score | Meaning |
360
+ |---|---|---|
361
+ | none | 0 | No strict-family flag is enabled, or JavaScript without checkJs. |
362
+ | partial | 40 | Some strict-family flags are enabled, or strict is true with one or more strict-family flags turned back off. |
363
+ | strict | 75 | strict is true and no strict-family flag is turned off. |
364
+ | strict_plus | 100 | strict plus noUncheckedIndexedAccess. |
365
+
366
+ Python:
367
+
368
+ | Level | Score | Meaning |
369
+ |---|---|---|
370
+ | none | 0 | No pyright or mypy configuration. |
371
+ | basic | 40 | pyright typeCheckingMode basic, or mypy with default settings. |
372
+ | standard | 75 | pyright typeCheckingMode standard (pyright's default when a config exists), or mypy with disallow_untyped_defs and check_untyped_defs. |
373
+ | strict | 100 | pyright typeCheckingMode strict, or mypy strict = true. |
374
+
375
+ **How it is measured.**
376
+
377
+ - TypeScript / JavaScript: tsconfig.json with extends resolved. Strict-family flags: noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, useUnknownInCatchVariables, alwaysStrict.
378
+ - Python: pyrightconfig.json, [tool.pyright] or [tool.basedpyright] in pyproject.toml; mypy.ini, .mypy.ini, setup.cfg [mypy] or [tool.mypy] in pyproject.toml. When both are configured, pyright's level is used.
379
+
380
+ **Why.** 2.1 counts errors under whatever config the repository chose. Without 2.2, turning strictness off would raise the score. Strict null checks catch access to null and undefined values, a common runtime error in both languages. noUncheckedIndexedAccess makes array and record lookups return possibly undefined values, which is where strict TypeScript still lets runtime errors through.
381
+
382
+ **Sources.**
383
+
384
+ - TypeScript TSConfig reference: strict. https://www.typescriptlang.org/tsconfig#strict
385
+ - TypeScript TSConfig reference: noUncheckedIndexedAccess. https://www.typescriptlang.org/tsconfig#noUncheckedIndexedAccess
386
+ - Pyright configuration: typeCheckingMode. https://microsoft.github.io/pyright/#/configuration
387
+ - mypy command line: --strict. https://mypy.readthedocs.io/en/stable/command_line.html
388
+
389
+ #### 2.3 Escape hatches
390
+
391
+ Weight 20%. Static layer. Pooled as a ratio across languages and packages.
392
+
393
+ **Raw value** (escape hatches per 1,000 logic lines). Points where code opts out of the typechecker, counted in logic files, divided by logic lines / 1,000. A file-level opt-out counts as 10.
394
+
395
+ | Raw | Score | Meaning |
396
+ |---|---|---|
397
+ | 0 | 100 | The types are trusted everywhere. |
398
+ | 2 | 75 | A few documented exceptions. |
399
+ | 5 | 50 | Casts are a common way to make errors go away. |
400
+ | 15 | 0 | The types say little about what the code does. |
401
+
402
+ Scores are interpolated between anchors and clamped outside them. If there is nothing to count, the score is 100.
403
+
404
+ **How it is measured.**
405
+
406
+ - TypeScript / JavaScript: Each counts 1: an explicit any type; an as assertion (as const is excluded; each as in "as unknown as T" counts separately); an angle-bracket assertion <T>x; a non-null assertion x!; a definite assignment assertion x!: T; @ts-ignore; @ts-expect-error. Counts 10: @ts-nocheck.
407
+ - Python: Each counts 1: Any in an annotation; typing.cast(...); # type: ignore on a line; # pyright: ignore on a line. Counts 10: # type: ignore before the first statement of a module; # mypy: ignore-errors.
408
+
409
+ **Why.** Each escape hatch is a place where the typechecker cannot help the next person. A change that breaks a contract passes the typechecker silently at that point. The typescript-eslint no-unsafe-* rules are not used in 2.5, so the same problem is not counted twice.
410
+
411
+ #### 2.4 Typed public surface
412
+
413
+ Weight 15%. Static layer. Pooled as a ratio across languages and packages.
414
+
415
+ **Raw value** (percent). Typed slots / all slots on the public surface of logic files. In TypeScript and JavaScript a slot is a parameter. In Python a slot is a parameter or a return type.
416
+
417
+ | Raw | Score | Meaning |
418
+ |---|---|---|
419
+ | 20 | 0 | The public surface is untyped. |
420
+ | 50 | 50 | Half of what callers use is typed. |
421
+ | 80 | 75 | Most of what callers use is typed. |
422
+ | 100 | 100 | Every public function says what it accepts. |
423
+
424
+ Scores are interpolated between anchors and clamped outside them. If there is nothing to count, the score is 100.
425
+
426
+ **How it is measured.**
427
+
428
+ - TypeScript / JavaScript: Public surface: exported functions, exported arrow functions and function expressions, default exports, and public methods of exported classes. A parameter is typed if it has a type annotation, a default value, a contextual type (for example a callback passed to a typed function), or a JSDoc @param type in a file checked with checkJs.
429
+ - Python: Public surface: module-level functions and classes whose names do not start with _, and their methods whose names do not start with _ (dunder methods are included). If __all__ is defined, it decides what is public. Modules whose names start with _ are excluded. self and cls are not slots. __init__ needs no return annotation.
430
+
431
+ **Why.** The public surface is what the next person calls without reading the body. In TypeScript, inferred return types are reliable, so only parameters are counted. In Python, an unannotated return is Any, so both are counted. Untyped JavaScript and untyped Python score low on purpose.
432
+
433
+ **Sources.**
434
+
435
+ - PEP 484: Type Hints. https://peps.python.org/pep-0484/
436
+
437
+ #### 2.5 Baseline lint findings
438
+
439
+ Weight 20%. Execution layer. Scores 0 if any of these steps fail: install, lint run. Pooled as a ratio across languages and packages.
440
+
441
+ **Raw value** (findings per 1,000 source lines). Findings from the methodology's pinned correctness ruleset in in-scope files (logic and test), divided by source lines / 1,000. The repository's own lint config is not used. A file that cannot be parsed counts as one finding.
442
+
443
+ | Raw | Score | Meaning |
444
+ |---|---|---|
445
+ | 0 | 100 | No known bug patterns. |
446
+ | 2 | 75 | A few. |
447
+ | 5 | 50 | Bug patterns are common. |
448
+ | 20 | 0 | The code has never been linted for correctness. |
449
+
450
+ Scores are interpolated between anchors and clamped outside them. If there is nothing to count, the score is 100.
451
+
452
+ **How it is measured.**
453
+
454
+ - TypeScript / JavaScript: ESLint with the pinned ruleset. Core rules: no-unreachable, no-dupe-keys, no-self-compare, no-unsafe-finally, no-fallthrough, no-cond-assign, use-isnan, valid-typeof, no-unsafe-optional-chaining, no-dupe-else-if, no-duplicate-case, no-self-assign, getter-return, no-async-promise-executor, no-constant-binary-expression, no-unused-vars (or @typescript-eslint/no-unused-vars on TypeScript files). Type-aware rules on TypeScript files only: @typescript-eslint/no-floating-promises, no-misused-promises, await-thenable. TypeScript files outside every tsconfig are linted without the type-aware rules. react-hooks/rules-of-hooks when react is a dependency.
455
+ - Python: ruff with the rule sets F (Pyflakes), B (flake8-bugbear) and E9 (syntax and IO errors).
456
+
457
+ **Why.** Each rule in the set catches a real bug pattern, such as an unawaited promise or an unreachable branch. Style rules are excluded, so formatting choices do not affect the score. The ruleset is pinned so every repository is judged by the same rules, and so turning rules off in the repository's own config does not raise the score.
458
+
459
+ **Sources.**
460
+
461
+ - typescript-eslint rules. https://typescript-eslint.io/rules/
462
+ - Ruff rules. https://docs.astral.sh/ruff/rules/
463
+
464
+ ### D3. Architecture & boundaries
465
+
466
+ **Question.** Can you change one part without reading the rest?
467
+
468
+ **Why it matters for agents.** An agent works inside a limited context window. Enforced boundaries limit how much of the codebase one change can affect, so the agent only needs to read the module it is changing and that module's public entry points.
469
+
470
+ | # | Parameter | Layer | Weight |
471
+ |---|---|---|---|
472
+ | 3.1 | Boundaries declared and enforced | Static | 15% |
473
+ | 3.2 | Boundary violations | Static | 30% |
474
+ | 3.3 | Import cycles | Static | 25% |
475
+ | 3.4 | Public-entry discipline | Static | 15% |
476
+ | 3.5 | Domain purity | Static | 15% |
477
+
478
+ **Presets.** When a repository declares no import rules, the scan uses the preset for the detected framework (presets/*.yaml). When it declares rules (see 3.1), its rules replace the preset. Detection order: nextjs, express, django, fastapi, generic. Each workspace package gets its own preset.
479
+
480
+ #### 3.1 Boundaries declared and enforced
481
+
482
+ Weight 15%. Static layer. Scored per language and package, then combined by logic-line weight.
483
+
484
+ **Raw value** (level). Whether the repository declares import rules in a supported rules file, and whether CI runs them.
485
+
486
+ | Level | Score | Meaning |
487
+ |---|---|---|
488
+ | none | 0 | No import rules are declared. |
489
+ | declared | 50 | A rules file declares at least one direction rule, but CI does not run it. |
490
+ | enforced | 100 | The rules are declared and CI runs them on pull requests. |
491
+
492
+ **How it is measured.**
493
+
494
+ - TypeScript / JavaScript: Rules file: .dependency-cruiser.{js,cjs,mjs,json}. Declared means at least one forbidden or allowed rule whose from and to paths name different directories. Enforced means a CI file invokes depcruise, directly or through a package script or make target (up to 3 levels).
495
+ - Python: Rules file: .importlinter, [importlinter] in setup.cfg, or [tool.importlinter] in pyproject.toml. Declared means at least one layers, forbidden or independence contract. Enforced means a CI file invokes lint-imports, directly or through a script or make target.
496
+ - Ci files: .github/workflows/*, .gitlab-ci.yml, .circleci/config.yml, bitbucket-pipelines.yml, azure-pipelines.yml.
497
+
498
+ **Why.** An architecture that exists only in people's heads is not visible to an agent and erodes with each change. A rules file states the architecture in a form a tool can check. Running it in CI is what stops new violations.
499
+
500
+ **Sources.**
501
+
502
+ - dependency-cruiser rules reference. https://github.com/sverweij/dependency-cruiser/blob/main/doc/rules-reference.md
503
+ - import-linter contract types. https://import-linter.readthedocs.io/en/stable/contract_types.html
504
+
505
+ #### 3.2 Boundary violations
506
+
507
+ Weight 30%. Static layer. Pooled as a ratio across languages and packages.
508
+
509
+ **Raw value** (violations per 1,000 imports). Import edges that break a rule, divided by all internal import edges / 1,000. Rules come from the repository's declared rules file, or from the preset when none is declared. Rules with severity error or warn count; info is ignored. Each violating edge counts once, even when it breaks several rules. Imports from test files are excluded. Type-only imports are included, except where a preset rule says otherwise.
510
+
511
+ | Raw | Score | Meaning |
512
+ |---|---|---|
513
+ | 0 | 100 | Every import respects the architecture. |
514
+ | 2 | 75 | A few known shortcuts. |
515
+ | 10 | 50 | The layers exist in name only in places. |
516
+ | 30 | 0 | Anything imports anything. |
517
+
518
+ Scores are interpolated between anchors and clamped outside them. If there is nothing to count, the score is 100.
519
+
520
+ **How it is measured.**
521
+
522
+ - TypeScript / JavaScript: dependency-cruiser with the declared rules or the preset converted to dependency-cruiser rules.
523
+ - Python: grimp builds the import graph; import-linter checks the declared contracts or the preset converted to contracts.
524
+
525
+ **Why.** Each violation is a dependency a reader would not expect from the directory structure. The count is normalized per 1,000 imports so repositories of different sizes are comparable.
526
+
527
+ **Sources.**
528
+
529
+ - Martin, R. C. Clean Architecture. Prentice Hall, 2017 (the Dependency Rule).
530
+ - Cockburn, A. Hexagonal Architecture (Ports and Adapters), 2005. https://alistair.cockburn.us/hexagonal-architecture/
531
+
532
+ #### 3.3 Import cycles
533
+
534
+ Weight 25%. Static layer. Pooled as a ratio across languages and packages.
535
+
536
+ **Raw value** (percent). Logic files that belong to an import cycle / all logic files. A cycle is a strongly connected component of two or more files in the graph of module-level static imports. Type-only imports, dynamic imports, imports inside functions and imports under "if TYPE_CHECKING:" are excluded.
537
+
538
+ | Raw | Score | Meaning |
539
+ |---|---|---|
540
+ | 0 | 100 | The import graph has no cycles. |
541
+ | 2 | 75 | One or two small cycles. |
542
+ | 5 | 50 | Cycles connect several areas. |
543
+ | 15 | 0 | Large parts of the code depend on each other. |
544
+
545
+ Scores are interpolated between anchors and clamped outside them. If there is nothing to count, the score is 100.
546
+
547
+ **How it is measured.**
548
+
549
+ - TypeScript / JavaScript: dependency-cruiser cycle detection on the filtered graph.
550
+ - Python: grimp import graph, filtered as defined, then strongly connected components.
551
+
552
+ **Why.** Files in a cycle cannot be understood, tested or changed one at a time. Cycles also cause import-order bugs at runtime, especially in Python. Type-only imports are excluded because they have no runtime effect.
553
+
554
+ **Sources.**
555
+
556
+ - Martin, R. C. Agile Software Development: Principles, Patterns, and Practices. Prentice Hall, 2002 (the Acyclic Dependencies Principle).
557
+
558
+ #### 3.4 Public-entry discipline
559
+
560
+ Weight 15%. Static layer. Pooled as a ratio across languages and packages.
561
+
562
+ **Raw value** (percent). Imports that cross from one module into another and land on that module's public entry / all imports that cross between modules. Modules are defined by the preset (or by the independence or layer contracts in the declared rules). The public entry is index.* in TypeScript and JavaScript, and __init__.py in Python.
563
+
564
+ | Raw | Score | Meaning |
565
+ |---|---|---|
566
+ | 50 | 0 | Other modules reach into internals as often as not. |
567
+ | 75 | 50 | Entry points exist and are often bypassed. |
568
+ | 90 | 75 | Entry points are mostly used. |
569
+ | 100 | 100 | Every cross-module import goes through the entry point. |
570
+
571
+ Scores are interpolated between anchors and clamped outside them. If there is nothing to count, the score is 100.
572
+
573
+ **How it is measured.**
574
+
575
+ - TypeScript / JavaScript: Import graph from dependency-cruiser; module roots from the preset.
576
+ - Python: grimp import graph; module roots from the preset. In Django apps, models, services and selectors are also public entries.
577
+
578
+ **Not applicable when.** The repository has fewer than two modules as defined by the preset or the declared rules. Splitting code into modules is a design choice the methodology does not score.
579
+
580
+ **Why.** A public entry point is a small interface the next person can read instead of the whole module. When other modules import internals directly, every internal file becomes part of the interface and cannot be changed safely.
581
+
582
+ **Sources.**
583
+
584
+ - HackSoft Django Styleguide. https://github.com/HackSoftware/Django-Styleguide
585
+
586
+ #### 3.5 Domain purity
587
+
588
+ Weight 15%. Static layer. Pooled as a ratio across languages and packages.
589
+
590
+ **Raw value** (percent). Files in the domain layer that import a framework, database, network or filesystem package, or an infrastructure directory / all files in the domain layer.
591
+
592
+ | Raw | Score | Meaning |
593
+ |---|---|---|
594
+ | 0 | 100 | Domain logic is plain code that runs and tests without infrastructure. |
595
+ | 5 | 75 | A few domain files touch infrastructure. |
596
+ | 15 | 50 | Domain logic regularly reaches for the database or the network. |
597
+ | 40 | 0 | The domain layer is a directory name. |
598
+
599
+ Scores are interpolated between anchors and clamped outside them. If there is nothing to count, the score is 100.
600
+
601
+ **How it is measured.**
602
+
603
+ - Domain layer: Files under a directory named domain, or files in a layer named domain in the declared rules.
604
+ - TypeScript / JavaScript: Forbidden imports: next, react, react-dom, express, @prisma/client, drizzle-orm, pg, mysql2, mongoose, redis, ioredis, axios, node-fetch, fs, node:fs, http, node:http, https, node:https. A call to the global fetch also counts.
605
+ - Python: Forbidden imports: django, fastapi, flask, starlette, sqlalchemy, requests, httpx, aiohttp, boto3, psycopg, psycopg2, redis, pymongo. A call to the builtin open also counts.
606
+ - Infrastructure directories: infrastructure, infra, db, adapters.
607
+
608
+ **Not applicable when.** The repository declares no domain layer. Choosing a layered or domain-driven design is a design choice the methodology does not score.
609
+
610
+ **Why.** This is the deterministic part of domain-driven design alignment. Pure domain code can be tested without a database or network and changed without reading infrastructure code. The methodology does not judge whether the domain model is good; it checks that the repository keeps the boundary it declared.
611
+
612
+ **Sources.**
613
+
614
+ - Evans, E. Domain-Driven Design. Addison-Wesley, 2003.
615
+ - Cockburn, A. Hexagonal Architecture (Ports and Adapters), 2005. https://alistair.cockburn.us/hexagonal-architecture/
616
+
617
+ ### D4. Complexity & maintainability
618
+
619
+ **Question.** How much does someone have to read to make one change correctly?
620
+
621
+ **Why it matters for agents.** Every line an agent reads uses context. Long files, complex functions, duplicated logic and dead code all increase how much it must read, and duplicated logic is where an agent fixes one copy and misses the other.
622
+
623
+ | # | Parameter | Layer | Weight |
624
+ |---|---|---|---|
625
+ | 4.1 | Complex functions | Static | 30% |
626
+ | 4.2 | Lines in large files | Static | 20% |
627
+ | 4.3 | Duplication | Static | 25% |
628
+ | 4.4 | Unused exports and files | Static | 25% |
629
+
630
+ #### 4.1 Complex functions
631
+
632
+ Weight 30%. Static layer. Pooled as a ratio across languages and packages.
633
+
634
+ **Raw value** (percent). Functions with cognitive complexity strictly greater than 15 / all functions in logic files, as enumerated by the tool.
635
+
636
+ | Raw | Score | Meaning |
637
+ |---|---|---|
638
+ | 0 | 100 | Every function can be read in one pass. |
639
+ | 3 | 75 | A few hard functions. |
640
+ | 8 | 50 | Hard functions in most areas. |
641
+ | 20 | 0 | Logic is concentrated in functions nobody wants to touch. |
642
+
643
+ Scores are interpolated between anchors and clamped outside them. If there is nothing to count, the score is 100.
644
+
645
+ **How it is measured.**
646
+
647
+ - TypeScript / JavaScript: eslint-plugin-sonarjs cognitive-complexity rule, threshold 15.
648
+ - Python: complexipy, threshold 15.
649
+
650
+ **Why.** Cognitive complexity counts nesting and breaks in linear flow, which is closer to how hard a function is to read than cyclomatic complexity. 15 is the default threshold in SonarQube, eslint-plugin-sonarjs and complexipy. Counting the share of functions over the threshold, instead of the mean, keeps many small functions from hiding a few large ones.
651
+
652
+ **Sources.**
653
+
654
+ - Campbell, G. A. Cognitive Complexity: A new way of measuring understandability. SonarSource, 2018. https://www.sonarsource.com/docs/CognitiveComplexity.pdf
655
+ - SonarQube rule S3776 (Cognitive Complexity of functions should not be too high), default threshold 15.
656
+ - McCabe, T. J. A Complexity Measure. IEEE Transactions on Software Engineering, 1976 (cyclomatic complexity, threshold 10).
657
+
658
+ #### 4.2 Lines in large files
659
+
660
+ Weight 20%. Static layer. Pooled as a ratio across languages and packages.
661
+
662
+ **Raw value** (percent). Logic lines in logic files with more than 500 logic lines / all logic lines.
663
+
664
+ | Raw | Score | Meaning |
665
+ |---|---|---|
666
+ | 0 | 100 | Every file fits in one read. |
667
+ | 10 | 75 | A few large files. |
668
+ | 25 | 50 | A quarter of the code lives in large files. |
669
+ | 50 | 0 | Most of the code lives in files too large to read whole. |
670
+
671
+ Scores are interpolated between anchors and clamped outside them. If there is nothing to count, the score is 100.
672
+
673
+ **How it is measured.**
674
+
675
+ - TypeScript / JavaScript: Logic line count per file (scope.yaml).
676
+ - Python: Logic line count per file (scope.yaml); docstrings do not count.
677
+
678
+ **Why.** A large file has to be read in parts, so changes to it are made with partial context. The threshold of 500 logic lines is more lenient than ESLint's max-lines default (300 physical lines) and stricter than Pylint's max-module-lines default (1,000). The raw value is a share of lines, not of files, so one 5,000-line file weighs more than one 501-line file.
679
+
680
+ **Sources.**
681
+
682
+ - ESLint max-lines rule, default 300. https://eslint.org/docs/latest/rules/max-lines
683
+ - Pylint too-many-lines (C0302), max-module-lines default 1000.
684
+
685
+ #### 4.3 Duplication
686
+
687
+ Weight 25%. Static layer. Pooled as a ratio across languages and packages.
688
+
689
+ **Raw value** (percent). Duplicated lines / all lines in logic files, as reported by jscpd.
690
+
691
+ | Raw | Score | Meaning |
692
+ |---|---|---|
693
+ | 3 | 100 | Duplication is incidental. |
694
+ | 5 | 75 | Some copied blocks. |
695
+ | 10 | 50 | Copy and paste is a common way to add features. |
696
+ | 20 | 0 | Large parts of the logic exist in several copies. |
697
+
698
+ Scores are interpolated between anchors and clamped outside them. If there is nothing to count, the score is 100.
699
+
700
+ **How it is measured.**
701
+
702
+ - TypeScript / JavaScript: jscpd, min-tokens 50, min-lines 5, logic files only.
703
+ - Python: jscpd, same settings.
704
+
705
+ **Why.** A bug fixed in one copy stays in the others. This is a common agent mistake: the agent finds one copy, fixes it, and its tests pass. 3% is the default duplication limit in SonarQube's quality gate.
706
+
707
+ **Sources.**
708
+
709
+ - SonarQube default quality gate (Sonar way): duplicated lines on new code at most 3%.
710
+ - jscpd. https://github.com/kucherenko/jscpd
711
+
712
+ #### 4.4 Unused exports and files
713
+
714
+ Weight 25%. Static layer. Pooled as a ratio across languages and packages.
715
+
716
+ **Raw value** (percent). (Unused exported symbols + exported symbols in unused files) / all exported symbols in logic files. Exported types count. Library entry points (package.json exports and main, Python __all__ of a distributed package) count as used.
717
+
718
+ | Raw | Score | Meaning |
719
+ |---|---|---|
720
+ | 0 | 100 | Everything exported is used. |
721
+ | 3 | 75 | A little dead code. |
722
+ | 8 | 50 | Dead code is common. |
723
+ | 20 | 0 | A large share of the code is never called. |
724
+
725
+ Scores are interpolated between anchors and clamped outside them. If there is nothing to count, the score is 100.
726
+
727
+ **How it is measured.**
728
+
729
+ - TypeScript / JavaScript: knip (unused files, exports and types), with framework plugins enabled.
730
+ - Python: vulture with min-confidence 60 for unused functions, classes and methods; decorated definitions are treated as used. A method is also treated as used when its class, directly or through base classes defined in the repository, has a base class defined outside the repository (standard library or third party), or when a base class defined in the repository has a method of the same name; object and Generic do not count as bases. Unused modules: modules no other module imports, excluding entry modules (__main__, manage.py, wsgi, asgi, settings, conftest, targets of [project.scripts], Django convention modules such as models, admin, apps, urls, views, and FastAPI main.py).
731
+
732
+ **Why.** Dead code costs reading time and looks like a place to make a change. An agent that edits an unused function gets no test failure and no effect. Decorated Python definitions are treated as used because frameworks register them by decorator (routes, fixtures, signals). Methods that override a base class are treated as used for the same reason: frameworks and the standard library call hooks such as do_GET, setUp or emit by name, which vulture cannot see.
733
+
734
+ **Sources.**
735
+
736
+ - knip. https://knip.dev
737
+ - vulture. https://github.com/jendrikseipp/vulture
738
+
739
+ ### D5. Dependency & security hygiene
740
+
741
+ **Question.** Is the code built on parts that are known, current and safe?
742
+
743
+ **Why it matters for agents.** An agent adds dependencies and copies patterns from what it finds. An undeclared dependency, a stale major version or a committed secret gets copied into new code the same way good patterns do.
744
+
745
+ | # | Parameter | Layer | Weight |
746
+ |---|---|---|---|
747
+ | 5.1 | Known vulnerabilities in production dependencies | Static | 30% |
748
+ | 5.2 | Dependencies a major version behind | Static | 20% |
749
+ | 5.3 | Lockfile | Static | 15% |
750
+ | 5.4 | Secrets in the working tree | Static | 20% |
751
+ | 5.5 | Dependency declaration errors | Static | 15% |
752
+
753
+ **As of date.** Parameters 5.1 and 5.2 are evaluated at the run's as-of date: advisories published on or before that date count, and the latest version of a package is the latest published on or before that date.
754
+
755
+ #### 5.1 Known vulnerabilities in production dependencies
756
+
757
+ Weight 30%. Static layer. Pooled as a ratio across languages and packages.
758
+
759
+ **Raw value** (severity points). Sum of severity points over advisories affecting resolved production dependencies (direct and transitive). Critical 10, high 5, moderate 2, low 0.5. Each advisory counts once per package; aliases are deduplicated. Withdrawn advisories are excluded.
760
+
761
+ | Raw | Score | Meaning |
762
+ |---|---|---|
763
+ | 0 | 100 | No known vulnerabilities. |
764
+ | 2 | 75 | One moderate, or a few low. |
765
+ | 10 | 50 | A critical, or several highs. |
766
+ | 30 | 0 | Several criticals. Nobody is watching. |
767
+
768
+ Scores are interpolated between anchors and clamped outside them. If there is nothing to count, the score is 100.
769
+
770
+ **How it is measured.**
771
+
772
+ - TypeScript / JavaScript: osv-scanner on the lockfile, against the OSV snapshot for the as-of date. Production dependencies only.
773
+ - Python: osv-scanner on the lockfile, against the OSV snapshot for the as-of date. Production dependencies only.
774
+ - Severity: The GitHub Security Advisory severity label when present. Otherwise the CVSS v3.1 base score band (critical 9.0-10.0, high 7.0-8.9, moderate 4.0-6.9, low 0.1-3.9). Otherwise moderate.
775
+
776
+ **Why.** Severity points stop one critical advisory from scoring the same as one low one. The count is not normalized by repository size, because a vulnerability's risk does not shrink as the codebase grows. Development-only dependencies are reported separately and not scored.
777
+
778
+ **Sources.**
779
+
780
+ - OSV schema (published, withdrawn, aliases). https://ossf.github.io/osv-schema/
781
+ - FIRST. CVSS v3.1 Specification, qualitative severity rating scale. https://www.first.org/cvss/v3.1/specification-document
782
+
783
+ #### 5.2 Dependencies a major version behind
784
+
785
+ Weight 20%. Static layer. Pooled as a ratio across languages and packages.
786
+
787
+ **Raw value** (percent). Direct dependencies (production and development) whose resolved version is at least one major version behind the highest stable, non-yanked version published on or before the as-of date / all direct dependencies. For 0.x versions, a minor version bump counts as major.
788
+
789
+ | Raw | Score | Meaning |
790
+ |---|---|---|
791
+ | 0 | 100 | Every direct dependency is on its current major. |
792
+ | 10 | 75 | A few dependencies are behind. |
793
+ | 25 | 50 | A quarter of dependencies are behind. |
794
+ | 50 | 0 | Upgrades have stopped. |
795
+
796
+ Scores are interpolated between anchors and clamped outside them. If there is nothing to count, the score is 100.
797
+
798
+ **How it is measured.**
799
+
800
+ - TypeScript / JavaScript: npm registry publish times from the registry snapshot.
801
+ - Python: PyPI release upload times from the registry snapshot.
802
+
803
+ **Why.** Each major version behind is a migration someone will have to do, and an agent writing code against current documentation will use APIs the installed version does not have. Development dependencies are included because outdated tooling affects every change.
804
+
805
+ **Sources.**
806
+
807
+ - Semantic Versioning 2.0.0, item 4 (major version zero). https://semver.org
808
+
809
+ #### 5.3 Lockfile
810
+
811
+ Weight 15%. Static layer. Scored per language and package, then combined by logic-line weight.
812
+
813
+ **Raw value** (level). Whether a lockfile exists and matches the manifest.
814
+
815
+ | Level | Score | Meaning |
816
+ |---|---|---|
817
+ | none | 0 | No lockfile. Every install can resolve different versions. |
818
+ | out_of_sync | 40 | A lockfile exists but does not match the manifest. |
819
+ | in_sync | 100 | The lockfile matches the manifest. |
820
+
821
+ **How it is measured.**
822
+
823
+ - TypeScript / JavaScript: package-lock.json, npm-shrinkwrap.json, pnpm-lock.yaml, yarn.lock or bun.lock. In sync means every dependency declared in package.json is present in the lockfile with a version that satisfies its range.
824
+ - Python: uv.lock, poetry.lock, pdm.lock, Pipfile.lock, or requirements files with every requirement pinned with == and --hash. In sync means every dependency declared in pyproject.toml (or Pipfile) is in the lockfile with a version that satisfies its specifier, or for uv.lock and poetry.lock, the lockfile's recorded manifest hash matches.
825
+
826
+ **Why.** Without a lockfile, two installs of the same commit can produce different code. Results stop being reproducible, including this methodology's results.
827
+
828
+ **Sources.**
829
+
830
+ - npm ci documentation. https://docs.npmjs.com/cli/commands/npm-ci
831
+
832
+ #### 5.4 Secrets in the working tree
833
+
834
+ Weight 20%. Static layer. Scored per language and package, then combined by logic-line weight.
835
+
836
+ **Raw value** (count). Secrets found by the pinned gitleaks default ruleset in every tracked file at the scanned commit. .healthignore does not apply. .gitleaksignore applies and is part of the ignore set.
837
+
838
+ | Raw | Score | Meaning |
839
+ |---|---|---|
840
+ | 0 | 100 | No secrets in the code. |
841
+ | 1 | 0 | At least one secret is in the code. |
842
+
843
+ Scores are interpolated between anchors and clamped outside them. If there is nothing to count, the score is 100.
844
+
845
+ **How it is measured.**
846
+
847
+ - All languages: gitleaks dir on the checkout, default ruleset of the pinned version.
848
+
849
+ **Why.** One committed secret is enough to cause a breach, so the parameter is binary. Secrets in git history are reported but not scored, because the fix is rotating the key, which cannot be seen in the code.
850
+
851
+ **Sources.**
852
+
853
+ - CWE-798: Use of Hard-coded Credentials. https://cwe.mitre.org/data/definitions/798.html
854
+ - gitleaks. https://github.com/gitleaks/gitleaks
855
+
856
+ #### 5.5 Dependency declaration errors
857
+
858
+ Weight 15%. Static layer. Pooled as a ratio across languages and packages.
859
+
860
+ **Raw value** (percent). (Declared dependencies that are never imported + imported packages that are not declared) / declared direct dependencies.
861
+
862
+ | Raw | Score | Meaning |
863
+ |---|---|---|
864
+ | 0 | 100 | The manifest matches the code. |
865
+ | 5 | 75 | A few stale or missing entries. |
866
+ | 15 | 50 | The manifest and the code have drifted apart. |
867
+ | 30 | 0 | Nobody maintains the manifest. |
868
+
869
+ Scores are interpolated between anchors and clamped outside them. If there is nothing to count, the score is 100.
870
+
871
+ **How it is measured.**
872
+
873
+ - TypeScript / JavaScript: knip unused dependencies plus unlisted dependencies.
874
+ - Python: deptry DEP001 (missing), DEP002 (unused) and DEP003 (transitive imported directly). DEP004 (development dependency used in production code) is not counted.
875
+
876
+ **Why.** An undeclared import works until the transitive dependency that provides it changes. An unused declaration adds install time and attack surface. Both mislead a reader about what the code depends on.
877
+
878
+ **Sources.**
879
+
880
+ - deptry rules. https://deptry.com/rules-violations/
881
+
882
+ ### D6. Operability & feedback loop
883
+
884
+ **Question.** Can someone new set it up, check a change and know they are done?
885
+
886
+ **Why it matters for agents.** An agent needs a command that tells it whether its change is correct, and it needs to find that command without asking. A slow or missing check means the agent stops early or guesses.
887
+
888
+ | # | Parameter | Layer | Weight |
889
+ |---|---|---|---|
890
+ | 6.1 | Clean install and build | Execution | 25% |
891
+ | 6.2 | One-command verify | Static | 15% |
892
+ | 6.3 | Feedback loop time | Execution | 10% |
893
+ | 6.4 | CI runs the checks on pull requests | Static | 20% |
894
+ | 6.5 | Reproducible environment | Static | 15% |
895
+ | 6.6 | Commands documented for agents | Static | 15% |
896
+
897
+ **Script resolution.** Parameters 6.2, 6.4 and 6.6 follow package.json scripts, Makefile targets, justfile recipes, tox and nox sessions, and pyproject.toml script tables up to 3 levels deep. Recognised tools: typecheckers tsc, vue-tsc, pyright, basedpyright, mypy; linters eslint, biome, oxlint, ruff, flake8, pylint; test runners vitest, jest, mocha, node --test, pytest, python -m unittest, tox, nox. The prefixes npx, bunx, pnpm exec, yarn, uv run, poetry run, pdm run and python -m are removed before a command is matched.
898
+
899
+ #### 6.1 Clean install and build
900
+
901
+ Weight 25%. Execution layer. Scored per language and package, then combined by logic-line weight.
902
+
903
+ **Raw value** (level). Result of a clean install from the lockfile, followed by the build step, in the pinned runner.
904
+
905
+ | Level | Score | Meaning |
906
+ |---|---|---|
907
+ | install_fails | 0 | The project does not install from a clean checkout. |
908
+ | build_fails | 40 | It installs, but the build fails. |
909
+ | works | 100 | It installs and builds from a clean checkout. A project with no build step scores 100 when the install succeeds. |
910
+
911
+ **How it is measured.**
912
+
913
+ - TypeScript / JavaScript: Install: npm ci, pnpm install --frozen-lockfile, yarn install --immutable or bun install --frozen-lockfile, chosen by lockfile; npm install when there is no lockfile. Build: the build script in package.json, when one exists.
914
+ - Python: Install: uv sync --frozen, poetry install --sync, pdm sync, pipenv sync, or pip install -r on the requirements files, chosen by lockfile; pip install . when there is only a pyproject.toml. Build: python manage.py check for Django projects; otherwise importing each first-party top-level package.
915
+
916
+ **Why.** Everything in the execution layer depends on this step. A project that does not install from a clean checkout depends on someone's machine.
917
+
918
+ #### 6.2 One-command verify
919
+
920
+ Weight 15%. Static layer. Scored per language and package, then combined by logic-line weight.
921
+
922
+ **Raw value** (count of checks (0-3)). The largest number of the three checks (typecheck, lint, tests) that a single entry point runs: one package.json script, Makefile target, justfile recipe, tox or nox session, or pyproject script.
923
+
924
+ | Level | Score | Meaning |
925
+ |---|---|---|
926
+ | 0 | 0 | No single command runs any check. |
927
+ | 1 | 33 | One command runs one check. |
928
+ | 2 | 66 | One command runs two checks. |
929
+ | 3 | 100 | One command runs typecheck, lint and tests. |
930
+
931
+ **How it is measured.**
932
+
933
+ - All languages: Static resolution of entry points as described in script_resolution.
934
+
935
+ **Why.** "Run this and it tells you if you are done" is the most useful fact an agent can learn about a repository. Without it, the agent has to find and run each check separately, and often skips one.
936
+
937
+ #### 6.3 Feedback loop time
938
+
939
+ Weight 10%. Execution layer. Scores 0 if any of these steps fail: install, typecheck run, lint run, test run. Scored per language and package, then combined by logic-line weight.
940
+
941
+ **Raw value** (seconds). Summed wall-clock time of the methodology's own typecheck (2.1), lint (2.5) and test (1.1) runs on the pinned runner.
942
+
943
+ | Raw | Score | Meaning |
944
+ |---|---|---|
945
+ | 60 | 100 | A full check takes about a minute. |
946
+ | 180 | 75 | A full check takes a few minutes. |
947
+ | 600 | 50 | The ten-minute build. |
948
+ | 1200 | 0 | Nobody runs the full check locally. |
949
+
950
+ Scores are interpolated between anchors and clamped outside them. If there is nothing to count, the score is 0.
951
+
952
+ **How it is measured.**
953
+
954
+ - All languages: Wall-clock time recorded by the orchestrator for the three steps.
955
+
956
+ **Why.** A slow check gets run less often, by people and by agents. This is the only parameter whose raw value varies between identical runs, so its weight is low and the runner hardware is fixed.
957
+
958
+ **Sources.**
959
+
960
+ - Beck, K. Extreme Programming Explained, 2nd ed. Addison-Wesley, 2004 (Ten-Minute Build).
961
+ - Fowler, M. Continuous Integration. https://martinfowler.com/articles/continuousIntegration.html
962
+
963
+ #### 6.4 CI runs the checks on pull requests
964
+
965
+ Weight 20%. Static layer. Scored per language and package, then combined by logic-line weight.
966
+
967
+ **Raw value** (count of checks (0-3)). How many of the three checks (typecheck, lint, tests) CI runs on pull requests or on every push. Checks may be spread across jobs and workflows.
968
+
969
+ | Level | Score | Meaning |
970
+ |---|---|---|
971
+ | 0 | 0 | CI runs no checks, or there is no CI. |
972
+ | 1 | 33 | CI runs one check. |
973
+ | 2 | 66 | CI runs two checks. |
974
+ | 3 | 100 | CI runs typecheck, lint and tests on every pull request. |
975
+
976
+ **How it is measured.**
977
+
978
+ - All languages: CI files as listed in 3.1. A workflow counts if it is triggered on pull_request, pull_request_target, merge_request events, or on push without a branch filter that limits it to the default branch. Commands are resolved as described in script_resolution.
979
+
980
+ **Why.** Local checks are optional; CI checks are not. CI is what keeps the score from dropping after the audit ends.
981
+
982
+ **Sources.**
983
+
984
+ - Fowler, M. Continuous Integration. https://martinfowler.com/articles/continuousIntegration.html
985
+
986
+ #### 6.5 Reproducible environment
987
+
988
+ Weight 15%. Static layer. Scored per language and package, then combined by logic-line weight.
989
+
990
+ **Raw value** (score). Mean of two components.
991
+
992
+ The score is the mean of two components, each 0-100:
993
+
994
+ - **Runtime version pinned**: 100 if pinned, 0 if not. Counts: Any of .nvmrc, .node-version, .tool-versions, mise.toml, engines.node in package.json, volta in package.json, .python-version, requires-python in pyproject.toml, or runtime.txt.
995
+ - **Environment variables documented**: Variable names read in logic files that appear in an env example file / all variable names read in logic files, times 100. No variables read scores 100. Example files: .env.example, .env.sample, .env.template, .env.dist, example.env. Reads: process.env.X, process.env["X"], destructuring from process.env, import.meta.env.X, os.environ["X"], os.environ.get("X"), os.getenv("X"). Excluded names: NODE_ENV, CI, HOME, PATH, PWD, TZ, NEXT_RUNTIME, and names starting with VERCEL_, NETLIFY, RENDER_, RAILWAY_ or GITHUB_.
996
+
997
+ **How it is measured.**
998
+
999
+ - All languages: Static reads of the files and patterns listed in the components.
1000
+
1001
+ **Why.** A missing environment variable is a common reason a fresh checkout does not start, and an agent cannot guess the names. Config read from the environment, with the names documented, is the Twelve-Factor approach.
1002
+
1003
+ **Sources.**
1004
+
1005
+ - The Twelve-Factor App, III. Config. https://12factor.net/config
1006
+
1007
+ #### 6.6 Commands documented for agents
1008
+
1009
+ Weight 15%. Static layer. Scored per language and package, then combined by logic-line weight.
1010
+
1011
+ **Raw value** (level). Whether the repository documents how to install, run and check it, and whether the documented commands exist.
1012
+
1013
+ | Level | Score | Meaning |
1014
+ |---|---|---|
1015
+ | none | 0 | No documented commands. |
1016
+ | readme | 50 | README documents commands, or an agent instructions file exists but at least one command in it does not resolve. |
1017
+ | agent_file | 100 | AGENTS.md or CLAUDE.md at the repository root documents commands, and every command in it resolves. |
1018
+
1019
+ **How it is measured.**
1020
+
1021
+ - All languages: A command is an inline code span or a line in a fenced code block that starts with a package manager (npm, pnpm, yarn, bun, npx, uv, poetry, pdm, pip, pipenv, python, make, just) or a recognised tool. It resolves when the script, target, recipe, session or tool it names exists in the repository or in its declared dependencies.
1022
+
1023
+ **Why.** An agent reads AGENTS.md or CLAUDE.md before anything else. Commands in that file that do not exist send it down the wrong path, which is worse than no file, so they drop the level to README.
1024
+
1025
+ **Sources.**
1026
+
1027
+ - AGENTS.md. https://agents.md
1028
+
1029
+ ## 7. Report-only signals
1030
+
1031
+ These appear in the report and never change the score.
1032
+
1033
+ | Signal | What it shows | Why it is not scored |
1034
+ |---|---|---|
1035
+ | Hotspots | Files ranked by churn (commits in the last 12 months) × cognitive complexity | The "fix these first" list. Churn describes how the team works, not the code's state, and would make two runs of the same tree differ. |
1036
+ | Secrets in git history | Secrets in past commits | The fix is rotating the key, which the code cannot show. |
1037
+ | Development dependency vulnerabilities | Advisories in packages reachable only from development dependencies | They do not ship to production. |
1038
+ | Top scares | The first three of a fixed list of consequences the findings trigger, such as a Supabase service-role key in the tree, secrets in the tree or history, known production advisories, or an install that fails | A consequence in plain words, not a measurement. The scan cannot see access rules such as row-level security. |
1039
+ | Excluded lines | Percentage of lines excluded by `.healthignore` | Makes exclusions visible to the reader. |
1040
+ | Per-package breakdown | Every dimension per workspace package and per language | Shows where the pooled score comes from. |
1041
+
1042
+ ## 8. What a 100 looks like
1043
+
1044
+ Concrete descriptions of a repository that scores 100 on every parameter.
1045
+
1046
+ ### TypeScript
1047
+
1048
+ - **Tests.** vitest with `coverage.all`, at least 80% line and 70% branch coverage. Every module has a test file next to it. Every test asserts something. The suite is green.
1049
+ - **Types.** `tsconfig.json` with `"strict": true` and `"noUncheckedIndexedAccess": true`. `tsc --noEmit` reports 0 errors. No `any`, no `as` casts other than `as const`, no `!` non-null assertions, no `@ts-ignore`. Every exported function has typed parameters.
1050
+ - **Lint.** 0 findings from the pinned correctness rules, including `no-floating-promises` and `no-misused-promises`.
1051
+ - **Architecture.** A `.dependency-cruiser.cjs` file with direction rules, run by `depcruise` in CI. No import cycles. Each feature folder has an `index.ts`, and other features import only from it. If there is a `domain/` folder, it imports no framework, database or network packages.
1052
+ - **Size.** No function above cognitive complexity 15. No file above 500 logic lines. Under 3% duplication. `knip` reports no unused exports, files or dependencies.
1053
+ - **Dependencies.** A lockfile in sync with `package.json`. No known vulnerabilities in production dependencies. Every direct dependency on its current major version. No secrets in tracked files.
1054
+ - **Operability.** `npm ci && npm run build` works on a clean checkout. One script runs everything:
1055
+
1056
+ ```json
1057
+ "scripts": {
1058
+ "typecheck": "tsc --noEmit",
1059
+ "lint": "eslint .",
1060
+ "test": "vitest run --coverage",
1061
+ "check": "npm run typecheck && npm run lint && npm run test"
1062
+ }
1063
+ ```
1064
+
1065
+ `npm run check` takes under a minute. CI runs it on every pull request. `.nvmrc` pins Node. Every `process.env.X` the code reads is listed in `.env.example`. `AGENTS.md` lists the commands, and every command in it exists.
1066
+
1067
+ ### Python
1068
+
1069
+ A Python project at 100 uses a small set of modern tools. Most of them are configured in `pyproject.toml`.
1070
+
1071
+ - **Project and lockfile.** `uv` manages the environment. `uv.lock` is committed. `.python-version` pins the interpreter, and `requires-python` is set.
1072
+ - **Tests.** pytest with pytest-cov, run with `--cov=<package> --cov-branch` so files no test imports are counted. At least 80% line and 70% branch coverage. Every module has a `test_<module>.py`. Every test has an `assert` or uses `pytest.raises`.
1073
+ - **Types.** pyright in strict mode (or mypy with `strict = true`), with 0 errors. Every public function annotates its parameters and its return type. No `Any`, no `cast()`, no `# type: ignore`. Python does not enforce annotations at runtime; only the typechecker reads them.
1074
+ - **Lint.** ruff reports 0 findings for rule sets F (Pyflakes: undefined names, unused imports), B (bugbear: likely bugs such as mutable default arguments) and E9 (syntax errors).
1075
+ - **Architecture.** import-linter contracts declare the layers, and CI runs `lint-imports`. No import cycles (Python cycles also cause import-order errors at runtime). Code that is only needed for type annotations is imported under `if TYPE_CHECKING:`. A `domain/` package imports no framework, ORM or HTTP client.
1076
+ - **Size.** No function above cognitive complexity 15 (complexipy). No module above 500 logic lines. Under 3% duplication. vulture finds no unused code, and deptry finds no unused, missing or transitive-only dependencies.
1077
+ - **Operability.** `uv sync --frozen` works on a clean checkout, and the package imports. One command runs all checks, CI runs it on pull requests, and `AGENTS.md` lists it.
1078
+
1079
+ Example `pyproject.toml`:
1080
+
1081
+ ```toml
1082
+ [project]
1083
+ name = "billing"
1084
+ requires-python = ">=3.12"
1085
+ dependencies = ["fastapi>=0.115", "sqlalchemy>=2.0"]
1086
+
1087
+ [dependency-groups]
1088
+ dev = ["pytest>=8", "pytest-cov>=5", "pyright>=1.1.380", "ruff>=0.6", "import-linter>=2"]
1089
+
1090
+ [tool.pyright]
1091
+ typeCheckingMode = "strict"
1092
+
1093
+ [tool.ruff.lint]
1094
+ select = ["E", "F", "B", "I", "UP"]
1095
+
1096
+ [tool.pytest.ini_options]
1097
+ addopts = "--cov=billing --cov-branch"
1098
+
1099
+ [tool.importlinter]
1100
+ root_package = "billing"
1101
+
1102
+ [[tool.importlinter.contracts]]
1103
+ name = "Layers"
1104
+ type = "layers"
1105
+ layers = ["billing.api", "billing.services", "billing.domain"]
1106
+ ```
1107
+
1108
+ Example `Makefile` target:
1109
+
1110
+ ```make
1111
+ check:
1112
+ uv run pyright
1113
+ uv run ruff check
1114
+ uv run pytest
1115
+ ```
1116
+
1117
+ ## 9. Known limits of v1.0
1118
+
1119
+ 1. **Anchors are not calibrated against a corpus.** They come from the cited sources and from judgement. The next step is to run v1.0 on a set of repositories and check that the bands match how those repositories feel to work in. Any anchor change will be a new version.
1120
+ 2. **Test quality is measured only by assertions.** A test can assert something trivial. Mutation testing would measure this directly but is too slow for every run, so it is out of v1.0.
1121
+ 3. **Tests that need external services fail.** No database, queue or browser is provided. Integration tests that need one count as failed in 1.1, and the code they cover counts as uncovered.
1122
+ 4. **Declared import rules replace the preset.** A repository can declare a permissive rules file and avoid the preset's rules. 3.1 still rewards the declaration with 50 or 100.
1123
+ 5. **Domain-driven design is checked only at the boundary.** 3.5 checks that domain code does not import infrastructure. It does not judge whether the domain model is good.
1124
+ 6. **Static analysis misses dynamic behaviour.** Dependency injection, plugin registration and string-based imports can make used code look unused (4.4) or hide imports (D3).
1125
+ 7. **Security covers known advisories and committed secrets only.** There is no static application security testing (SAST).
1126
+ 8. **6.3 varies between runs.** It is the only parameter that does, and it has a weight of 10%.
1127
+ 9. **Other languages are ignored.** Files in other languages do not count toward any parameter.
1128
+
1129
+ ## 10. Versioning
1130
+
1131
+ - The methodology version is in `methodology.yaml` and in every `run.json`.
1132
+ - Any change that can change a score creates a new version: a weight, an anchor, a band, a threshold, a rule, a preset, or the scope. Wording changes that cannot change a score do not.
1133
+ - Runs from different methodology versions are never compared.
1134
+ - Tool upgrades produce a new image digest. Runs with different digests are not compared either.
1135
+
1136
+ ## 11. Sources
1137
+
1138
+ - AGENTS.md. https://agents.md
1139
+ - Arguelles, Ivanković, Bender. Code Coverage Best Practices. Google Testing Blog, 2020. https://testing.googleblog.com/2020/08/code-coverage-best-practices.html
1140
+ - Beck, K. Extreme Programming Explained, 2nd ed. Addison-Wesley, 2004 (Ten-Minute Build).
1141
+ - Campbell, G. A. Cognitive Complexity: A new way of measuring understandability. SonarSource, 2018. https://www.sonarsource.com/docs/CognitiveComplexity.pdf
1142
+ - Cockburn, A. Hexagonal Architecture (Ports and Adapters), 2005. https://alistair.cockburn.us/hexagonal-architecture/
1143
+ - CWE-798: Use of Hard-coded Credentials. https://cwe.mitre.org/data/definitions/798.html
1144
+ - dependency-cruiser rules reference. https://github.com/sverweij/dependency-cruiser/blob/main/doc/rules-reference.md
1145
+ - deptry rules. https://deptry.com/rules-violations/
1146
+ - ESLint max-lines rule, default 300. https://eslint.org/docs/latest/rules/max-lines
1147
+ - Evans, E. Domain-Driven Design. Addison-Wesley, 2003.
1148
+ - FastAPI: Bigger Applications - Multiple Files. https://fastapi.tiangolo.com/tutorial/bigger-applications/
1149
+ - FIRST. CVSS v3.1 Specification, qualitative severity rating scale. https://www.first.org/cvss/v3.1/specification-document
1150
+ - Fowler, M. Continuous Integration. https://martinfowler.com/articles/continuousIntegration.html
1151
+ - gitleaks. https://github.com/gitleaks/gitleaks
1152
+ - HackSoft Django Styleguide. https://github.com/HackSoftware/Django-Styleguide
1153
+ - import-linter contract types. https://import-linter.readthedocs.io/en/stable/contract_types.html
1154
+ - jscpd. https://github.com/kucherenko/jscpd
1155
+ - knip. https://knip.dev
1156
+ - Martin, R. C. Agile Software Development: Principles, Patterns, and Practices. Prentice Hall, 2002 (the Acyclic Dependencies Principle).
1157
+ - Martin, R. C. Clean Architecture. Prentice Hall, 2017 (the Dependency Rule).
1158
+ - McCabe, T. J. A Complexity Measure. IEEE Transactions on Software Engineering, 1976 (cyclomatic complexity, threshold 10).
1159
+ - mypy command line: --strict. https://mypy.readthedocs.io/en/stable/command_line.html
1160
+ - 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
1161
+ - npm ci documentation. https://docs.npmjs.com/cli/commands/npm-ci
1162
+ - OSV schema (published, withdrawn, aliases). https://ossf.github.io/osv-schema/
1163
+ - PEP 484: Type Hints. https://peps.python.org/pep-0484/
1164
+ - Pylint too-many-lines (C0302), max-module-lines default 1000.
1165
+ - Pyright configuration: typeCheckingMode. https://microsoft.github.io/pyright/#/configuration
1166
+ - Ruff rules. https://docs.astral.sh/ruff/rules/
1167
+ - Semantic Versioning 2.0.0, item 4 (major version zero). https://semver.org
1168
+ - SonarQube default quality gate (Sonar way): coverage on new code at least 80%.
1169
+ - SonarQube default quality gate (Sonar way): duplicated lines on new code at most 3%.
1170
+ - SonarQube rule S3776 (Cognitive Complexity of functions should not be too high), default threshold 15.
1171
+ - The Twelve-Factor App, III. Config. https://12factor.net/config
1172
+ - TypeScript TSConfig reference: noUncheckedIndexedAccess. https://www.typescriptlang.org/tsconfig#noUncheckedIndexedAccess
1173
+ - TypeScript TSConfig reference: strict. https://www.typescriptlang.org/tsconfig#strict
1174
+ - typescript-eslint rules. https://typescript-eslint.io/rules/
1175
+ - TypeScript: Type checking JavaScript files (checkJs). https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html
1176
+ - vulture. https://github.com/jendrikseipp/vulture