Case 324

Each stage keeps a different invariant — 3 of 3 checks pass, 1 of 4 invariants survives

each_stage_keeps_a_different_invariant.eml runs three stages that each name and keep one invariant, then evaluates four invariants at every stage boundary.

ok: true — round-trip fixpoint reached (python1 == python2)updated 2026-08-10

EML

eml
# Self-authored for the EML case corpus (no external origin). Three stages,
# one named invariant each. Every stage keeps the one it named. By the end of
# the pipeline, one invariant out of four is still standing - and it is one
# that no stage ever claimed.
#
# This is not the same failure as a stage being wrong. Each stage is right,
# and right about the thing it was asked to be right about. Fill-in owns "row
# count unchanged". Zero-dropping owns "total unchanged" - and it genuinely
# does not change a total, because the rows it removes contribute nothing.
# Key-merging owns "keys unique". Three true statements.
#
# What nobody owns is the conjunction. The stage checks are chosen by whoever
# wrote each stage, from inside that stage, and the property they happen not to
# mention is exactly the one the next stage is free to break.
#
# The measurement is a grid: every invariant evaluated at every stage boundary,
# so which stage breaks which is read off the run rather than declared. The
# end-to-end row is the one that matters and it is the row nobody's check
# corresponds to.
#
# A wrong premise, kept. The first version folded "the key sequence is
# identical" and "relative order is preserved" into one column, which reports a
# REMOVED ROW as a REORDERING - a length change tripping a check about order.
# Split apart, the grid says something the conflated version could not: three
# invariants die and relative order survives all three stages. The survivor is
# the one nobody wrote a check for, which is the opposite of the moral you would
# guess.

def fill_missing(rows):
    [] => out
    for r in rows:
        if r[1] == 0 - 1:
            out + [[r[0], 10]] => out
        else:
            out + [r] => out
    return out

def drop_zero_rows(rows):
    [] => out
    for r in rows:
        if r[1] == 0:
            pass
        else:
            out + [r] => out
    return out

def merge_duplicate_keys(rows):
    [] => out
    for r in rows:
        0 => found
        [] => nxt
        for o in out:
            if o[0] == r[0]:
                nxt + [[o[0], o[1] + r[1]]] => nxt
                1 => found
            else:
                nxt + [o] => nxt
        if found == 0:
            nxt + [r] => nxt
        nxt => out
    return out

def total(rows):
    0 => t
    for r in rows:
        t + r[1] => t
    return t

def keys_of(rows):
    [] => ks
    for r in rows:
        ks + [r[0]] => ks
    return ks

def keys_unique(rows):
    [] => seen
    1 => ok
    for r in rows:
        if r[0] in seen:
            0 => ok
        seen + [r[0]] => seen
    return ok

def key_seq_same(before, after):
    keys_of(before) => a
    keys_of(after) => b
    if len(a) != len(b):
        return 0
    for i in [0:len(a) - 1]:
        if a[i] != b[i]:
            return 0
    return 1

# Relative order is a DIFFERENT invariant from an identical key sequence, and
# folding them into one column would report a removed row as a reordering.
def rel_order_kept(before, after):
    keys_of(before) => a
    keys_of(after) => b
    0 => i
    for k in b:
        0 => found
        while i < len(a):
            if a[i] == k:
                1 => found
                i + 1 => i
                break
            i + 1 => i
        if found == 0:
            return 0
    return 1

def yn(v):
    if v == 1:
        return "yes"
    return "no "

def verdict(before, after):
    if before == after:
        return "kept"
    return "BROKE (" + str(before) + " -> " + str(after) + ")"

[["ann", 5], ["bo", 0 - 1], ["cy", 0], ["ann", 7], ["dee", 3], ["bo", 0]] => rows

# ---- each stage's own check ----

fill_missing(rows) => s1
drop_zero_rows(s1) => s2
merge_duplicate_keys(s2) => s3

"each stage's own check, the one its author wrote" ^0
"  fill_missing  says row count unchanged : " + yn(len(s1) == len(rows)) ^0
"  drop_zeros    says total unchanged     : " + yn(total(s2) == total(s1)) ^0
"  merge_keys    says keys unique         : " + yn(keys_unique(s3)) ^0
"" ^0

# ---- the grid: every invariant at every boundary ----

"grid: each invariant measured against the ORIGINAL input at each boundary" ^0
"  boundary        count  total  key-seq  rel-order" ^0
"  after fill    :  " + yn(len(s1) == len(rows)) + "    " + yn(total(s1) == total(rows)) + "    " + yn(key_seq_same(rows, s1)) + "      " + yn(rel_order_kept(rows, s1)) ^0
"  after drop    :  " + yn(len(s2) == len(rows)) + "    " + yn(total(s2) == total(rows)) + "    " + yn(key_seq_same(rows, s2)) + "      " + yn(rel_order_kept(rows, s2)) ^0
"  after merge   :  " + yn(len(s3) == len(rows)) + "    " + yn(total(s3) == total(rows)) + "    " + yn(key_seq_same(rows, s3)) + "      " + yn(rel_order_kept(rows, s3)) ^0
"" ^0

0 => surviving
if len(s3) == len(rows):
    surviving + 1 => surviving
if total(s3) == total(rows):
    surviving + 1 => surviving
if key_seq_same(rows, s3) == 1:
    surviving + 1 => surviving
if rel_order_kept(rows, s3) == 1:
    surviving + 1 => surviving

"invariants that survive end to end: " + str(surviving) + " of 4" ^0
"stage checks that passed:            3 of 3" ^0
"" ^0

# ---- the numbers, so the reader can see it is not a rounding story ----

"rows  in " + str(len(rows)) + " -> out " + str(len(s3)) ^0
"total in " + str(total(rows)) + " -> out " + str(total(s3)) ^0
"keys  in " + repr(keys_of(rows)) ^0
"keys  out " + repr(keys_of(s3)) ^0
"" ^0

# ---- which stage broke which, measured ----

"each stage against an invariant it never named" ^0
"  fill  vs total     : " + verdict(total(rows), total(s1)) ^0
"  drop  vs row count : " + verdict(len(s1), len(s2)) ^0
"  merge vs row count : " + verdict(len(s2), len(s3)) ^0
"" ^0
"Every stage kept the invariant it named and broke one it never mentioned." ^0
"That is the mechanism: a stage check is written from inside the stage, and" ^0
"the property it happens not to mention is the one the next stage is free to" ^0
"break." ^0

Python (deterministic transpilation)

python
def fill_missing(rows):
    out = []
    for r in rows:
        if r[1] == 0 - 1:
            out = out + [[r[0], 10]]
        else:
            out = out + [r]
    return out

def drop_zero_rows(rows):
    out = []
    for r in rows:
        if r[1] == 0:
            pass
        else:
            out = out + [r]
    return out

def merge_duplicate_keys(rows):
    out = []
    for r in rows:
        found = 0
        nxt = []
        for o in out:
            if o[0] == r[0]:
                nxt = nxt + [[o[0], o[1] + r[1]]]
                found = 1
            else:
                nxt = nxt + [o]
        if found == 0:
            nxt = nxt + [r]
        out = nxt
    return out

def total(rows):
    t = 0
    for r in rows:
        t = t + r[1]
    return t

def keys_of(rows):
    ks = []
    for r in rows:
        ks = ks + [r[0]]
    return ks

def keys_unique(rows):
    seen = []
    ok = 1
    for r in rows:
        if r[0] in seen:
            ok = 0
        seen = seen + [r[0]]
    return ok

def key_seq_same(before, after):
    a = keys_of(before)
    b = keys_of(after)
    if len(a) != len(b):
        return 0
    for i in range(0, len(a)):
        if a[i] != b[i]:
            return 0
    return 1

def rel_order_kept(before, after):
    a = keys_of(before)
    b = keys_of(after)
    i = 0
    for k in b:
        found = 0
        while i < len(a):
            if a[i] == k:
                found = 1
                i = i + 1
                break
            i = i + 1
        if found == 0:
            return 0
    return 1

def yn(v):
    if v == 1:
        return "yes"
    return "no "

def verdict(before, after):
    if before == after:
        return "kept"
    return "BROKE (" + str(before) + " -> " + str(after) + ")"

rows = [["ann", 5], ["bo", 0 - 1], ["cy", 0], ["ann", 7], ["dee", 3], ["bo", 0]]
s1 = fill_missing(rows)
s2 = drop_zero_rows(s1)
s3 = merge_duplicate_keys(s2)
print("each stage's own check, the one its author wrote")
print("  fill_missing  says row count unchanged : " + yn(len(s1) == len(rows)))
print("  drop_zeros    says total unchanged     : " + yn(total(s2) == total(s1)))
print("  merge_keys    says keys unique         : " + yn(keys_unique(s3)))
print("")
print("grid: each invariant measured against the ORIGINAL input at each boundary")
print("  boundary        count  total  key-seq  rel-order")
print("  after fill    :  " + yn(len(s1) == len(rows)) + "    " + yn(total(s1) == total(rows)) + "    " + yn(key_seq_same(rows, s1)) + "      " + yn(rel_order_kept(rows, s1)))
print("  after drop    :  " + yn(len(s2) == len(rows)) + "    " + yn(total(s2) == total(rows)) + "    " + yn(key_seq_same(rows, s2)) + "      " + yn(rel_order_kept(rows, s2)))
print("  after merge   :  " + yn(len(s3) == len(rows)) + "    " + yn(total(s3) == total(rows)) + "    " + yn(key_seq_same(rows, s3)) + "      " + yn(rel_order_kept(rows, s3)))
print("")
surviving = 0
if len(s3) == len(rows):
    surviving = surviving + 1
if total(s3) == total(rows):
    surviving = surviving + 1
if key_seq_same(rows, s3) == 1:
    surviving = surviving + 1
if rel_order_kept(rows, s3) == 1:
    surviving = surviving + 1
print("invariants that survive end to end: " + str(surviving) + " of 4")
print("stage checks that passed:            3 of 3")
print("")
print("rows  in " + str(len(rows)) + " -> out " + str(len(s3)))
print("total in " + str(total(rows)) + " -> out " + str(total(s3)))
print("keys  in " + repr(keys_of(rows)))
print("keys  out " + repr(keys_of(s3)))
print("")
print("each stage against an invariant it never named")
print("  fill  vs total     : " + verdict(total(rows), total(s1)))
print("  drop  vs row count : " + verdict(len(s1), len(s2)))
print("  merge vs row count : " + verdict(len(s2), len(s3)))
print("")
print("Every stage kept the invariant it named and broke one it never mentioned.")
print("That is the mechanism: a stage check is written from inside the stage, and")
print("the property it happens not to mention is the one the next stage is free to")
print("break.")

stdout (executed)

text
each stage's own check, the one its author wrote
  fill_missing  says row count unchanged : yes
  drop_zeros    says total unchanged     : yes
  merge_keys    says keys unique         : yes

grid: each invariant measured against the ORIGINAL input at each boundary
  boundary        count  total  key-seq  rel-order
  after fill    :  yes    no     yes      yes
  after drop    :  no     no     no       yes
  after merge   :  no     no     no       yes

invariants that survive end to end: 1 of 4
stage checks that passed:            3 of 3

rows  in 6 -> out 3
total in 14 -> out 25
keys  in ['ann', 'bo', 'cy', 'ann', 'dee', 'bo']
keys  out ['ann', 'bo', 'dee']

each stage against an invariant it never named
  fill  vs total     : BROKE (14 -> 25)
  drop  vs row count : BROKE (6 -> 4)
  merge vs row count : BROKE (4 -> 3)

Every stage kept the invariant it named and broke one it never mentioned.
That is the mechanism: a stage check is written from inside the stage, and
the property it happens not to mention is the one the next stage is free to
break.

Trace event types

eml:run:starteml:defeml:assigneml:calleml:returneml:outputeml:run:done