From 80a6a28e16b53389c0a402f453725457c28d0618 Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Sat, 24 Oct 2026 16:45:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(A3):=20Interscript=20equivalence=20checkin?= =?UTF-8?q?g=20=E2=80=94=20the=20python=20port=20of=20the=20ruby=20contrac?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit check(first_source, second_source, samples) -> {tested, matched, ratio, distinguishing}, probing through the real ISC engine (isc_to_tree -> Engine). Mirrors ruby lib/interscript/equivalence.rb and the TS port (interscript-ts src/equivalence.ts) — one contract, three runtimes. --- src/interscript/equivalence.py | 34 ++++++++++++++++++++++++ tests/test_equivalence.py | 48 ++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 src/interscript/equivalence.py create mode 100644 tests/test_equivalence.py diff --git a/src/interscript/equivalence.py b/src/interscript/equivalence.py new file mode 100644 index 0000000..d422ab8 --- /dev/null +++ b/src/interscript/equivalence.py @@ -0,0 +1,34 @@ +"""A3 (WO59): mechanical equivalence checking between two systems. + +Mirrors the ruby Interscript::Equivalence contract: probe a sample set +through both systems, report the agreement ratio plus the distinguishing +samples. +""" + +from __future__ import annotations + +from .engine import Engine +from .isc import isc_to_tree + + +def check(first_source: str, second_source: str, samples: list[str]) -> dict: + first = Engine(isc_to_tree(first_source)) + second = Engine(isc_to_tree(second_source)) + distinguishing = [] + tested = 0 + matched = 0 + for sample in samples: + tested += 1 + a = first.transliterate(sample) + b = second.transliterate(sample) + if a == b: + matched += 1 + else: + distinguishing.append({"sample": sample, "first": a, "second": b}) + ratio = 1.0 if tested == 0 else matched / tested + return { + "tested": tested, + "matched": matched, + "ratio": ratio, + "distinguishing": distinguishing, + } diff --git a/tests/test_equivalence.py b/tests/test_equivalence.py new file mode 100644 index 0000000..6ce6ccf --- /dev/null +++ b/tests/test_equivalence.py @@ -0,0 +1,48 @@ +import pytest + +from interscript.engine import Engine +from interscript.equivalence import check +from interscript.isc import isc_to_tree + +A = ( + 'system "TSTA:xx-Latn:Latn:1" {\n' + ' stage main { parallel {\n sub "a" "x"\n } }\n' + "}\n" +) +B = ( + 'system "TSTB:xx-Latn:Latn:2" {\n' + ' stage main { parallel {\n sub "a" "y"\n } }\n' + "}\n" +) +B_EQUIV = ( + 'system "TSTB2:xx-Latn:Latn:3" {\n' + ' stage main { parallel {\n sub "a" "x"\n } }\n' + "}\n" +) + + +def test_equivalent_systems_report_ratio_one(): + r = check(A, B_EQUIV, ["ba", "a", "zzz"]) + assert r["tested"] == 3 + assert r["matched"] == 3 + assert r["ratio"] == 1.0 + assert r["distinguishing"] == [] + + +def test_distinguishing_samples_are_collected(): + r = check(A, B, ["zzz", "ba"]) + assert r["tested"] == 2 + assert r["matched"] == 1 + assert r["ratio"] == 0.5 + assert r["distinguishing"] == [ + {"sample": "ba", "first": "bx", "second": "by"} + ] + + +def test_empty_sample_set_is_vacuously_equivalent(): + assert check(A, B, [])["ratio"] == 1.0 + + +@pytest.mark.parametrize("src", [A, B, B_EQUIV]) +def test_sources_parse(src): + Engine(isc_to_tree(src))