oxn 0.1.0__tar.gz

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 (300) hide show
  1. oxn-0.1.0/.gitignore +50 -0
  2. oxn-0.1.0/LICENSE +21 -0
  3. oxn-0.1.0/PKG-INFO +498 -0
  4. oxn-0.1.0/README.md +455 -0
  5. oxn-0.1.0/benchmarks/ceiling-observations.json +2112 -0
  6. oxn-0.1.0/benchmarks/lock.json +19 -0
  7. oxn-0.1.0/benchmarks/manifest.yaml +226 -0
  8. oxn-0.1.0/benchmarks/retrieval-corpus-oxn.json +11 -0
  9. oxn-0.1.0/benchmarks/retrieval-labels-external.json +869 -0
  10. oxn-0.1.0/benchmarks/retrieval-labels-oxn.json +427 -0
  11. oxn-0.1.0/docs/adr/0001-dependency-policy.md +152 -0
  12. oxn-0.1.0/docs/adr/0002-resolution-strategy.md +927 -0
  13. oxn-0.1.0/docs/adr/0003-enforcement-model.md +149 -0
  14. oxn-0.1.0/docs/adr/0004-freshness-model.md +267 -0
  15. oxn-0.1.0/docs/adr/0005-rule-engine.md +531 -0
  16. oxn-0.1.0/docs/adr/0006-retrieval-and-budgeting.md +366 -0
  17. oxn-0.1.0/docs/divergences.md +705 -0
  18. oxn-0.1.0/docs/metrics.md +1758 -0
  19. oxn-0.1.0/docs/proposals/runtime-evidence.md +181 -0
  20. oxn-0.1.0/pyproject.toml +108 -0
  21. oxn-0.1.0/scripts/actor.py +414 -0
  22. oxn-0.1.0/scripts/actors.py +259 -0
  23. oxn-0.1.0/scripts/arms.py +101 -0
  24. oxn-0.1.0/scripts/attempt.py +344 -0
  25. oxn-0.1.0/scripts/beds.py +430 -0
  26. oxn-0.1.0/scripts/check.py +347 -0
  27. oxn-0.1.0/scripts/console.py +28 -0
  28. oxn-0.1.0/scripts/discovered_modules.py +159 -0
  29. oxn-0.1.0/scripts/dogfood.py +387 -0
  30. oxn-0.1.0/scripts/experiment.py +145 -0
  31. oxn-0.1.0/scripts/extract_citation_labels.py +198 -0
  32. oxn-0.1.0/scripts/extract_retrieval_labels.py +123 -0
  33. oxn-0.1.0/scripts/fetch_corpora.py +160 -0
  34. oxn-0.1.0/scripts/freeze_retrieval_corpus.py +109 -0
  35. oxn-0.1.0/scripts/gauntlet.py +527 -0
  36. oxn-0.1.0/scripts/measure_ceilings.py +521 -0
  37. oxn-0.1.0/scripts/measure_cycles.py +115 -0
  38. oxn-0.1.0/scripts/measure_diagnostics.py +79 -0
  39. oxn-0.1.0/scripts/measure_duplication.py +220 -0
  40. oxn-0.1.0/scripts/measure_parse_errors.py +170 -0
  41. oxn-0.1.0/scripts/measure_redundancy.py +101 -0
  42. oxn-0.1.0/scripts/measure_resolution.py +139 -0
  43. oxn-0.1.0/scripts/measure_semantic.py +300 -0
  44. oxn-0.1.0/scripts/runlog.py +338 -0
  45. oxn-0.1.0/scripts/summary.py +166 -0
  46. oxn-0.1.0/scripts/targets.py +161 -0
  47. oxn-0.1.0/src/oxn/__init__.py +10 -0
  48. oxn-0.1.0/src/oxn/__main__.py +16 -0
  49. oxn-0.1.0/src/oxn/_app.py +470 -0
  50. oxn-0.1.0/src/oxn/calibration.py +603 -0
  51. oxn-0.1.0/src/oxn/calls.py +282 -0
  52. oxn-0.1.0/src/oxn/check.py +770 -0
  53. oxn-0.1.0/src/oxn/cli.py +256 -0
  54. oxn-0.1.0/src/oxn/config.py +403 -0
  55. oxn-0.1.0/src/oxn/context/__init__.py +15 -0
  56. oxn-0.1.0/src/oxn/context/bm25.py +133 -0
  57. oxn-0.1.0/src/oxn/context/bundle.py +352 -0
  58. oxn-0.1.0/src/oxn/context/decisions.py +46 -0
  59. oxn-0.1.0/src/oxn/context/project.py +34 -0
  60. oxn-0.1.0/src/oxn/doctor.py +94 -0
  61. oxn-0.1.0/src/oxn/graph/__init__.py +1 -0
  62. oxn-0.1.0/src/oxn/graph/algos.py +380 -0
  63. oxn-0.1.0/src/oxn/graph/architecture.py +509 -0
  64. oxn-0.1.0/src/oxn/graph/asp.py +113 -0
  65. oxn-0.1.0/src/oxn/graph/builder.py +313 -0
  66. oxn-0.1.0/src/oxn/graph/contracts.py +357 -0
  67. oxn-0.1.0/src/oxn/graph/depgraph.py +283 -0
  68. oxn-0.1.0/src/oxn/graph/imports.py +470 -0
  69. oxn-0.1.0/src/oxn/graph/indexer.py +433 -0
  70. oxn-0.1.0/src/oxn/graph/manifests.py +393 -0
  71. oxn-0.1.0/src/oxn/graph/model.py +219 -0
  72. oxn-0.1.0/src/oxn/graph/resolve.py +461 -0
  73. oxn-0.1.0/src/oxn/graph/resolve_rust.py +162 -0
  74. oxn-0.1.0/src/oxn/graph/rings.py +72 -0
  75. oxn-0.1.0/src/oxn/graph/rows.py +114 -0
  76. oxn-0.1.0/src/oxn/graph/sources.py +165 -0
  77. oxn-0.1.0/src/oxn/graph/store.py +596 -0
  78. oxn-0.1.0/src/oxn/health.py +205 -0
  79. oxn-0.1.0/src/oxn/init.py +518 -0
  80. oxn-0.1.0/src/oxn/languages.py +68 -0
  81. oxn-0.1.0/src/oxn/llm.py +386 -0
  82. oxn-0.1.0/src/oxn/metrics/__init__.py +26 -0
  83. oxn-0.1.0/src/oxn/metrics/callgraph.py +240 -0
  84. oxn-0.1.0/src/oxn/metrics/cognitive.py +549 -0
  85. oxn-0.1.0/src/oxn/metrics/cohesion.py +242 -0
  86. oxn-0.1.0/src/oxn/metrics/coupling.py +385 -0
  87. oxn-0.1.0/src/oxn/metrics/cyclomatic.py +113 -0
  88. oxn-0.1.0/src/oxn/metrics/engine.py +304 -0
  89. oxn-0.1.0/src/oxn/metrics/halstead.py +129 -0
  90. oxn-0.1.0/src/oxn/metrics/profile.py +197 -0
  91. oxn-0.1.0/src/oxn/metrics/shredding.py +173 -0
  92. oxn-0.1.0/src/oxn/metrics/size.py +250 -0
  93. oxn-0.1.0/src/oxn/profiles/__init__.py +59 -0
  94. oxn-0.1.0/src/oxn/profiles/base.py +535 -0
  95. oxn-0.1.0/src/oxn/profiles/go.py +206 -0
  96. oxn-0.1.0/src/oxn/profiles/java.py +214 -0
  97. oxn-0.1.0/src/oxn/profiles/python.py +238 -0
  98. oxn-0.1.0/src/oxn/profiles/rust.py +186 -0
  99. oxn-0.1.0/src/oxn/profiles/spec.py +305 -0
  100. oxn-0.1.0/src/oxn/profiles/typescript.py +298 -0
  101. oxn-0.1.0/src/oxn/py.typed +0 -0
  102. oxn-0.1.0/src/oxn/render.py +613 -0
  103. oxn-0.1.0/src/oxn/report.py +602 -0
  104. oxn-0.1.0/src/oxn/resolve/__init__.py +25 -0
  105. oxn-0.1.0/src/oxn/resolve/importers.py +201 -0
  106. oxn-0.1.0/src/oxn/resolve/measure.py +424 -0
  107. oxn-0.1.0/src/oxn/resolve/members.py +544 -0
  108. oxn-0.1.0/src/oxn/resolve/project.py +156 -0
  109. oxn-0.1.0/src/oxn/resolve/scopes.py +641 -0
  110. oxn-0.1.0/src/oxn/resolve/symbols.py +366 -0
  111. oxn-0.1.0/src/oxn/retry.py +231 -0
  112. oxn-0.1.0/src/oxn/review.py +475 -0
  113. oxn-0.1.0/src/oxn/rules/__init__.py +0 -0
  114. oxn-0.1.0/src/oxn/rules/adr.py +249 -0
  115. oxn-0.1.0/src/oxn/rules/builtin.py +239 -0
  116. oxn-0.1.0/src/oxn/rules/engine.py +299 -0
  117. oxn-0.1.0/src/oxn/rules/facts.py +262 -0
  118. oxn-0.1.0/src/oxn/rules/model.py +152 -0
  119. oxn-0.1.0/src/oxn/scip/__init__.py +29 -0
  120. oxn-0.1.0/src/oxn/scip/aliases.py +183 -0
  121. oxn-0.1.0/src/oxn/scip/index.py +247 -0
  122. oxn-0.1.0/src/oxn/scip/ingest.py +208 -0
  123. oxn-0.1.0/src/oxn/scip/join.py +401 -0
  124. oxn-0.1.0/src/oxn/scip/runner.py +399 -0
  125. oxn-0.1.0/src/oxn/scip/wire.py +99 -0
  126. oxn-0.1.0/src/oxn/server.py +434 -0
  127. oxn-0.1.0/src/oxn/thresholds.py +186 -0
  128. oxn-0.1.0/src/oxn/vcs/__init__.py +33 -0
  129. oxn-0.1.0/src/oxn/vcs/analysis.py +249 -0
  130. oxn-0.1.0/src/oxn/vcs/log.py +291 -0
  131. oxn-0.1.0/src/oxn/volume/__init__.py +24 -0
  132. oxn-0.1.0/src/oxn/volume/clones.py +360 -0
  133. oxn-0.1.0/src/oxn/volume/erosion.py +96 -0
  134. oxn-0.1.0/src/oxn/volume/scan.py +169 -0
  135. oxn-0.1.0/src/oxn/volume/tokens.py +107 -0
  136. oxn-0.1.0/src/oxn/writers.py +281 -0
  137. oxn-0.1.0/tests/conftest.py +43 -0
  138. oxn-0.1.0/tests/e2e/conftest.py +87 -0
  139. oxn-0.1.0/tests/e2e/fixtures/broken.py.txt +5 -0
  140. oxn-0.1.0/tests/e2e/fixtures/over_ceiling.py.txt +18 -0
  141. oxn-0.1.0/tests/e2e/fixtures/same_shape/Sample.java.txt +21 -0
  142. oxn-0.1.0/tests/e2e/fixtures/same_shape/sample.go.txt +21 -0
  143. oxn-0.1.0/tests/e2e/fixtures/same_shape/sample.js.txt +19 -0
  144. oxn-0.1.0/tests/e2e/fixtures/same_shape/sample.py.txt +13 -0
  145. oxn-0.1.0/tests/e2e/fixtures/same_shape/sample.rs.txt +19 -0
  146. oxn-0.1.0/tests/e2e/fixtures/same_shape/sample.ts.txt +19 -0
  147. oxn-0.1.0/tests/e2e/fixtures/shredded.py.txt +49 -0
  148. oxn-0.1.0/tests/e2e/fixtures/worse_ceiling.py.txt +23 -0
  149. oxn-0.1.0/tests/e2e/harness.py +94 -0
  150. oxn-0.1.0/tests/e2e/test_deep.py +168 -0
  151. oxn-0.1.0/tests/e2e/test_gate.py +121 -0
  152. oxn-0.1.0/tests/e2e/test_hook.py +90 -0
  153. oxn-0.1.0/tests/e2e/test_install.py +119 -0
  154. oxn-0.1.0/tests/e2e/test_lifecycle.py +147 -0
  155. oxn-0.1.0/tests/e2e/test_packaging.py +204 -0
  156. oxn-0.1.0/tests/e2e/test_pr_review.py +174 -0
  157. oxn-0.1.0/tests/e2e/test_ratchet.py +162 -0
  158. oxn-0.1.0/tests/e2e/test_surfaces.py +147 -0
  159. oxn-0.1.0/tests/fixtures/ck_agreement/M.java.txt +15 -0
  160. oxn-0.1.0/tests/fixtures/ck_agreement/m.js.txt +14 -0
  161. oxn-0.1.0/tests/fixtures/ck_agreement/m.py.txt +20 -0
  162. oxn-0.1.0/tests/fixtures/ck_agreement/m.ts.txt +15 -0
  163. oxn-0.1.0/tests/fixtures/class_fields/Fields.java.txt +15 -0
  164. oxn-0.1.0/tests/fixtures/class_fields/Methods.java.txt +15 -0
  165. oxn-0.1.0/tests/fixtures/class_fields/fields.js.txt +15 -0
  166. oxn-0.1.0/tests/fixtures/class_fields/fields.py.txt +14 -0
  167. oxn-0.1.0/tests/fixtures/class_fields/fields.ts.txt +15 -0
  168. oxn-0.1.0/tests/fixtures/class_fields/methods.js.txt +15 -0
  169. oxn-0.1.0/tests/fixtures/class_fields/methods.py.txt +40 -0
  170. oxn-0.1.0/tests/fixtures/class_fields/methods.ts.txt +15 -0
  171. oxn-0.1.0/tests/fixtures/class_scope/before.py.txt +33 -0
  172. oxn-0.1.0/tests/fixtures/class_scope/gamed.py.txt +70 -0
  173. oxn-0.1.0/tests/fixtures/class_scope/plump.py.txt +94 -0
  174. oxn-0.1.0/tests/fixtures/class_scope/public.py.txt +70 -0
  175. oxn-0.1.0/tests/fixtures/class_scope/shared.py.txt +94 -0
  176. oxn-0.1.0/tests/fixtures/class_scope/spread.py.txt +43 -0
  177. oxn-0.1.0/tests/fixtures/cohesion_shapes/bare_fields.java.txt +24 -0
  178. oxn-0.1.0/tests/fixtures/cohesion_shapes/god_class.py.txt +52 -0
  179. oxn-0.1.0/tests/fixtures/cohesion_shapes/repository.py.txt +48 -0
  180. oxn-0.1.0/tests/fixtures/cohesion_shapes/service.go.txt +11 -0
  181. oxn-0.1.0/tests/fixtures/cohesion_shapes/service.java.txt +9 -0
  182. oxn-0.1.0/tests/fixtures/cohesion_shapes/service.js.txt +14 -0
  183. oxn-0.1.0/tests/fixtures/cohesion_shapes/service.py.txt +16 -0
  184. oxn-0.1.0/tests/fixtures/cohesion_shapes/service.rs.txt +8 -0
  185. oxn-0.1.0/tests/fixtures/cohesion_shapes/service.ts.txt +9 -0
  186. oxn-0.1.0/tests/fixtures/cohesion_shapes/statics.py.txt +15 -0
  187. oxn-0.1.0/tests/fixtures/cohesion_shapes/statics.rs.txt +6 -0
  188. oxn-0.1.0/tests/fixtures/cohesive_extraction.py.txt +70 -0
  189. oxn-0.1.0/tests/fixtures/impl_blocks/one_block.rs.txt +21 -0
  190. oxn-0.1.0/tests/fixtures/impl_blocks/trait_impl.rs.txt +24 -0
  191. oxn-0.1.0/tests/fixtures/impl_blocks/two_blocks.rs.txt +24 -0
  192. oxn-0.1.0/tests/fixtures/impl_blocks/under.rs.txt +18 -0
  193. oxn-0.1.0/tests/fixtures/inheritance/M.java.txt +3 -0
  194. oxn-0.1.0/tests/fixtures/inheritance/m.go.txt +7 -0
  195. oxn-0.1.0/tests/fixtures/inheritance/m.js.txt +3 -0
  196. oxn-0.1.0/tests/fixtures/inheritance/m.py.txt +8 -0
  197. oxn-0.1.0/tests/fixtures/inheritance/m.rs.txt +5 -0
  198. oxn-0.1.0/tests/fixtures/inheritance/m.ts.txt +3 -0
  199. oxn-0.1.0/tests/fixtures/shred_control.py.txt +99 -0
  200. oxn-0.1.0/tests/fixtures/shred_languages/Public.java.txt +19 -0
  201. oxn-0.1.0/tests/fixtures/shred_languages/Router.java.txt +19 -0
  202. oxn-0.1.0/tests/fixtures/shred_languages/common.js.txt +20 -0
  203. oxn-0.1.0/tests/fixtures/shred_languages/public.rs.txt +18 -0
  204. oxn-0.1.0/tests/fixtures/shred_languages/public.ts.txt +18 -0
  205. oxn-0.1.0/tests/fixtures/shred_languages/shred.go.txt +20 -0
  206. oxn-0.1.0/tests/fixtures/shred_languages/shred.js.txt +18 -0
  207. oxn-0.1.0/tests/fixtures/shred_languages/shred.py.txt +86 -0
  208. oxn-0.1.0/tests/fixtures/shred_languages/shred.rs.txt +18 -0
  209. oxn-0.1.0/tests/fixtures/shred_languages/shred.ts.txt +18 -0
  210. oxn-0.1.0/tests/fixtures/split_across_files/more.go.txt +5 -0
  211. oxn-0.1.0/tests/fixtures/split_across_files/more.rs.txt +7 -0
  212. oxn-0.1.0/tests/fixtures/split_across_files/type.go.txt +9 -0
  213. oxn-0.1.0/tests/fixtures/split_across_files/type.rs.txt +6 -0
  214. oxn-0.1.0/tests/oracle_support.py +85 -0
  215. oxn-0.1.0/tests/oracles/README.md +18 -0
  216. oxn-0.1.0/tests/oracles/eslint.config.mjs +10 -0
  217. oxn-0.1.0/tests/oracles/sonarjs_runner.mjs +20 -0
  218. oxn-0.1.0/tests/test_actors.py +108 -0
  219. oxn-0.1.0/tests/test_architecture.py +659 -0
  220. oxn-0.1.0/tests/test_arms.py +153 -0
  221. oxn-0.1.0/tests/test_beds.py +379 -0
  222. oxn-0.1.0/tests/test_bm25.py +98 -0
  223. oxn-0.1.0/tests/test_bound_callables.py +165 -0
  224. oxn-0.1.0/tests/test_bundle.py +284 -0
  225. oxn-0.1.0/tests/test_calls_surface.py +377 -0
  226. oxn-0.1.0/tests/test_ceiling_observations.py +357 -0
  227. oxn-0.1.0/tests/test_check.py +584 -0
  228. oxn-0.1.0/tests/test_ck_agreement.py +72 -0
  229. oxn-0.1.0/tests/test_class_field_evasion.py +212 -0
  230. oxn-0.1.0/tests/test_class_scope_evasion.py +266 -0
  231. oxn-0.1.0/tests/test_cli.py +269 -0
  232. oxn-0.1.0/tests/test_contracts_on_the_hook.py +186 -0
  233. oxn-0.1.0/tests/test_corpus.py +100 -0
  234. oxn-0.1.0/tests/test_cross_language_cohesion.py +276 -0
  235. oxn-0.1.0/tests/test_curried_callables.py +100 -0
  236. oxn-0.1.0/tests/test_dogfood.py +882 -0
  237. oxn-0.1.0/tests/test_experiment.py +115 -0
  238. oxn-0.1.0/tests/test_go_class_ownership.py +165 -0
  239. oxn-0.1.0/tests/test_go_resolution.py +205 -0
  240. oxn-0.1.0/tests/test_grammars.py +204 -0
  241. oxn-0.1.0/tests/test_graph_algos.py +319 -0
  242. oxn-0.1.0/tests/test_graph_builder.py +133 -0
  243. oxn-0.1.0/tests/test_graph_rings.py +94 -0
  244. oxn-0.1.0/tests/test_graph_store.py +214 -0
  245. oxn-0.1.0/tests/test_health.py +187 -0
  246. oxn-0.1.0/tests/test_impl_block_evasion.py +81 -0
  247. oxn-0.1.0/tests/test_import_aliases.py +224 -0
  248. oxn-0.1.0/tests/test_import_guard.py +128 -0
  249. oxn-0.1.0/tests/test_imports.py +604 -0
  250. oxn-0.1.0/tests/test_indexer.py +229 -0
  251. oxn-0.1.0/tests/test_inheritance.py +142 -0
  252. oxn-0.1.0/tests/test_init.py +257 -0
  253. oxn-0.1.0/tests/test_java_resolution.py +141 -0
  254. oxn-0.1.0/tests/test_latency.py +161 -0
  255. oxn-0.1.0/tests/test_llm.py +272 -0
  256. oxn-0.1.0/tests/test_llm_protocol.py +445 -0
  257. oxn-0.1.0/tests/test_measures.py +311 -0
  258. oxn-0.1.0/tests/test_metrics_cognitive.py +266 -0
  259. oxn-0.1.0/tests/test_metrics_engine.py +194 -0
  260. oxn-0.1.0/tests/test_metrics_properties.py +172 -0
  261. oxn-0.1.0/tests/test_monorepo.py +185 -0
  262. oxn-0.1.0/tests/test_oracle_depcruise.py +197 -0
  263. oxn-0.1.0/tests/test_oracle_grimp.py +107 -0
  264. oxn-0.1.0/tests/test_oracle_jdeps.py +198 -0
  265. oxn-0.1.0/tests/test_oracle_lizard.py +85 -0
  266. oxn-0.1.0/tests/test_oracle_mcp.py +115 -0
  267. oxn-0.1.0/tests/test_oracle_pmd.py +96 -0
  268. oxn-0.1.0/tests/test_oracle_radon_complexipy.py +236 -0
  269. oxn-0.1.0/tests/test_oracle_scip.py +752 -0
  270. oxn-0.1.0/tests/test_oracle_sonarjs.py +144 -0
  271. oxn-0.1.0/tests/test_oracle_vulture.py +127 -0
  272. oxn-0.1.0/tests/test_polyglot.py +526 -0
  273. oxn-0.1.0/tests/test_profiles.py +198 -0
  274. oxn-0.1.0/tests/test_qualified_calls.py +435 -0
  275. oxn-0.1.0/tests/test_resolve.py +440 -0
  276. oxn-0.1.0/tests/test_resolve_accuracy.py +299 -0
  277. oxn-0.1.0/tests/test_retrieval_external.py +201 -0
  278. oxn-0.1.0/tests/test_retrieval_quality.py +243 -0
  279. oxn-0.1.0/tests/test_retry.py +524 -0
  280. oxn-0.1.0/tests/test_review.py +824 -0
  281. oxn-0.1.0/tests/test_risk_profile.py +128 -0
  282. oxn-0.1.0/tests/test_rule_switch.py +101 -0
  283. oxn-0.1.0/tests/test_rules_adr.py +241 -0
  284. oxn-0.1.0/tests/test_rules_corpus.py +209 -0
  285. oxn-0.1.0/tests/test_rules_engine.py +262 -0
  286. oxn-0.1.0/tests/test_rules_parity.py +195 -0
  287. oxn-0.1.0/tests/test_rust_resolution.py +143 -0
  288. oxn-0.1.0/tests/test_scip.py +252 -0
  289. oxn-0.1.0/tests/test_scip_local_symbols.py +208 -0
  290. oxn-0.1.0/tests/test_scip_registry.py +150 -0
  291. oxn-0.1.0/tests/test_scip_runner.py +275 -0
  292. oxn-0.1.0/tests/test_server.py +361 -0
  293. oxn-0.1.0/tests/test_shred_languages.py +133 -0
  294. oxn-0.1.0/tests/test_shredding.py +316 -0
  295. oxn-0.1.0/tests/test_split_across_files.py +82 -0
  296. oxn-0.1.0/tests/test_tier3.py +313 -0
  297. oxn-0.1.0/tests/test_toolchains.py +269 -0
  298. oxn-0.1.0/tests/test_vcs.py +322 -0
  299. oxn-0.1.0/tests/test_volume.py +253 -0
  300. oxn-0.1.0/tests/test_wide_repository.py +142 -0
oxn-0.1.0/.gitignore ADDED
@@ -0,0 +1,50 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ build/
6
+ dist/
7
+ .venv/
8
+ venv/
9
+ .venvs/
10
+ .tox/
11
+
12
+ # Tooling
13
+ .pytest_cache/
14
+ .mypy_cache/
15
+ .ruff_cache/
16
+ # complexipy writes this one in the oracle lane, and it was the single non-source
17
+ # directory reaching the sdist -- hatchling excludes what git ignores, so this is
18
+ # the fix for both at once. See tests/e2e/test_packaging.py.
19
+ .complexipy_cache/
20
+ .coverage
21
+ htmlcov/
22
+
23
+ # OXN working state. The cache is a build artifact; the baseline/ratchet is NOT --
24
+ # it belongs in version control so the "don't make it worse" gate is shared.
25
+ # Anchored patterns would only match at the root. A stray `src/.oxn/cache/graph.db`
26
+ # was committed on 2026-09-04 and went unnoticed until it turned up as the largest
27
+ # file in the v0.1.0 sdist, so these match an `.oxn/` at any depth.
28
+ **/.oxn/cache/
29
+ **/.oxn/index/
30
+ !.oxn/baseline.json
31
+
32
+ # Benchmark corpora are fetched, never vendored (see benchmarks/manifest.yaml).
33
+ benchmarks/corpora/
34
+ # Built SCIP indexes and their caches. Minutes of work, deterministic for a pinned
35
+ # corpus, and rebuilt on demand by scripts/measure_resolution.py -- kept between runs
36
+ # because rebuilding every time is the reason nobody re-measures.
37
+ benchmarks/.scip-cache/
38
+
39
+ # Oracle runners install their tools locally; never vendored.
40
+ tests/oracles/node_modules/
41
+ tests/oracles/package-lock.json
42
+
43
+ # OS
44
+ .DS_Store
45
+
46
+ # Oracle tools are downloaded, never vendored.
47
+ tools/
48
+
49
+ # Self-repair attempts are a run artifact; the harness is the source.
50
+ benchmarks/dogfood-log.jsonl
oxn-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mustafa Arslan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
oxn-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,498 @@
1
+ Metadata-Version: 2.5
2
+ Name: oxn
3
+ Version: 0.1.0
4
+ Summary: Oxygen (oxn) - a local architecture and quality gatekeeper for LLM coding agents
5
+ Project-URL: Repository, https://github.com/mustafarslan/oxn
6
+ Author: Mustafa Arslan
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: code-quality,llm-agents,mcp,software-architecture,static-analysis
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Software Development :: Quality Assurance
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: pydantic>=2.7
20
+ Requires-Dist: pyyaml>=6.0
21
+ Requires-Dist: rich>=13.7
22
+ Requires-Dist: tree-sitter-language-pack>=1.15
23
+ Requires-Dist: tree-sitter>=0.25
24
+ Requires-Dist: typer>=0.12
25
+ Provides-Extra: asp
26
+ Requires-Dist: clingo>=5.7; extra == 'asp'
27
+ Provides-Extra: dev
28
+ Requires-Dist: build>=1.2; extra == 'dev'
29
+ Requires-Dist: mypy>=1.11; extra == 'dev'
30
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
31
+ Requires-Dist: pytest>=8.0; extra == 'dev'
32
+ Requires-Dist: ruff>=0.6; extra == 'dev'
33
+ Requires-Dist: types-pyyaml>=6.0; extra == 'dev'
34
+ Provides-Extra: oracle
35
+ Requires-Dist: complexipy>=2.0; extra == 'oracle'
36
+ Requires-Dist: grimp>=3.0; extra == 'oracle'
37
+ Requires-Dist: lizard>=1.17; extra == 'oracle'
38
+ Requires-Dist: mcp>=2.0; extra == 'oracle'
39
+ Requires-Dist: networkx>=3.0; extra == 'oracle'
40
+ Requires-Dist: radon>=6.0; extra == 'oracle'
41
+ Requires-Dist: vulture>=2.11; extra == 'oracle'
42
+ Description-Content-Type: text/markdown
43
+
44
+ # Oxygen
45
+
46
+ A local architecture and quality gatekeeper for LLM coding agents.
47
+
48
+ The project is **Oxygen**; `oxn` is what you type — the command, the package and the import
49
+ name. Oxygen is the thing you never notice until it runs out, and architectural quality
50
+ behaves the same way: invisible while it holds, and the only thing that matters once it is
51
+ gone. Agents burn through it faster than people do.
52
+
53
+ OXN computes software architecture and quality metrics itself — from one normalized code
54
+ graph, uniformly across languages — and uses them to hold coding agents to a project's
55
+ declared architectural invariants. Semantic context goes *in* over MCP; deterministic
56
+ enforcement comes *out* over hooks. One `pip install`, one `oxn init`, no cloud, no daemon,
57
+ no paid tooling.
58
+
59
+ > **Status: alpha.** The analysis engine is built and gating: OXN has gated its own
60
+ > development since 2026-08-30, across six languages, on the hook path and in CI. What that
61
+ > leaves is breadth rather than scaffolding -- more corpora behind the ceilings, more
62
+ > languages past the launch six. See **[docs/metrics.md](docs/metrics.md)** for the metric
63
+ > specifications and **[docs/adr/](docs/adr/)** for the decisions behind them, each recording
64
+ > what was measured rather than what was intended.
65
+
66
+ ## Install, and the first five minutes
67
+
68
+ ```sh
69
+ python -m pip install oxn # `pipx install oxn` to keep it off your project's path
70
+
71
+ cd your-project
72
+ oxn init # writes oxn.yaml, the hook, the MCP entry, a CLAUDE.md section
73
+ oxn check # where you stand today
74
+ oxn baseline # accept today's debt; new violations still fail
75
+ ```
76
+
77
+ To run against unreleased changes, install from a checkout instead --
78
+ `git clone https://github.com/mustafarslan/oxn && python -m pip install ./oxn`.
79
+
80
+ If `oxn` is not on your `PATH` afterwards -- an unactivated virtualenv, or a `pipx` whose
81
+ bin directory is not exported -- `python -m oxn` is the same program by another name, and
82
+ `oxn init` says so rather than wiring a hook to a command that will not resolve.
83
+
84
+ `oxn init` is additive and idempotent: it writes `oxn.yaml` only when absent, keeps its
85
+ `CLAUDE.md` section between markers, and merges `.claude/settings.json` and `.mcp.json`
86
+ rather than replacing them. Running it twice produces no diff.
87
+
88
+ What each of those commands leaves behind, and how to take it back out:
89
+
90
+ | path | written by | removing it |
91
+ |---|---|---|
92
+ | `oxn.yaml` | `init` | delete |
93
+ | `.mcp.json` | `init` | delete, or drop the `oxn` entry if you have others |
94
+ | `.gitignore` | `init` | delete if `init` created it; otherwise drop the lines it appended |
95
+ | `.claude/settings.json` | `init` | drop OXN's `PostToolUse` entry; other hooks are untouched |
96
+ | `CLAUDE.md` | `init` | delete the block between `<!-- oxn:begin -->` and `<!-- oxn:end -->` |
97
+ | `.oxn/` | the first `check` | delete -- it holds the cache, the baseline and the retry ledger |
98
+
99
+ There is deliberately no `oxn uninstall`: the list is short, every entry is inspectable, and
100
+ a command that deletes files from a repository to undo a setup step is a worse trade than a
101
+ table. `pip uninstall oxn` removes the tool itself; the hook in `.claude/settings.json` then
102
+ names a command that no longer resolves, so remove that entry too.
103
+
104
+ ## Why it computes its own metrics
105
+
106
+ Every tool that does *architecture* metrics well — Sonargraph, Structure101, NDepend,
107
+ Understand, Lattix, CodeScene — is commercial. The free complexity tools (radon, gocyclo,
108
+ complexipy, eslint-plugin-sonarjs) are each single-language or single-metric, so composing
109
+ them means several subprocess boundaries inside a per-edit hook, and metric definitions
110
+ that quietly disagree across languages.
111
+
112
+ So OXN takes exactly two free dependencies where the work is genuine commodity —
113
+ **tree-sitter** for parsing and **SCIP** for name resolution — and computes everything else
114
+ itself. The free tools become differential-test oracles in CI, which is what keeps
115
+ "self-implemented" credible rather than merely asserted.
116
+
117
+ The full rule and the classification of every tool considered: **[ADR-0001](docs/adr/0001-dependency-policy.md)**.
118
+
119
+ ## Design decisions
120
+
121
+ | | |
122
+ |---|---|
123
+ | [ADR-0001](docs/adr/0001-dependency-policy.md) | Dependency policy — free-only, self-implement the rest |
124
+ | [ADR-0002](docs/adr/0002-resolution-strategy.md) | Name resolution — a tiered ladder with stamped outputs |
125
+ | [ADR-0003](docs/adr/0003-enforcement-model.md) | Enforcement — hooks enforce, MCP informs, constraints are budgeted |
126
+ | [ADR-0004](docs/adr/0004-freshness-model.md) | Freshness — how OXN answers quickly without a daemon |
127
+ | [ADR-0005](docs/adr/0005-rule-engine.md) | Rule engine — relations and conjunctive rules, Datalog-shaped |
128
+ | [ADR-0006](docs/adr/0006-retrieval-and-budgeting.md) | Retrieval — BM25 over decisions, budgeted, never blocking |
129
+
130
+ ## Development
131
+
132
+ ```sh
133
+ python -m pip install -e ".[dev]"
134
+ python scripts/check.py # the fast lane: lint, types, unit tests
135
+ python scripts/check.py --release # before tagging a version (see below)
136
+ python scripts/check.py --all --install # every lane, fetching what it needs
137
+ ```
138
+
139
+ `--release` is the lane to run before publishing. It is the *hermetic* set -- everything
140
+ that needs nothing but Python and git -- and it is deliberately smaller than `--all`:
141
+
142
+ | | |
143
+ |---|---|
144
+ | preflight | the tree is clean, `__version__` is declared, and that version is not already tagged |
145
+ | fast | lint, types, unit tests, and OXN's own gate over `src`, `tests` and `scripts` |
146
+ | matrix | the fast lane on every supported interpreter (needs `uv`) |
147
+ | e2e | build the wheel **and the sdist**, install each into a fresh virtualenv, and drive the `oxn` script |
148
+
149
+ `--all` additionally runs `oracle` (needs node and java), `corpus` (needs ~150k lines of
150
+ fetched source) and `llm` (needs Ollama reachable). Those are excluded from `--release` on
151
+ purpose: a release gate that fails because a toolchain is missing is a gate people learn to
152
+ skip.
153
+
154
+ OXN runs either as the `oxn` console script or as a module, which is what you want when the
155
+ script is not on `PATH` -- an unactivated virtualenv, `uv run`, or a hook whose environment
156
+ you do not control:
157
+
158
+ ```sh
159
+ oxn doctor # report grammars, toolchains and indexers found
160
+ python -m oxn doctor # identical, no PATH required
161
+ python -m oxn metrics --explain src/
162
+ ```
163
+
164
+ Lanes, in increasing cost:
165
+
166
+ | lane | what it runs |
167
+ |---|---|
168
+ | default | lint, types and the deterministic test suite -- no network, no toolchains |
169
+ | `--matrix` | the default lane on Python 3.10 through 3.14 |
170
+ | `--oracle` | differential tests against the third-party tools OXN deliberately does not depend on; needs Node, a JDK and Go |
171
+ | `--corpus` | phase exit criteria measured on real repositories |
172
+ | `--llm` | the few tests that need a language model, via Ollama |
173
+ | `--e2e` | builds a wheel, installs it into a fresh virtualenv and drives the `oxn` script |
174
+
175
+ The `e2e` lane is the one that does not trust the working tree. Every other lane imports
176
+ `oxn` from the repository, where everything is on the path and `pyproject.toml` is barely
177
+ consulted -- so a module the wheel does not ship, a dependency that is imported but never
178
+ declared, or a console script that does not resolve would pass all of them and fail on the
179
+ first machine that ran `pip install oxn`. This lane builds the wheel, installs it into a
180
+ fresh virtualenv, and drives the `oxn` script through install, `init`, `check`, `baseline`
181
+ and removal against fixtures whose numbers are known before OXN is asked -- including one
182
+ shape written in all six launch languages, which must measure identically in each.
183
+
184
+ GitHub Actions is manual-only by design -- `scripts/check.py` is this project's CI.
185
+
186
+ The `llm` lane uses Ollama with `kimi-k3:cloud`, overridable through `OXN_OLLAMA_MODEL` and
187
+ `OXN_OLLAMA_HOST`. It skips when the host is unreachable, and no other lane depends on a
188
+ model being up.
189
+
190
+ Benchmark corpora are pinned, never vendored:
191
+
192
+ ```sh
193
+ python scripts/fetch_corpora.py --list
194
+ python scripts/fetch_corpora.py --use eval
195
+ ```
196
+
197
+ ## The hook, and what it says
198
+
199
+ `oxn init` writes a `PostToolUse` hook on `Edit`/`Write`/`MultiEdit`. On a violation it exits
200
+ 2 and tells the agent what broke, at which line, and by how much:
201
+
202
+ ```
203
+ OXN: this edit breaks 1 invariant(s). Fix them before moving on.
204
+
205
+ src/api/client.py:6 api.client.send has cognitive_complexity 24, above the ceiling of 12 [attempt 1 of 3]
206
+ +1 at line 8: `for`
207
+ +2 at line 9: `if` nested 1 deep
208
+ +3 at line 10: `for` nested 2 deep
209
+ ```
210
+
211
+ That text goes to **stderr**, which is the stream Claude Code shows the agent on exit 2; the
212
+ JSON goes to stdout for CI and for `check_code`. Getting that backwards is not cosmetic, and
213
+ OXN had it backwards until 2026-09-05: the gate blocked every bad edit correctly and the
214
+ agent received the string `No stderr output`
215
+ ([ADR-0003](docs/adr/0003-enforcement-model.md)'s amendment).
216
+
217
+ **The loop is bounded.** LLM refactoring does not reliably converge (arXiv 2508.11958), so
218
+ after `retry_budget` failed repairs of the *same* violation the hook stops asking and reports
219
+ instead, with the trajectory that shows whether the agent was getting anywhere:
220
+
221
+ ```
222
+ OXN: retry budget spent. 3 repair(s) have not cleared 1 violation(s), and this is what each attempt scored:
223
+
224
+ src/api/client.py:6 api.client.send — cognitive_complexity 24 -> 17 -> 17 -> 17 (ceiling 12)
225
+
226
+ Stop editing these entities and report to the user.
227
+ ```
228
+
229
+ Attempts are counted per session and per violation, and repairing one forgets it. Set
230
+ `retry_budget: 0` in `oxn.yaml` to disable the bound — the right setting for CI, where there
231
+ is no agent to escalate to. Note the honest limit: a `PostToolUse` hook runs *after* the tool
232
+ and cannot end a turn, so the halt is an instruction rather than an enforcement.
233
+
234
+ The hook checks layer contracts too, on the edited file's own imports:
235
+
236
+ ```
237
+ src/oxn/graph/store.py:1 src/oxn/graph/store.py -> src/oxn/check.py: depends on a layer above it
238
+ ```
239
+
240
+ It sees edges *out of* the files it measured. Edges *into* them from files nobody edited,
241
+ and import cycles, need the whole graph — that is `oxn check --deep`, and it is the CI
242
+ scope:
243
+
244
+ ```sh
245
+ oxn check --deep # exit 0 clean · 2 violations · 1 OXN itself failed to run
246
+ ```
247
+
248
+ Exit 1 and exit 2 are deliberately different numbers: a gate that could not start must
249
+ never be read as a gate that passed. The retry budget does not apply in CI — it is keyed
250
+ on the agent `session_id` a hook sends and a pipeline does not — so CI always reports.
251
+
252
+ ## The health view, which never gates
253
+
254
+ `oxn health` is the other output, and it decides nothing. Per declared ceiling it reports the
255
+ share of source lines sitting in code over budget and names the code carrying it — a risk
256
+ profile rather than a rating, because a 1–5 score needs calibrated boundaries and OXN has five
257
+ corpora, not SIG's hundred.
258
+
259
+ ```sh
260
+ oxn health src/
261
+ ```
262
+
263
+ ```
264
+ cognitive_complexity <= 12 4.1% of 12,043 lines over budget (7 of 1,204 entities)
265
+ 21 oxn.graph.indexer.Indexer.index src/oxn/graph/indexer.py:88
266
+
267
+ coupling no ceiling; distribution over 22 exactly-measured of 30 classes
268
+ CBO median 0 p90 0 max 3
269
+ RFC median 1 p90 3 max 7
270
+ 8 class(es) left a base or callee unplaced; their numbers are lower bounds
271
+ >= 5 cbo 16 rfc Indexer src/oxn/graph/indexer.py
272
+ ```
273
+
274
+ Coupling is the one dimension with no share and no ceiling. CBO and RFC have no entry in
275
+ `oxn calibration`, so no boundary here has a measured cost, and the numbers the literature
276
+ offers were fitted to systems this project has not measured. The `>=` is the other half of
277
+ that honesty: below L2 an unplaced base or callee makes a value a floor, so the distribution
278
+ covers the exactly-measured classes and the rest are counted and named rather than averaged in.
279
+
280
+ ## Cycles, and the smallest edit that breaks one
281
+
282
+ `oxn arch` reports the dependency graph, and a dependency cycle is the one architectural
283
+ finding it can turn into a concrete edit. Naming the components in a ring states the problem
284
+ and leaves the hard half to the reader, so it also names the fewest component edges whose
285
+ removal breaks it, and the import statements behind each:
286
+
287
+ ```sh
288
+ oxn arch
289
+ ```
290
+
291
+ ```
292
+ Cycles (1)
293
+ 2 components: src/oxn, src/oxn/vcs
294
+ smallest break: 1 component edge, 1 import statement
295
+ src/oxn/vcs/analysis.py -> src/oxn/thresholds.py
296
+ ```
297
+
298
+ That set is minimal, not merely sufficient: it is the minimum feedback arc set, computed
299
+ exactly by a dynamic program over the ring's components. It is also not always the *only*
300
+ minimal set — two packages importing each other are broken by cutting either — so read it as
301
+ the size being proven and the particular edges being one answer of that size.
302
+
303
+ **Two counts, because only one of them is the work.** The minimisation is over *component
304
+ edges*, and removing one means deleting every import behind it. Those numbers diverge fast:
305
+ `typescript-nest`'s largest ring is 19 component edges and 81 import statements. Minimising
306
+ the second instead — weighting each edge by the imports behind it — is a different and equally
307
+ well-defined problem, and it is not built; the number reported is the unweighted one.
308
+
309
+ Rings above 20 components are declined rather than guessed at, unless `oxn[asp]` is installed:
310
+
311
+ ```sh
312
+ python -m pip install "oxn[asp]" # clingo; report path only, never the hook
313
+ ```
314
+
315
+ Across seven projects OXN measures 21 rings; 18 are five components or fewer and 19 are decided
316
+ exactly, so the extra changes two answers out of twenty-one. `oxn check` — the hook path — never touches
317
+ any of this.
318
+
319
+ ## The MCP server
320
+
321
+ `oxn init` writes `.mcp.json`, so an agent gets four read-only tools. They inform; the hook
322
+ is what enforces ([ADR-0003](docs/adr/0003-enforcement-model.md)).
323
+
324
+ | tool | what it answers |
325
+ |---|---|
326
+ | `get_architectural_context` | which constraints govern this task -- ranked, capped, and honest about what it left out |
327
+ | `check_code` | the gate's verdict on these files, without the power to stop anything |
328
+ | `get_metrics` | how the code measures, ranked by one metric |
329
+ | `explain_violation` | why one finding is a violation: the increment trail, and where its ceiling was declared |
330
+
331
+ ```sh
332
+ oxn serve # speak MCP over stdio; stdout is the wire, so nothing is printed
333
+ claude mcp add oxn -- oxn serve
334
+ ```
335
+
336
+ The protocol is hand-written against revision 2025-06-18 rather than taken from the MCP
337
+ Python SDK, which pulls 28 packages and two compiled wheels for an OAuth flow a stdio server
338
+ never performs — the measurement, and what would reverse it, are in
339
+ [ADR-0001](docs/adr/0001-dependency-policy.md)'s amendment. The SDK is a CI oracle instead:
340
+ `tests/test_oracle_mcp.py` drives `oxn serve` with the official client.
341
+
342
+ **`oxn` has to be on the PATH of the shell your editor starts the server with**, which an
343
+ unactivated virtualenv is not. `oxn init` detects that case and says so; the fix is a `pipx`
344
+ or `--user` install, or editing the `oxn` entry in `.mcp.json` to a path that resolves. Claude
345
+ Code expands variables there, so this repository's own entry reads
346
+
347
+ ```json
348
+ { "mcpServers": { "oxn": { "command": "${CLAUDE_PROJECT_DIR:-.}/.venv/bin/oxn", "args": ["serve"] } } }
349
+ ```
350
+
351
+ which names the virtualenv without naming a machine, and mirrors the `PostToolUse` hook
352
+ beside it. A hand-edited entry is left alone on every later `init`.
353
+
354
+ The server also answers about the *repository*, not about wherever your editor happened to
355
+ start it: it moves to the project root on startup, and `--root` or `OXN_ROOT` overrides that
356
+ when discovery would find the wrong tree.
357
+
358
+ ## `@oxn` on a pull request
359
+
360
+ `oxn review` measures what a pull request changed and, optionally, writes the comment.
361
+
362
+ ```console
363
+ $ oxn review --base origin/main
364
+ origin/main...HEAD 12 changed, 10 measured, 2 excluded
365
+ 1 new 0 regressed 4 pre-existing
366
+ new parse has cognitive_complexity 16, above the ceiling of 12 src/a.py:44
367
+ baselined Store has weighted_methods_per_class 31, above the ceiling of 25 src/c.py:3
368
+ ```
369
+
370
+ `oxn init --github` writes the workflow that runs it: tag `@oxn` in a pull-request comment and
371
+ the job measures the diff and posts the summary. It is opt-in, and an existing workflow file is
372
+ never overwritten.
373
+
374
+ **It does not gate.** Your CI `oxn check` job does that. Two gates disagreeing about one pull
375
+ request is worse than one gate.
376
+
377
+ ### The numbers come from OXN. The sentences may come from a model, which may not invent a
378
+ number
379
+
380
+ Without `--write` the comment is OXN's own, and every figure in it is read straight out of the
381
+ measurement because nothing composes one. That is what the generated workflow posts: a GitHub
382
+ runner has no model host, and a review with nothing to say is not a review.
383
+
384
+ `--write ollama` hands the *measurement* to a language model and asks it to phrase the summary
385
+ instead. Every numeral in the reply is checked against the measurement, and a reply stating a
386
+ number OXN did not measure is **refused** — the prose is thrown away and the mechanical body
387
+ posted in its place, with `invented` naming the numbers that cost the model its turn. There is
388
+ one retry, shown exactly which numbers were rejected, and no second one: a tool whose honesty
389
+ depends on how many times it asked is not honest.
390
+
391
+ Refusing costs the sentences, not the review. The rule is that prose nobody audits may not
392
+ carry an unmeasured number — not that a pull request goes unanswered because a model
393
+ misbehaved.
394
+
395
+ The check reads numbers from the measurement, and **only where they are numbers rather than
396
+ fragments of a name**. The payload carries the commit sha, and mining every digit run out of
397
+ `d4e89d72e5c4e0206173e6dc3df513f8f131f126` put `126`, `131`, `206173`, `513`, `72` and `89`
398
+ into an allowed set of twenty — a third of what a model could state came from an identifier,
399
+ so "coverage rose to 206173" passed. The prose side stays deliberately looser: a writer who
400
+ types digits is stating a number whatever they are glued to, and excusing `v2.1` or
401
+ `Python 3.11` is exactly what the rule exists to refuse.
402
+
403
+ The model is never shown the diff. A model given code reviews the code; this one restates
404
+ measurements. It also means a workflow triggered from a fork never hands that fork's contents
405
+ to a model.
406
+
407
+ Each finding answers two questions, because they are different. `origin` says whether
408
+ `.oxn/baseline.json` had seen it. **`introduced`** says whether the base commit had it — a long
409
+ function already over the ceiling and never baselined is `origin: new` and
410
+ `introduced: false`, and telling the author they wrote it is how a review stops being read.
411
+ OXN checks the merge base out into a throwaway worktree (not a stash: your uncommitted work is
412
+ left alone) and measures the same files under *your current* `oxn.yaml`, so a ceiling you
413
+ tightened does not read as code that got worse. `was` carries the base's value. Where the base
414
+ cannot be checked out — a shallow clone has the tip and not the merge base — `introduced` is
415
+ `null` rather than `false`, because "nobody looked" is not "you did not cause this".
416
+
417
+ **Which model writes it is `--model`, not another backend.** Ollama reaches local and hosted
418
+ models alike, so `--write ollama --model glm-5.3:cloud` is how you pick a different one; a
419
+ second vendor client would buy a second billing relationship rather than a second capability.
420
+ Two are measured — `kimi-k3:cloud` and `glm-5.3:cloud`, both producing a usable comment on the
421
+ first attempt in four runs each, neither inventing a number, glm the faster at 8–11 s against
422
+ 15–27 s.
423
+
424
+ One limit worth knowing: the writer is never shown the diff.
425
+
426
+ ## Self-repair
427
+
428
+ OXN gates coding agents on complexity, and used to violate its own ceilings.
429
+ `scripts/dogfood.py` drives the loop OXN exists to create, on OXN itself — a loop that has
430
+ now run out of work here: **`src`, `scripts` and `tests` report zero violations and the
431
+ baseline is empty**, so `plan` returns nothing unless you lower `--ceiling`.
432
+
433
+ ```sh
434
+ python scripts/dogfood.py plan # what is over the ceiling, and why
435
+ python scripts/dogfood.py repair --limit 3 # attempt repairs, write diffs for review
436
+ python scripts/dogfood.py repair --dry-run # exercise the loop with no model calls
437
+ python scripts/dogfood.py report # convergence, and the arm table
438
+ ```
439
+
440
+ The same loop is the evaluation harness, along three axes it varies independently:
441
+
442
+ ```sh
443
+ --bed self | go-kit | rust-ripgrep | python-httpx | typescript-nest | java-spring-petclinic
444
+ --arm none | claude-md | mcp | hooks | hybrid | hybrid-budget
445
+ --backend ollama | claude-code | dry-run
446
+ ```
447
+
448
+ A **bed** is where targets come from and *how a repair is checked there* — `go test` and
449
+ `go vet` for Go, `cargo clippy` for Rust, this project's pytest/ruff/mypy for itself. A check
450
+ a bed does not declare is skipped rather than failed, since a Go module has no type checker;
451
+ a declared check whose tool is missing is a failure, because then the repair was not verified.
452
+
453
+ An **arm** is which of OXN's three channels the agent gets: the rules `oxn init` writes into
454
+ `CLAUDE.md`, the increment trail `explain_violation` returns over MCP, and the hook rejecting
455
+ an edit for the agent to answer. `none` is the control and gets one attempt — without a
456
+ rejection there is nothing to retry against, and extra rounds would be re-rolls of the dice
457
+ scored as though feedback had helped.
458
+
459
+ `scripts/experiment.py` runs a whole grid, which is the only way to get a table worth reading:
460
+
461
+ ```sh
462
+ python scripts/experiment.py --dry-run --arms none,hybrid --repeat 3 # price it first
463
+ python scripts/experiment.py --bed self --arms none,hybrid --repeat 3
464
+ ```
465
+
466
+ It resolves the targets **once** and hands the same list to every arm. Running the harness by
467
+ hand per arm does not, and the first pilot's table read "none 100%, hybrid 0%" for exactly
468
+ that reason — two invocations, two different functions, one plausible-looking finding. The
469
+ `n` column and the note under the table exist for the same kind of reason: `generate` is
470
+ temperature 0 and not deterministic, so a rate over one sample per target cannot be told
471
+ apart from noise and should not be read as though it can.
472
+
473
+ Two models, deliberately different: **kimi-k3** writes the repair, **deepseek-v4-pro**
474
+ assesses it. A model grading its own output is not an independent check. The actor default was
475
+ `glm-5.3` until it was measured filling every output budget it was given and cutting off
476
+ mid-answer, deterministically; `kimi-k3` is measured as a *writer* and not yet as a repair
477
+ actor, which is a starting point rather than a claim. `--backend
478
+ claude-code` drives `claude -p` instead, which is the arm that decides what a result means:
479
+ Claude Code under OXN's own hooks is the agent the tool exists to govern.
480
+
481
+ **The judge never overrules the deterministic gauntlet.** A candidate that fails tests,
482
+ types, lint, or the shredding detector is rejected before a judge sees it — because "the
483
+ score went down" is precisely the gaming a per-function ceiling invites, and detecting it
484
+ needs a measurement, not an opinion. Complexity *mass* was the obvious measurement and it
485
+ turned out to be exactly wrong — it ranks a shred above a good refactoring, for a reason
486
+ that is a property of the metric rather than a bad threshold (`docs/metrics.md` §10.5).
487
+ What works is what the helpers are worth: a cohesive split yields helpers with bodies, a
488
+ shred yields lines with names.
489
+
490
+ Nothing touches the working tree. Every attempt runs in a throwaway copy with its own
491
+ virtualenv, and the loop emits diffs plus a JSONL log for a human to review and commit.
492
+ That log is also the convergence evidence: LLM refactoring is known to fail to reach
493
+ complexity thresholds ([arXiv 2508.11958](https://arxiv.org/abs/2508.11958)), so measuring
494
+ it on a real codebase is worth having.
495
+
496
+ ## Licence
497
+
498
+ MIT. See [LICENSE](LICENSE).