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.
- oxn-0.1.0/.gitignore +50 -0
- oxn-0.1.0/LICENSE +21 -0
- oxn-0.1.0/PKG-INFO +498 -0
- oxn-0.1.0/README.md +455 -0
- oxn-0.1.0/benchmarks/ceiling-observations.json +2112 -0
- oxn-0.1.0/benchmarks/lock.json +19 -0
- oxn-0.1.0/benchmarks/manifest.yaml +226 -0
- oxn-0.1.0/benchmarks/retrieval-corpus-oxn.json +11 -0
- oxn-0.1.0/benchmarks/retrieval-labels-external.json +869 -0
- oxn-0.1.0/benchmarks/retrieval-labels-oxn.json +427 -0
- oxn-0.1.0/docs/adr/0001-dependency-policy.md +152 -0
- oxn-0.1.0/docs/adr/0002-resolution-strategy.md +927 -0
- oxn-0.1.0/docs/adr/0003-enforcement-model.md +149 -0
- oxn-0.1.0/docs/adr/0004-freshness-model.md +267 -0
- oxn-0.1.0/docs/adr/0005-rule-engine.md +531 -0
- oxn-0.1.0/docs/adr/0006-retrieval-and-budgeting.md +366 -0
- oxn-0.1.0/docs/divergences.md +705 -0
- oxn-0.1.0/docs/metrics.md +1758 -0
- oxn-0.1.0/docs/proposals/runtime-evidence.md +181 -0
- oxn-0.1.0/pyproject.toml +108 -0
- oxn-0.1.0/scripts/actor.py +414 -0
- oxn-0.1.0/scripts/actors.py +259 -0
- oxn-0.1.0/scripts/arms.py +101 -0
- oxn-0.1.0/scripts/attempt.py +344 -0
- oxn-0.1.0/scripts/beds.py +430 -0
- oxn-0.1.0/scripts/check.py +347 -0
- oxn-0.1.0/scripts/console.py +28 -0
- oxn-0.1.0/scripts/discovered_modules.py +159 -0
- oxn-0.1.0/scripts/dogfood.py +387 -0
- oxn-0.1.0/scripts/experiment.py +145 -0
- oxn-0.1.0/scripts/extract_citation_labels.py +198 -0
- oxn-0.1.0/scripts/extract_retrieval_labels.py +123 -0
- oxn-0.1.0/scripts/fetch_corpora.py +160 -0
- oxn-0.1.0/scripts/freeze_retrieval_corpus.py +109 -0
- oxn-0.1.0/scripts/gauntlet.py +527 -0
- oxn-0.1.0/scripts/measure_ceilings.py +521 -0
- oxn-0.1.0/scripts/measure_cycles.py +115 -0
- oxn-0.1.0/scripts/measure_diagnostics.py +79 -0
- oxn-0.1.0/scripts/measure_duplication.py +220 -0
- oxn-0.1.0/scripts/measure_parse_errors.py +170 -0
- oxn-0.1.0/scripts/measure_redundancy.py +101 -0
- oxn-0.1.0/scripts/measure_resolution.py +139 -0
- oxn-0.1.0/scripts/measure_semantic.py +300 -0
- oxn-0.1.0/scripts/runlog.py +338 -0
- oxn-0.1.0/scripts/summary.py +166 -0
- oxn-0.1.0/scripts/targets.py +161 -0
- oxn-0.1.0/src/oxn/__init__.py +10 -0
- oxn-0.1.0/src/oxn/__main__.py +16 -0
- oxn-0.1.0/src/oxn/_app.py +470 -0
- oxn-0.1.0/src/oxn/calibration.py +603 -0
- oxn-0.1.0/src/oxn/calls.py +282 -0
- oxn-0.1.0/src/oxn/check.py +770 -0
- oxn-0.1.0/src/oxn/cli.py +256 -0
- oxn-0.1.0/src/oxn/config.py +403 -0
- oxn-0.1.0/src/oxn/context/__init__.py +15 -0
- oxn-0.1.0/src/oxn/context/bm25.py +133 -0
- oxn-0.1.0/src/oxn/context/bundle.py +352 -0
- oxn-0.1.0/src/oxn/context/decisions.py +46 -0
- oxn-0.1.0/src/oxn/context/project.py +34 -0
- oxn-0.1.0/src/oxn/doctor.py +94 -0
- oxn-0.1.0/src/oxn/graph/__init__.py +1 -0
- oxn-0.1.0/src/oxn/graph/algos.py +380 -0
- oxn-0.1.0/src/oxn/graph/architecture.py +509 -0
- oxn-0.1.0/src/oxn/graph/asp.py +113 -0
- oxn-0.1.0/src/oxn/graph/builder.py +313 -0
- oxn-0.1.0/src/oxn/graph/contracts.py +357 -0
- oxn-0.1.0/src/oxn/graph/depgraph.py +283 -0
- oxn-0.1.0/src/oxn/graph/imports.py +470 -0
- oxn-0.1.0/src/oxn/graph/indexer.py +433 -0
- oxn-0.1.0/src/oxn/graph/manifests.py +393 -0
- oxn-0.1.0/src/oxn/graph/model.py +219 -0
- oxn-0.1.0/src/oxn/graph/resolve.py +461 -0
- oxn-0.1.0/src/oxn/graph/resolve_rust.py +162 -0
- oxn-0.1.0/src/oxn/graph/rings.py +72 -0
- oxn-0.1.0/src/oxn/graph/rows.py +114 -0
- oxn-0.1.0/src/oxn/graph/sources.py +165 -0
- oxn-0.1.0/src/oxn/graph/store.py +596 -0
- oxn-0.1.0/src/oxn/health.py +205 -0
- oxn-0.1.0/src/oxn/init.py +518 -0
- oxn-0.1.0/src/oxn/languages.py +68 -0
- oxn-0.1.0/src/oxn/llm.py +386 -0
- oxn-0.1.0/src/oxn/metrics/__init__.py +26 -0
- oxn-0.1.0/src/oxn/metrics/callgraph.py +240 -0
- oxn-0.1.0/src/oxn/metrics/cognitive.py +549 -0
- oxn-0.1.0/src/oxn/metrics/cohesion.py +242 -0
- oxn-0.1.0/src/oxn/metrics/coupling.py +385 -0
- oxn-0.1.0/src/oxn/metrics/cyclomatic.py +113 -0
- oxn-0.1.0/src/oxn/metrics/engine.py +304 -0
- oxn-0.1.0/src/oxn/metrics/halstead.py +129 -0
- oxn-0.1.0/src/oxn/metrics/profile.py +197 -0
- oxn-0.1.0/src/oxn/metrics/shredding.py +173 -0
- oxn-0.1.0/src/oxn/metrics/size.py +250 -0
- oxn-0.1.0/src/oxn/profiles/__init__.py +59 -0
- oxn-0.1.0/src/oxn/profiles/base.py +535 -0
- oxn-0.1.0/src/oxn/profiles/go.py +206 -0
- oxn-0.1.0/src/oxn/profiles/java.py +214 -0
- oxn-0.1.0/src/oxn/profiles/python.py +238 -0
- oxn-0.1.0/src/oxn/profiles/rust.py +186 -0
- oxn-0.1.0/src/oxn/profiles/spec.py +305 -0
- oxn-0.1.0/src/oxn/profiles/typescript.py +298 -0
- oxn-0.1.0/src/oxn/py.typed +0 -0
- oxn-0.1.0/src/oxn/render.py +613 -0
- oxn-0.1.0/src/oxn/report.py +602 -0
- oxn-0.1.0/src/oxn/resolve/__init__.py +25 -0
- oxn-0.1.0/src/oxn/resolve/importers.py +201 -0
- oxn-0.1.0/src/oxn/resolve/measure.py +424 -0
- oxn-0.1.0/src/oxn/resolve/members.py +544 -0
- oxn-0.1.0/src/oxn/resolve/project.py +156 -0
- oxn-0.1.0/src/oxn/resolve/scopes.py +641 -0
- oxn-0.1.0/src/oxn/resolve/symbols.py +366 -0
- oxn-0.1.0/src/oxn/retry.py +231 -0
- oxn-0.1.0/src/oxn/review.py +475 -0
- oxn-0.1.0/src/oxn/rules/__init__.py +0 -0
- oxn-0.1.0/src/oxn/rules/adr.py +249 -0
- oxn-0.1.0/src/oxn/rules/builtin.py +239 -0
- oxn-0.1.0/src/oxn/rules/engine.py +299 -0
- oxn-0.1.0/src/oxn/rules/facts.py +262 -0
- oxn-0.1.0/src/oxn/rules/model.py +152 -0
- oxn-0.1.0/src/oxn/scip/__init__.py +29 -0
- oxn-0.1.0/src/oxn/scip/aliases.py +183 -0
- oxn-0.1.0/src/oxn/scip/index.py +247 -0
- oxn-0.1.0/src/oxn/scip/ingest.py +208 -0
- oxn-0.1.0/src/oxn/scip/join.py +401 -0
- oxn-0.1.0/src/oxn/scip/runner.py +399 -0
- oxn-0.1.0/src/oxn/scip/wire.py +99 -0
- oxn-0.1.0/src/oxn/server.py +434 -0
- oxn-0.1.0/src/oxn/thresholds.py +186 -0
- oxn-0.1.0/src/oxn/vcs/__init__.py +33 -0
- oxn-0.1.0/src/oxn/vcs/analysis.py +249 -0
- oxn-0.1.0/src/oxn/vcs/log.py +291 -0
- oxn-0.1.0/src/oxn/volume/__init__.py +24 -0
- oxn-0.1.0/src/oxn/volume/clones.py +360 -0
- oxn-0.1.0/src/oxn/volume/erosion.py +96 -0
- oxn-0.1.0/src/oxn/volume/scan.py +169 -0
- oxn-0.1.0/src/oxn/volume/tokens.py +107 -0
- oxn-0.1.0/src/oxn/writers.py +281 -0
- oxn-0.1.0/tests/conftest.py +43 -0
- oxn-0.1.0/tests/e2e/conftest.py +87 -0
- oxn-0.1.0/tests/e2e/fixtures/broken.py.txt +5 -0
- oxn-0.1.0/tests/e2e/fixtures/over_ceiling.py.txt +18 -0
- oxn-0.1.0/tests/e2e/fixtures/same_shape/Sample.java.txt +21 -0
- oxn-0.1.0/tests/e2e/fixtures/same_shape/sample.go.txt +21 -0
- oxn-0.1.0/tests/e2e/fixtures/same_shape/sample.js.txt +19 -0
- oxn-0.1.0/tests/e2e/fixtures/same_shape/sample.py.txt +13 -0
- oxn-0.1.0/tests/e2e/fixtures/same_shape/sample.rs.txt +19 -0
- oxn-0.1.0/tests/e2e/fixtures/same_shape/sample.ts.txt +19 -0
- oxn-0.1.0/tests/e2e/fixtures/shredded.py.txt +49 -0
- oxn-0.1.0/tests/e2e/fixtures/worse_ceiling.py.txt +23 -0
- oxn-0.1.0/tests/e2e/harness.py +94 -0
- oxn-0.1.0/tests/e2e/test_deep.py +168 -0
- oxn-0.1.0/tests/e2e/test_gate.py +121 -0
- oxn-0.1.0/tests/e2e/test_hook.py +90 -0
- oxn-0.1.0/tests/e2e/test_install.py +119 -0
- oxn-0.1.0/tests/e2e/test_lifecycle.py +147 -0
- oxn-0.1.0/tests/e2e/test_packaging.py +204 -0
- oxn-0.1.0/tests/e2e/test_pr_review.py +174 -0
- oxn-0.1.0/tests/e2e/test_ratchet.py +162 -0
- oxn-0.1.0/tests/e2e/test_surfaces.py +147 -0
- oxn-0.1.0/tests/fixtures/ck_agreement/M.java.txt +15 -0
- oxn-0.1.0/tests/fixtures/ck_agreement/m.js.txt +14 -0
- oxn-0.1.0/tests/fixtures/ck_agreement/m.py.txt +20 -0
- oxn-0.1.0/tests/fixtures/ck_agreement/m.ts.txt +15 -0
- oxn-0.1.0/tests/fixtures/class_fields/Fields.java.txt +15 -0
- oxn-0.1.0/tests/fixtures/class_fields/Methods.java.txt +15 -0
- oxn-0.1.0/tests/fixtures/class_fields/fields.js.txt +15 -0
- oxn-0.1.0/tests/fixtures/class_fields/fields.py.txt +14 -0
- oxn-0.1.0/tests/fixtures/class_fields/fields.ts.txt +15 -0
- oxn-0.1.0/tests/fixtures/class_fields/methods.js.txt +15 -0
- oxn-0.1.0/tests/fixtures/class_fields/methods.py.txt +40 -0
- oxn-0.1.0/tests/fixtures/class_fields/methods.ts.txt +15 -0
- oxn-0.1.0/tests/fixtures/class_scope/before.py.txt +33 -0
- oxn-0.1.0/tests/fixtures/class_scope/gamed.py.txt +70 -0
- oxn-0.1.0/tests/fixtures/class_scope/plump.py.txt +94 -0
- oxn-0.1.0/tests/fixtures/class_scope/public.py.txt +70 -0
- oxn-0.1.0/tests/fixtures/class_scope/shared.py.txt +94 -0
- oxn-0.1.0/tests/fixtures/class_scope/spread.py.txt +43 -0
- oxn-0.1.0/tests/fixtures/cohesion_shapes/bare_fields.java.txt +24 -0
- oxn-0.1.0/tests/fixtures/cohesion_shapes/god_class.py.txt +52 -0
- oxn-0.1.0/tests/fixtures/cohesion_shapes/repository.py.txt +48 -0
- oxn-0.1.0/tests/fixtures/cohesion_shapes/service.go.txt +11 -0
- oxn-0.1.0/tests/fixtures/cohesion_shapes/service.java.txt +9 -0
- oxn-0.1.0/tests/fixtures/cohesion_shapes/service.js.txt +14 -0
- oxn-0.1.0/tests/fixtures/cohesion_shapes/service.py.txt +16 -0
- oxn-0.1.0/tests/fixtures/cohesion_shapes/service.rs.txt +8 -0
- oxn-0.1.0/tests/fixtures/cohesion_shapes/service.ts.txt +9 -0
- oxn-0.1.0/tests/fixtures/cohesion_shapes/statics.py.txt +15 -0
- oxn-0.1.0/tests/fixtures/cohesion_shapes/statics.rs.txt +6 -0
- oxn-0.1.0/tests/fixtures/cohesive_extraction.py.txt +70 -0
- oxn-0.1.0/tests/fixtures/impl_blocks/one_block.rs.txt +21 -0
- oxn-0.1.0/tests/fixtures/impl_blocks/trait_impl.rs.txt +24 -0
- oxn-0.1.0/tests/fixtures/impl_blocks/two_blocks.rs.txt +24 -0
- oxn-0.1.0/tests/fixtures/impl_blocks/under.rs.txt +18 -0
- oxn-0.1.0/tests/fixtures/inheritance/M.java.txt +3 -0
- oxn-0.1.0/tests/fixtures/inheritance/m.go.txt +7 -0
- oxn-0.1.0/tests/fixtures/inheritance/m.js.txt +3 -0
- oxn-0.1.0/tests/fixtures/inheritance/m.py.txt +8 -0
- oxn-0.1.0/tests/fixtures/inheritance/m.rs.txt +5 -0
- oxn-0.1.0/tests/fixtures/inheritance/m.ts.txt +3 -0
- oxn-0.1.0/tests/fixtures/shred_control.py.txt +99 -0
- oxn-0.1.0/tests/fixtures/shred_languages/Public.java.txt +19 -0
- oxn-0.1.0/tests/fixtures/shred_languages/Router.java.txt +19 -0
- oxn-0.1.0/tests/fixtures/shred_languages/common.js.txt +20 -0
- oxn-0.1.0/tests/fixtures/shred_languages/public.rs.txt +18 -0
- oxn-0.1.0/tests/fixtures/shred_languages/public.ts.txt +18 -0
- oxn-0.1.0/tests/fixtures/shred_languages/shred.go.txt +20 -0
- oxn-0.1.0/tests/fixtures/shred_languages/shred.js.txt +18 -0
- oxn-0.1.0/tests/fixtures/shred_languages/shred.py.txt +86 -0
- oxn-0.1.0/tests/fixtures/shred_languages/shred.rs.txt +18 -0
- oxn-0.1.0/tests/fixtures/shred_languages/shred.ts.txt +18 -0
- oxn-0.1.0/tests/fixtures/split_across_files/more.go.txt +5 -0
- oxn-0.1.0/tests/fixtures/split_across_files/more.rs.txt +7 -0
- oxn-0.1.0/tests/fixtures/split_across_files/type.go.txt +9 -0
- oxn-0.1.0/tests/fixtures/split_across_files/type.rs.txt +6 -0
- oxn-0.1.0/tests/oracle_support.py +85 -0
- oxn-0.1.0/tests/oracles/README.md +18 -0
- oxn-0.1.0/tests/oracles/eslint.config.mjs +10 -0
- oxn-0.1.0/tests/oracles/sonarjs_runner.mjs +20 -0
- oxn-0.1.0/tests/test_actors.py +108 -0
- oxn-0.1.0/tests/test_architecture.py +659 -0
- oxn-0.1.0/tests/test_arms.py +153 -0
- oxn-0.1.0/tests/test_beds.py +379 -0
- oxn-0.1.0/tests/test_bm25.py +98 -0
- oxn-0.1.0/tests/test_bound_callables.py +165 -0
- oxn-0.1.0/tests/test_bundle.py +284 -0
- oxn-0.1.0/tests/test_calls_surface.py +377 -0
- oxn-0.1.0/tests/test_ceiling_observations.py +357 -0
- oxn-0.1.0/tests/test_check.py +584 -0
- oxn-0.1.0/tests/test_ck_agreement.py +72 -0
- oxn-0.1.0/tests/test_class_field_evasion.py +212 -0
- oxn-0.1.0/tests/test_class_scope_evasion.py +266 -0
- oxn-0.1.0/tests/test_cli.py +269 -0
- oxn-0.1.0/tests/test_contracts_on_the_hook.py +186 -0
- oxn-0.1.0/tests/test_corpus.py +100 -0
- oxn-0.1.0/tests/test_cross_language_cohesion.py +276 -0
- oxn-0.1.0/tests/test_curried_callables.py +100 -0
- oxn-0.1.0/tests/test_dogfood.py +882 -0
- oxn-0.1.0/tests/test_experiment.py +115 -0
- oxn-0.1.0/tests/test_go_class_ownership.py +165 -0
- oxn-0.1.0/tests/test_go_resolution.py +205 -0
- oxn-0.1.0/tests/test_grammars.py +204 -0
- oxn-0.1.0/tests/test_graph_algos.py +319 -0
- oxn-0.1.0/tests/test_graph_builder.py +133 -0
- oxn-0.1.0/tests/test_graph_rings.py +94 -0
- oxn-0.1.0/tests/test_graph_store.py +214 -0
- oxn-0.1.0/tests/test_health.py +187 -0
- oxn-0.1.0/tests/test_impl_block_evasion.py +81 -0
- oxn-0.1.0/tests/test_import_aliases.py +224 -0
- oxn-0.1.0/tests/test_import_guard.py +128 -0
- oxn-0.1.0/tests/test_imports.py +604 -0
- oxn-0.1.0/tests/test_indexer.py +229 -0
- oxn-0.1.0/tests/test_inheritance.py +142 -0
- oxn-0.1.0/tests/test_init.py +257 -0
- oxn-0.1.0/tests/test_java_resolution.py +141 -0
- oxn-0.1.0/tests/test_latency.py +161 -0
- oxn-0.1.0/tests/test_llm.py +272 -0
- oxn-0.1.0/tests/test_llm_protocol.py +445 -0
- oxn-0.1.0/tests/test_measures.py +311 -0
- oxn-0.1.0/tests/test_metrics_cognitive.py +266 -0
- oxn-0.1.0/tests/test_metrics_engine.py +194 -0
- oxn-0.1.0/tests/test_metrics_properties.py +172 -0
- oxn-0.1.0/tests/test_monorepo.py +185 -0
- oxn-0.1.0/tests/test_oracle_depcruise.py +197 -0
- oxn-0.1.0/tests/test_oracle_grimp.py +107 -0
- oxn-0.1.0/tests/test_oracle_jdeps.py +198 -0
- oxn-0.1.0/tests/test_oracle_lizard.py +85 -0
- oxn-0.1.0/tests/test_oracle_mcp.py +115 -0
- oxn-0.1.0/tests/test_oracle_pmd.py +96 -0
- oxn-0.1.0/tests/test_oracle_radon_complexipy.py +236 -0
- oxn-0.1.0/tests/test_oracle_scip.py +752 -0
- oxn-0.1.0/tests/test_oracle_sonarjs.py +144 -0
- oxn-0.1.0/tests/test_oracle_vulture.py +127 -0
- oxn-0.1.0/tests/test_polyglot.py +526 -0
- oxn-0.1.0/tests/test_profiles.py +198 -0
- oxn-0.1.0/tests/test_qualified_calls.py +435 -0
- oxn-0.1.0/tests/test_resolve.py +440 -0
- oxn-0.1.0/tests/test_resolve_accuracy.py +299 -0
- oxn-0.1.0/tests/test_retrieval_external.py +201 -0
- oxn-0.1.0/tests/test_retrieval_quality.py +243 -0
- oxn-0.1.0/tests/test_retry.py +524 -0
- oxn-0.1.0/tests/test_review.py +824 -0
- oxn-0.1.0/tests/test_risk_profile.py +128 -0
- oxn-0.1.0/tests/test_rule_switch.py +101 -0
- oxn-0.1.0/tests/test_rules_adr.py +241 -0
- oxn-0.1.0/tests/test_rules_corpus.py +209 -0
- oxn-0.1.0/tests/test_rules_engine.py +262 -0
- oxn-0.1.0/tests/test_rules_parity.py +195 -0
- oxn-0.1.0/tests/test_rust_resolution.py +143 -0
- oxn-0.1.0/tests/test_scip.py +252 -0
- oxn-0.1.0/tests/test_scip_local_symbols.py +208 -0
- oxn-0.1.0/tests/test_scip_registry.py +150 -0
- oxn-0.1.0/tests/test_scip_runner.py +275 -0
- oxn-0.1.0/tests/test_server.py +361 -0
- oxn-0.1.0/tests/test_shred_languages.py +133 -0
- oxn-0.1.0/tests/test_shredding.py +316 -0
- oxn-0.1.0/tests/test_split_across_files.py +82 -0
- oxn-0.1.0/tests/test_tier3.py +313 -0
- oxn-0.1.0/tests/test_toolchains.py +269 -0
- oxn-0.1.0/tests/test_vcs.py +322 -0
- oxn-0.1.0/tests/test_volume.py +253 -0
- 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).
|