ADR-1125: Reconciling seven independent vmafx-tune Go ports into one tree¶
- Status: Accepted
- Date: 2026-08-30
- Deciders: Lusoris
- Tags:
go,vmafx-tune,python-sunset,integration,fork-local
Context¶
The remaining fourteen vmaf-tune subcommands were ported to Go by seven agent groups working in parallel, each in its own worktree branched from the same commit. Every group's work is individually sound: all seven build, vet and pass their own tests in isolation.
They were not, however, written against each other. Six packages were invented independently by two or three groups at once, under the same import path:
| Package | Groups | Sizes (LOC) |
|---|---|---|
pkg/codecadapter | 3, 4, 6 | 1822 / 877 / 1074 |
pkg/pershot | 1, 6 | 1887 / 776 |
pkg/predictor | 3, 6 | 484 / 2325 |
pkg/conformal | 2, 6 | 1410 / 479 |
pkg/scorebackend | 1, 2 | 516 / 770 |
internal/pyjson | 4, 6 | 629 / 632 |
Some collisions are invisible to git. Groups 3 and 6 both defined pkg/codecadapter but in differently-named files (adapter.go vs codecadapter.go), so the merge reported no conflict and produced a package with two Adapter types. Only the compiler surfaced it.
Six of the seven groups also edit the same three accumulator files — cmd/vmafx-tune/cmd/root.go, its test, and docs/usage/vmafx-tune-go.md — each registering its own subcommands and shortening the "not yet ported" list.
Decision¶
Integrate onto one branch by merging group branches sequentially, resolving each collision against evidence rather than by merge order, and letting go build / go test gate every step.
One implementation per package, chosen on evidence — not on size:
| Package | Kept | Why |
|---|---|---|
pkg/codecadapter | group 6 | Both registries register the identical 19 codecs, verified by enumerating Known(), so the interface-vs-struct choice costs no coverage. Group 3's per-codec method interface converts mechanically to group 6's struct fields. This tiebreaker was not sufficient — see Consequences. |
pkg/pershot | group 1 | Superset; carries the byte-identical plan_json emitter verified against CPython across all ten supported codecs. |
pkg/predictor | group 6 | Superset (adds features.go). Group 3's Clamp helper was carried across. |
pkg/conformal | group 2 | Superset: adds CVPlusCalibration, the Calibration interface, load/save and StaleCalibrationError. |
pkg/scorebackend | group 2 | Superset. |
JSON encoding is the messiest case, and this ADR originally described it wrong. It claimed there were two CPython-JSON implementations, kept deliberately. There are four, at import paths that never collided in git, totalling ~2,641 lines:
| Package | LOC | Author | Consumers |
|---|---|---|---|
pkg/pyjson | 723 | group 3 | pkg/corpus/{corpus,encode,score,jsonl}.go |
internal/pyjson | 632 | group 6 | four cmd/ files, pkg/corpusrow |
internal/pyjsonstrict | 641 | group 4 | pkg/benchmark, pkg/encodeprofile, cmd/encodeprofile |
pkg/tune/pyjson | 645 | group 5 | cmd/sidecar, pkg/tune/{auto,executor,sidecar} |
They are redundant rather than divergent, which was measured rather than assumed: 200,000 random finite float64 (arbitrary bit patterns, the 1e16 and 1e-4 exponent thresholds, subnormals, -0.0) produced zero disagreements across all four, and 10,000 sampled renderings matched CPython's repr() and json.dumps() exactly. Consolidation is therefore behaviour-preserving, and is left as follow-up rather than bolted onto an already-large change.
Where APIs disagreed, the receiving package grew the missing seam rather than the consumer being rewritten around it — WithAlpha and IntervalFor on conformal.SplitCalibration, package-level ResolveCodecArgs / DefaultPreset / LegacyCodecArgs on codecadapter, Clamp on predictor. Each carries a comment naming the merge as its origin.
Two behavioural details had to be preserved explicitly:
- The package-level
codecadapter.ResolveCodecArgsvalidates the preset before building argv; the(*Adapter)method deliberately does not. Group 4's contract (and its test) treats an out-of-vocabulary preset as an error, while group 6's method is the low-level token builder. Both are now true. exitCodeErrorexisted twice with identical fields but different receiver kinds. It is unified on the value receiver and gainedExitCode(), so bothexitCodeError{...}and&exitCodeError{...}satisfyexitCoder, andexitCodeOfcomposes as the final fallback after the interface andfastExitCodechecks. No group's exit contract was dropped.
Alternatives considered¶
| Option | Pros | Cons | Why not chosen |
|---|---|---|---|
| Seven separate PRs, one per group | Small, independently reviewable | The duplicate packages collide no matter the order; whoever merges second does this same reconciliation without the other six branches in hand, six times over | Rejected — moves the work later and does it worse |
| Keep the largest implementation of each package | Trivial rule, no judgement | Size is not correctness: group 3's codecadapter is the largest and has the least Python-parity evidence | Rejected — the tiebreaker has to be evidence |
| Merge by branch order and let last-writer-win | Zero decisions | Silently drops capability; the group-3/group-6 collision produced no git conflict at all, so "last writer" would not even have been visible | Rejected outright |
Namespace every group's package (codecadapter3, codecadapter6) | No reconciliation needed | Ships two registries of the same 19 codecs that will drift apart | Rejected — duplication with a deadline |
| Collapse both pyjson encoders into one | One JSON package | The two mirror different Python functions with different non-finite handling; one package would answer to two contracts | Rejected for pyjson specifically, accepted everywhere else |
Consequences¶
The codecadapter tiebreaker verified the wrong property. Comparing the two registries' codec names showed them identical and was taken as evidence of equivalence. It is not: the kept group-6 registry had InvertQuality set to true for all four VideoToolbox codecs, where the Python adapters set invert_quality=False (a VideoToolbox -q:v is higher-value-is-higher-quality, unlike every CRF/CQ/QP codec) — and the deleted group-3 registry had it right. Two argv divergences survived the same way: ProRes tier 5 emitted 4444xq instead of xq, and libx265 fell back to ffmpeg's generic -pass/-passlogfile instead of -x265-params. All three are fixed, and pkg/codecadapter/python_argv_parity_test.go now pins the emitted argv for every (codec, preset, quality) triple against the Python adapters, which is the comparison that should have decided this in the first place.
Positive. All fourteen subcommands land together, with one implementation of each shared package and the vmaf-tune Python CLI fully shadowed — root.go no longer registers a single redirect stub, and the stub machinery is deleted. TestStubSubcommands (which asserted stubs still existed) is inverted into TestNoStubSubcommandsRemain, so re-introducing a stub now fails a test.
Negative. This is a 228-file, ~112k-insertion change. It is large because the reconciliation is only correct when done with every branch present; splitting it would mean performing the same merge repeatedly against partial information.
Integration surfaced three latent defects that each group's own suite could not see:
.gitignorecarried a barecorpus.jsonlpattern (intended for Phase A scratch output) that matches at any depth, sopkg/benchmark/testdata/corpus.jsonlwas silently excluded from its own commit. Fixed with a!**/testdata/corpus.jsonlnegation..gitattributes* text=autonormalised the CRLF out of the benchmark CSV golden fixtures. Python'scsvmodule writes CRLF, so the committed goldens stopped matching the renderer — invisible in the authoring worktree, which still held the pre-normalisation bytes, and red on any fresh checkout. Fixed by exemptingpkg/benchmark/testdata/*.csvfrom normalisation.pkg/libvmaf's cgoLDFLAGSnames-L${SRCDIR}/../../core/build-cpu/src. When that directory does not exist — any checkout that has not built the C library — the linker does not fail; it silently falls through to a distro-installedlibvmaf. On this workstation that is upstream 3.2.0 with zerovmaf_dnn_*symbols, so a Go binary can link against a library that is not this fork at all. Recorded here; the fix (failing closed when the fork's libvmaf is absent) is deliberately left out of this PR's scope.
The third item was resolved on 2026-09-26. pkg/libvmaf no longer supplies an implicit #cgo LDFLAGS value. Make, Go CI, and each cgo container build now select the verified fork library explicitly; an unqualified go test fails at link time instead of searching a system libvmaf. The source-level workflow contract pins every required caller so a new build surface cannot silently reintroduce the fallback.
References¶
req— user direction 2026-08-30: continue the Go migration and get local-only work to remote so the repo "gets cleaner not worse".- ADR-1124 — the group-1 per-shot port this integrates.
- ADR-0221 — changelog/ADR fragment pattern the seven groups' fragments follow.
docs/research/vmafx-tune-go-fast-2026-08-30.md— the group-2 research digest.