Topic · Cross-language port

How do I migrate from one language to another and guarantee the same behavior?

Gist

You cannot guarantee identical bytes from a rename. Proof fails the merge with proof audit --check mirror_complete when a ledger cell on the old language has no counterpart on the new one. Names can change. The cell cannot vanish.

proof audit --check mirror_complete

Keep the transpiler. Keep golden-master dumps. They do not ask whether malformed:negative still exists after Python became Rust.

01 · The renamed suite

A green Rust test is not the Python negative.

The port looks done. Every new test has a new name. The old malformed-input guard never made the jump.

The rewrite that already has a ledger, in one language or two, lives on characterization testing and mirrors. Two live services and a traffic split live on migrating a critical service. This page is the language pair: Python to Rust, Java to Kotlin, Go to something else. The test identifiers will not match. The cell key has to.

A Mirror is a contract that the target file-set reproduces the verification ledger of the source, for every requirement in scope. Cells are keyed by (requirement, dimension, discriminant), never by test name. A Python TestRejectMalformed and a Rust rejects_malformed are the same cell if they witness malformed:negative. Matching names is the hole the port opens.

// source, Python
// SYS-REQ-ocr:malformed:nominal
// SYS-REQ-ocr:malformed:negative   // TestRejectMalformed

// target, Rust
// SYS-REQ-ocr:malformed:nominal    // accepts_ok_payload
// malformed:negative               // absent
// cargo test: green

Declare both trees at proof/mirrors/MIR-*.yaml with source.name and target.name set (Python / Rust). Those names are labels. The parity check does not read them. proof audit --check mirror_complete fails when the negative exists on the source and not on the target, once the mirror status is complete.

02 · The exhibit

Same port. Two names, or one cell.

The Rust suite is green under new identifiers. The catalog still owes a negative. Click the tabs.

What the port matched

  • Python TestRejectMalformed
  • Rust rejects_malformed never written
cargo test green

The shall

When the payload is malformed, the decoder shall reject it and leave the store unchanged.

No negative on Rust

The names

Still different identifiers. Still not the key. A complete mirror does not read them.

Rename kept

Proof

  • Ask does malformed still have negative
  • Job audit --check mirror_complete
Merge blocked if the cell vanished

Two names, or one cell. Click the tabs.

Side Python Rust
Test name TestRejectMalformed not written
Cell ocr:malformed:negative absent
MC/DC row same FRETish truth-table row missing that row is a gap
Suite pytest green cargo test green
Move What it buys What it loses
Transpile / Babel A syntax rewrite. Useful when the languages are close. transpile is that SERP. It does not re-read the shall. Keep the compiler. It is not this bar.
Golden master Observed bytes from the old process, including bugs you meant to drop. A dump that records a wrong answer fails the port that finally corrects it. That job lives on characterization.
Canary / strangler Traffic on the new binary, or a facade that peels routes. Routing is not a cell key. The cutover H1 lives on migrating a critical service.
Proof Each source cell has a target counterpart, or a complete mirror stays red. Names are labels. Presence, not I/O. Jama still authors. Two shims are a different check.

The check reads the Mirror object, then the cells on each side. Test identifiers are not an input. MC/DC row parity is the in-between signal: witness rows come from the language-independent FRETish truth table, so requiring the same rows means both decision logics were exercised at the same condition combinations.

proof mirror add --id MIR-001 \
  --source "litellm/ocr/**,tests/**/ocr/**" \
  --target "litellm-rust/crates/providers/src/mistral/ocr/**" \
  --status in_progress

proof mirror status MIR-001
proof audit --check mirror_complete --verbose

// SYS-REQ-ocr:malformed:nominal
// SYS-REQ-ocr:malformed:negative

proof audit --check differential_conformance

mirror_complete proves the evidence cell exists on the target. It does not re-run the test. A mirrored test that fails is tests_pass, not a parity gap. in_progress gaps stay warnings. A complete mirror that later grows a source-only cell re-opens at error.

differential_conformance is the I/O check: one language-neutral input, two shims, compare envelopes. A transform with fewer than two complete shims is skipped as information. The differential dimension on the Mirror object itself is deferred. Never describe a completed mirror as “the Rust crate matches the Python package.” We have not run this gate against a frozen corpus of language ports next to a transpiler. The loss is named, not scored.

A GIL-specific race on Python can be an exemption with a reason and a reviewer, not a silent skip. Rust then owes its own Send / Sync coverage. That waiver is visible. It is not “the languages are different, so parity does not apply.”

03 · The honest loss

A complete mirror is not identical output.

If nobody declared a Mirror, mirror_complete is a no-op. The transpiler still has to exist.

Proof does not translate source. Babel still does, when the languages are close. Proof does not pin observed bytes. A golden master still does. Proof does not split traffic. A canary still does. mirror_complete does not prove the two implementations agree on an input. That is differential_conformance on a declared transform, with two complete shims. A Python-only or Rust-only shim is skipped as information.

Proof does not invent the shall. Jama still authors the programme. 100% MC/DC on the written decision still misses a partition that was never a condition. A completed mirror is not a finished port.

The rewrite H1 lives on characterization testing and mirrors. The cutover H1 lives on migrating a critical service. The empty-start H1 lives on replace a component with no specs. The schema H1 lives on database or API migration.

04 · Nearby questions

What people type next.

How do I rewrite a legacy system without introducing regressions? The campaign on a tree that already has a ledger, language-agnostic. Characterization.

We're migrating a critical service. How do I prove the new one matches the old one? Two live services, a traffic split. Service cutover.

How do I safely replace a legacy component that has no specs and no tests? The empty start. Replace with no specs.

Is there a way to check behavioral equivalence between old and new implementations? Presence is mirror_complete. Agreement on an input is differential_conformance. Both are named on this page. The cutover write-up lives on migrating a critical service.

What's the safest way to modernize legacy software that nobody fully understands? Do not invent the spec from the unread tree. Unknown-system modernize.