Code quality¶
A project born from desecration, built with AI across multiple sessions, whose primary architectural document is a fake Lovecraftian grimoire — has no business passing a linter. It passes every single one. That's somehow worse.
The inquisitors arrived at dawn, instruments of measurement in hand, expecting to find the temple in ruin. Instead they found the walls straight, the columns load-bearing, and the altar — though undeniably profane — structurally sound. They left in silence, more disturbed than when they came.
— De Vermis Mysteriis, On the Inadequacy of Metrics (unfortunately)
Tools¶
All repos use the same toolchain:
- pylint — static analysis. Style warnings (too-many-locals, line-too-long, too-many-arguments) are accepted. Real issues (unused imports, actual bugs, f-strings without placeholders) are not.
- pyflakes — fast, zero-config, no false positives. Must be clean.
- radon — cyclomatic complexity. Target: no function rated D or worse. C-rated functions are tolerated when they're natural dispatchers or sequential logic that wouldn't benefit from splitting. Current worst:
_read_yaml_config(20) and_collect_uids(18), both C.
Aberrant, not sloppy (we hope)¶
AI-generated code rots. Not dramatically — not "the server is on fire" rot. The quiet kind. Unused imports that accumulate like dust. Guards that made sense three refactors ago. Patterns applied because the model saw them elsewhere, not because they belong here. Left alone, the temple doesn't collapse — it just slowly fills with cobwebs that nobody notices until something breaks at 3 AM.
So the temple got inquisitors.
The inquisitors run after every session. pylint, pyflakes, radon — not in CI, not in a weekly cron, but in the conversation itself. The AI is told to lint before declaring victory. It usually finds something. The something is usually its own fault.
The scripture is carved into the walls. Every repo has a CLAUDE.md that encodes what the AI must not forget between sessions: null-safe YAML patterns, duck typing rationale, naming conventions, what not to "improve." The AI reads these on every session start. Without them, it would helpfully refactor working code into something subtly different — same tests passing, different behavior in production. The guardrails exist because the oracle has no memory and infinite confidence.
The architect walks the halls. Periodic sessions where no features are built — just reading, linting, questioning. Pure inspection. The v2.3.1 null-safety sweep (30+ fixes) came from one such walk. So did the realization that _fix_postgresql had been silently eating volumes for three versions.
A rival constellation was summoned. Gemini — a competing oracle — was fed the codebase cold and told to find what Claude had missed. Forty accusations. Twelve truths. The truths had survived months of single-oracle review. The journal has the full accounting. The lesson: even a lesser scribe, held at the wrong angle, casts shadows the sun never revealed.
The tentacles have always been in the idea, not in the code. These rituals exist to keep it that way.
Current scores¶
Last updated: 2026-03-07 — six helpers promoted, an ordering bug fixed, and the linters still approve.
This page covers dekube-engine, dekube-manager, and the built-in distribution extensions (the Eight Monks). Third-party and official extensions track their own scores in their respective READMEs.
Pylint¶
| Repo | Score | Notes |
|---|---|---|
| dekube-indexer-configmap | 10.00/10 | Built-in distribution extension |
| dekube-indexer-secret | 10.00/10 | Built-in distribution extension |
| dekube-indexer-pvc | 10.00/10 | Built-in distribution extension |
| dekube-rewriter-haproxy | 10.00/10 | Built-in distribution extension |
| dekube-provider-caddy | 9.88/10 | Built-in distribution extension |
| dekube-transform-fix-permissions | 9.86/10 | Built-in distribution extension |
| dekube-manager | 9.74/10 | Style only (too-many-locals) |
| dekube-provider-simple-workload | 9.66/10 | Built-in distribution extension |
| dekube-engine | 9.58/10 | Style only (too-many-args, too-many-locals) |
| dekube-indexer-service | 9.23/10 | Built-in distribution extension |
Remaining warnings are accepted style issues (R0914 too-many-locals, R0913 too-many-arguments). E0401 (import-error) and R0903 (too-few-public-methods) are suppressed inline — the import resolves at runtime, and the one-class-one-method contract is by design.
Pyflakes¶
Zero warnings across all repos (the pre-existing f-string is missing placeholders in cli.py:90 is a false positive — the f-string is intentional). The inquisitors left in silence.
Radon (cyclomatic complexity)¶
| Repo | Worst function | CC | Rating |
|---|---|---|---|
| dekube-transform-fix-permissions | _collect_uids |
18 | C |
| dekube-manager | main |
17 | C |
| dekube-engine | build_service_port_map |
16 | C |
| dekube-engine | main (cli) |
16 | C |
| dekube-engine | convert |
16 | C |
| dekube-provider-simple-workload | SimpleWorkloadProvider._build_service |
13 | C |
| dekube-engine | _auto_register |
13 | C |
| dekube-indexer-pvc | PVCIndexer.convert |
12 | C |
| dekube-rewriter-haproxy | HAProxyRewriter.rewrite |
12 | C |
| dekube-transform-fix-permissions | FixPermissions (class) |
12 | C |
| dekube-engine | _resolve_env_entry |
11 | C |
| dekube-provider-simple-workload | _get_exposed_ports |
11 | C |
| dekube-provider-simple-workload | SimpleWorkloadProvider._convert_one |
11 | C |
| dekube-engine | _resolve_secret_keys |
11 | C |
| dekube-engine | convert_volume_mounts |
11 | C |
| dekube-provider-caddy | _write_caddy_host_block |
11 | C |
| dekube-transform-fix-permissions | FixPermissions.transform |
11 | C |
| dekube-manager | _read_yaml_config |
11 | C |
| dekube-manager | _install |
11 | C |
No D/E/F rated functions. dekube-manager's _read_yaml_config dropped from D(23) to C(11) after replacing the hand-rolled YAML parser with pyyaml. The remaining C-rated functions are natural dispatchers or sequential steps where splitting would move complexity without improving readability.
Average complexity & maintainability¶
| Repo | MI | MI rating | Avg CC | CC rating |
|---|---|---|---|---|
| dekube-indexer-service | 78.97 | A | — | A |
| dekube-indexer-configmap | 70.29 | A | 4.5 | A |
| dekube-indexer-secret | 70.29 | A | 4.5 | A |
| dekube-indexer-pvc | 67.76 | A | 9.7 | B |
| dekube-transform-fix-permissions | 60.33 | A | 8.0 | B |
| dekube-provider-caddy | 53.90 | A | 7.6 | B |
| dekube-rewriter-haproxy | 41.75 | A | 7.2 | B |
| dekube-provider-simple-workload | 40.96 | A | 7.6 | B |
| dekube-manager | 32.89 | A | 4.8 | A |
All repos are MI A-rated. dekube-engine is not listed here (19 modules, overall MI varies per module; see radon mi src/dekube/ -s for per-module scores — lowest is extensions.py at 37.56).
The uncomfortable truth¶
This project is, by every objective metric, well-structured code. The functions are short. The concerns are separated. The naming is consistent. The linters approve.
This is deeply unsettling.
The expectation — the hope, even — was that a tool this conceptually wrong would at least have the decency to be poorly written. That the code quality would serve as a warning label: "abandon all hope, ye who read this." Instead, the temple stands. The prayers work. The linters nod approvingly. And somewhere, a developer who understands what this project actually does stares at a pylint score of 9.55 and feels nothing but existential dread.
The code is clean. The architecture is sound. The concept remains an abomination. These things are not in conflict — they are in conspiracy.
The final horror is not that the ritual failed — it is that it succeeded, was reproducible, and scored well on peer review.
— Cultes des Goules, On Accidental Rigor (lamentably)
And then it got refactored¶
It wasn't enough for it to work. It wasn't enough for it to lint clean. It had to be maintainable.
v2.3.2 split the 1858-line monolith into 21 modules across three layers: pacts/ (the sacred contracts), core/ (the engine), io/ (the plumbing). A build script concatenates them back into a single file for distribution. Users see no change. The metrics see everything.
The split alone fixed MI — from 0.00 (C) to 68.38 (A). Then a cyclomatic complexity pass extracted helpers from every function rated CC 14+. The worst offender dropped from 18 to 16. Average CC went from 6.6 to 5.9. All 21 modules score MI A individually.
A project whose existence is an architectural crime now has an architecture page. With a dependency graph. And layer boundaries. And a section called "the sacred contracts."
They said: let us impose order upon the chaos, that future disciples may navigate the labyrinth without losing their minds. And so the tablet was broken into twenty-one fragments, each labeled and indexed. The labyrinth remained — but now it had signage.
— Book of Eibon, On the Cataloguing of Madness (we tried)
And then it got tests¶
IT HAS TESTS. IT HAS AN EXECUTIONER.
A regression executioner that downloads two versions of dekube-engine, runs every extension combo against a battery of edge-case manifests, and diffs the output. A torturer that produces O(n³) manifests. It cleans up after itself. It passes shellcheck.
A project that emulates a container orchestrator by flattening its output into a different container orchestrator, whose fake kube-apiserver lives on a personal GitHub account for plausible deniability — this project has automated regression testing. With CI. Weekly runs. Artifact uploads.
The linters approved. The complexity metrics nodded. And now the executioner confirms: the rituals produce identical output every time. The temple is no longer merely structurally sound — it is under continuous inspection.
And then it divided by zero¶
v3.0.0 split the repo in two. dekube-engine (the bare engine) and helmfile2compose (the distribution). Two repos. Two build scripts. Two release workflows. Two READMEs. A sys.modules injection hack so runtime-loaded extensions can import from a namespace that technically doesn't exist in the concatenated file. An _auto_register() that scans globals for classes, deduplicates kind claims, and populates registries that were deliberately left empty.
The output is identical to v2.3.1.
Not "similar." Not "equivalent." Identical. The executioner diffs every byte across every extension combo and confirms: the split changed nothing about what the tool produces. It changed everything about how it's organized. dekube-engine's MI went from 68.38 to 74.07 — not because any function improved, but because workloads.py and haproxy.py left. The core got lighter by becoming less. The distribution got heavier by becoming more. The sum is greater than the whole was.
Two repos. Fourteen files. Zero functional change. The metrics improved. The architecture is now a dependency graph with layer boundaries and a section called "the sacred contracts." There is a page called Distributions that explains the Kubernetes distribution model as applied to a tool whose sole purpose is to flee Kubernetes.
The linters approved. The executioner confirmed. The architect looked at what he had built to harness the temple's power and recognized its floor plan.
The cartographer, having mapped every corridor of the labyrinth, burned the labyrinth and built a new one from the ashes. It was smaller. It was cleaner. It was the same labyrinth. The map still worked.
— Unaussprechlichen Kulten, On the Conservation of Complexity (unfortunately²)
The null that devours silently¶
The one bug that keeps coming back. The one we fixed thirty times and found again.
Helm charts with conditional {{ if }} blocks render absent fields as explicit null. Python's .get("key", {}) returns the default only when the key is absent. When the key exists with value None — which is what YAML null becomes — .get() returns None, not your carefully chosen fallback. Your code receives the Void wearing absence as a mask.
# WRONG — returns None when key exists with null value
annotations = manifest.get("metadata", {}).get("annotations", {})
# RIGHT — coalesces None to empty dict
annotations = manifest.get("metadata", {}).get("annotations") or {}
Every field that Helm may render as null is a trap: annotations, ports, initContainers, securityContext, data, stringData, rules, selector. Use or {} / or [] on all of them. v2.3.1 fixed 30+ instances across dekube-engine, nginx, and traefik. v1.3.3 found four more that had survived. The null finds passages we didn't know existed.
Running locally¶
# Extensions (single-file repos)
pylint <script>.py
pyflakes <script>.py
radon cc <script>.py -a -s -n C
# dekube-engine (package)
cd dekube-engine
PYTHONPATH=src pylint src/dekube/
PYTHONPATH=src pyflakes src/dekube/
PYTHONPATH=src radon cc src/dekube/ -a -s -n C # CC hotspots
PYTHONPATH=src radon mi src/dekube/ -s # maintainability index
For dekube-engine specifically, the CLAUDE.md in the repo root documents the complexity targets and lint workflow.
Note: For distribution extensions (bundled in the distribution's extensions/ directory), run radon per-extension file — not on the concatenated distribution artifact.