Bosun
All posts

Parity - Capturing intent between COBOL and Rust

Replace COBOL with Rust without losing the behavior your team depends on.

, by Timon Vonk

Replacing an old system is risky. The new one can look right while getting a small detail wrong. It might save an order twice. It might forget to send an update that another system needs.

LLMs rapidly accelerate software development. Code looks convincing, and it will happily tell you it implemented all requirements to the letter. Engineers need to go through great depths to verify the initial intent is correctly represented in the change, with no regressions. For modernization, this poses a much harder problem, already present before AI assisted engineering. Likely, we did not write the original code, business rules are not clearly expressed nor well known, and the risk of modernization and mistakes is very real.

Teams learn small rules over years of running a system. Many of those rules never reach the prompt.

We are building Parity inside Bosun. It runs the same scenario against the old and new systems, then automatically records behavior. Network, database, file system, responses, are all captured and compared. Only explicit change is allowed. Similar to differential testing, and then automatic.

The language is purpose build to live inside markdown specifications, and read like a testing scenario, with no added fuzz. This makes it easy to review for humans, and easy to operate on for LLMs.

Parity is a development preview. In this post we are going to explore how Parity works and helps by modernizing a COBOL service to Rust. The example was made for this experiment. Both services are working programs.

You can read the complete COBOL-to-Rust example.

Specifying our first scenario

We will start with a specification of what the service does, in OpenSpec format.

The service lets a customer reserve stock. The scenario says:

- **Given** SKU `book` has 3 available units and none reserved
- **When** 2 units are reserved
- **Then** the request succeeds, 1 unit remains, and one reservation event is appended

These lines give the team and the LLM the same job. They describe the result without telling Rust how to produce it.

The same Markdown contains the Parity check:

The reservation scenario
proof inventory.reserve {
example available { sku = "book", quantity = "2" }
when reserve(sku, quantity)
then {
assert (result.status == 201 &&
result.body.sku == examples.sku &&
result.body.available == 3 &&
result.body.reserved == 2 &&
result.body.remaining == 1)
assert views.inventory == {
"added": [{"sku": "book", "available": 3, "reserved": 2, "remaining": 1}],
"removed": [{"sku": "book", "available": 3, "reserved": 0, "remaining": 3}]
}
assert (views.events.added == [] &&
views.events.removed == [] &&
views.events.modified.size() == 1 &&
views.events.modified[0].path == "reservations.tsv" &&
views.events.modified[0].before.content == "" &&
views.events.modified[0].after.content == "reserved|book|2|2|1\n")
assert ((diff.path == ["views", "filesystem", "added", 0, "resource"] ||
diff.path == ["views", "filesystem_state", "added", 0, "resource"]) &&
diff.legacy.endsWith("/legacy/reservations.tsv") &&
diff.target.endsWith("/target/reservations.tsv"))
assert (diff.path == ["views", "postgres", "added", 0, "request", 2, "target"] &&
diff.legacy == "portal" && diff.target == "statement")
}
}

Parity runs the scenario against both services. It checks what the customer sees, the saved stock, the database work, and the reservation event. The Rust service passes:

COBOL and Rust match
$ parity verify --scenario inventory.reserve --verbose
PASS inventory.reserve [available] | MATCH reserve.result, views.events, views.filesystem, +4 more
Summary: 1 passed, 0 failed, 0 errors, 0 skipped

The first three assertions state what both services must do. The last two name differences Parity must see between them. Parity fails if one is missing or another value changes.

Detect a racing issue

The COBOL service checks the stock and reserves it in one database operation:

COBOL: reserve only when stock is available
STRING
"WITH updated AS ("
"UPDATE inventory SET reserved = reserved + $2 "
"WHERE sku = $1 AND available - reserved >= $2 "
"RETURNING sku, available, reserved, available - reserved AS remaining"
") SELECT TRUE AS reserved_now, sku, available, reserved, remaining "
"FROM updated UNION ALL "
"SELECT FALSE AS reserved_now, sku, available, reserved, "
"available - reserved FROM inventory "
"WHERE sku = $1 AND NOT EXISTS (SELECT 1 FROM updated)"
X"00"
DELIMITED BY SIZE INTO WS-SQL-COMMAND
END-STRING

PostgreSQL can accept one request and reject the other when two customers try to reserve the last item at the same time. If this is not properly reflected in the new code, that would be a serious bug.

On purpose, let’s change the Rust request such that the query and update are separate queries.

Rust experiment: split the database operation
let Some(current) = self.find(sku).await? else {
return Ok(None);
};
if current.available() - current.reserved() < quantity.get() {
return Ok(Some(ReservationOutcome::Insufficient(current)));
}
let row = self.client
.query_typed_one(UPDATE_INVENTORY, parameters)
.await?;

The request still returned 201, left one item, and wrote the event. A test that checked data or response would pass.

Parity failed. This is the focused part of the output:

Changed database operation
$ parity verify --scenario inventory.reserve --verbose
FAIL inventory.reserve [available]
MISMATCH (auto) views.postgres POSTGRES connection:database
request.0.sql:
- WITH updated AS (UPDATE inventory SET reserved = reserved + $2 ...)
+ SELECT sku, available, reserved, available - reserved AS remaining ...
...
Summary: 0 passed, 1 failed, 0 errors, 0 skipped

Parity automatically observes that Rust makes two queries as opposed to COBOL.

Capture missing side-effects

A successful reservation writes a file as a side-effect. I removed that write in the target and ran the scenario again.

Captured a missing side-effect
$ parity verify --scenario inventory.reserve --verbose
FAIL inventory.reserve [available]
MISMATCH views.events/modified/0
MISMATCH (auto) views.filesystem_state FILE_EDIT reservations.tsv
Summary: 0 passed, 1 failed, 0 errors, 0 skipped

Parity correctly failed because the target forgot to change a file that we did not.

Multiple examples and fuzzing

A small set of examples can miss bad inputs. Another scenario describes what the service should do with one:

  • Given a reservation path contains an invalid SKU or quantity
  • When the reservation is requested
  • Then the request is rejected without changing inventory or appending an event

The check has four named examples. It also generates four inputs (fuzzing). Fuzzing allows us to capture behavior we did not anticipate. Simply put, if our program adds one and outputs the result of x + 1, running it many times gives confidence that it works as advertised.

Verifying many examples
proof inventory.invalid-reservation {
example uppercase-sku { sku = "BOOK", quantity = "1" }
example zero-quantity { sku = "book", quantity = "0" }
example non-numeric-quantity { sku = "book", quantity = "two" }
example excessive-quantity { sku = "book", quantity = "10000" }
generate 4 cases {
sku = text matching "[a-z]{1,12}"
quantity = text matching "[1-9][0-9]{4}"
}
when reserve(sku, quantity)
then {
assert (result.status == 400 && result.body.error == "invalid_request")
assert views.inventory == {"added": [], "removed": []}
assert views.events == {"added": [], "removed": [], "modified": []}
}
}

When run, it will verify each example and generated case:

Eight invalid requests match
$ parity verify --scenario inventory.invalid-reservation --seed 0 --verbose
PASS inventory.invalid-reservation (8 cases) | MATCH reserve.result, views.events, views.inventory
PASS generated case 1
PASS generated case 2
PASS generated case 3
PASS generated case 4

The full demo covers a reservation, bad inputs, missing stock, too little stock, and an inventory read:

You decide what may change

Parity requires every captured value to match by default, even if omitted. Any difference between legacy and target has to be explicit. This requires no boilerplate and can live directly inside Markdown. This makes it easier to review up front, and verify after implementation.

The Rust service uses its own modules, types, and libraries. Parity compares the behavior it records without forcing Rust to copy the shape of the COBOL code.

Parity inside Bosun

Bosun guides the migration from assessment to pull request, bit-by-bit. Every pull request will have its intent defined and verified, helping with with review, testing, and verification.

An LLM can change the code and run the checks. A reviewer sees each difference before accepting it. The review now includes results from both programs for the scenarios the team chose.

Each scenario gives the reviewer evidence to decide whether the replacement did the job the team gave it.

Parity is a development preview. The checks in this article work today. We are still developing how teams use Parity across larger migrations.

If you are planning a legacy modernization and want to see what Parity finds in one of your user journeys, request a Bosun demo.