diff --git a/lib/interscript/equivalence.rb b/lib/interscript/equivalence.rb new file mode 100644 index 00000000..6fbcab68 --- /dev/null +++ b/lib/interscript/equivalence.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Interscript + # A3 (WO59): mechanical equivalence checking between two systems. + # + # Probes a sample set through both systems and reports the agreement + # ratio plus the minimal distinguishing samples. The sample set is + # caller-supplied or derived from the systems' own embedded tests. + class Equivalence + def self.check(first, second, samples:) + new(first, second).check(samples) + end + + # Embedded test inputs of the given systems (the natural probe set + # for near-equivalent authorities). + def self.samples_from_tests(*codes) + codes.flat_map do |code| + Array(Interscript.parse(code).tests&.data).map(&:first).compact + end.uniq + end + + def initialize(first, second) + @first = first + @second = second + end + + def check(samples) + distinguishing = [] + tested = 0 + matched = 0 + samples.each do |sample| + tested += 1 + a = Interscript.transliterate(@first, sample) + b = Interscript.transliterate(@second, sample) + if a == b + matched += 1 + else + distinguishing << { sample: sample, first: a, second: b } + end + end + ratio = tested.zero? ? 1.0 : matched.fdiv(tested) + { tested: tested, matched: matched, ratio: ratio, + distinguishing: distinguishing } + end + end +end diff --git a/spec/interscript/equivalence_spec.rb b/spec/interscript/equivalence_spec.rb new file mode 100644 index 00000000..6263c0ee --- /dev/null +++ b/spec/interscript/equivalence_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require "json" +require "tmpdir" +require "interscript/equivalence" + +RSpec.describe Interscript::Equivalence do + def write_map(dir, name, body) + path = File.join(dir, "#{name}.isc") + File.write(path, body) + path + end + + def identical_pair(dir) + body = <<~ISC + system "TST:xx-Latn:Latn:1" { + metadata { + authority_id tst + id 1 + language iso-639-2:xx + source_script Latn + destination_script Latn + } + tests { "ab" -> "ab" } + stage main { parallel { sub "a" "a" } } + } + ISC + [write_map(dir, "tst-a-1", body), write_map(dir, "tst-b-1", body.gsub("tst-a-1", "tst-b-1"))] + end + + it "reports full agreement for identical systems" do + Dir.mktmpdir do |dir| + a, b = identical_pair(dir) + Interscript.load_path.unshift(dir) + begin + result = described_class.check("tst-a-1", "tst-b-1", samples: ["ab", "abc", "b"]) + expect(result[:ratio]).to eq(1.0) + expect(result[:distinguishing]).to be_empty + ensure + Interscript.load_path.delete(dir) + end + end + end + + it "finds the minimal distinguishing sample for divergent systems" do + Dir.mktmpdir do |dir| + body_a = <<~ISC + system "TST:xx-Latn:Latn:1" { + metadata { + authority_id tst + id 1 + language iso-639-2:xx + source_script Latn + destination_script Latn + } + stage main { parallel { sub "a" "x" } } + } + ISC + body_b = body_a.gsub('sub "a" "x"', 'sub "a" "y"').gsub("id 1", "id 2") + write_map(dir, "tst-c-1", body_a) + write_map(dir, "tst-c-2", body_b) + Interscript.load_path.unshift(dir) + begin + result = described_class.check("tst-c-1", "tst-c-2", samples: ["a", "b"]) + expect(result[:ratio]).to eq(0.5) + expect(result[:distinguishing]).to eq([{ sample: "a", first: "x", second: "y" }]) + ensure + Interscript.load_path.delete(dir) + end + end + end +end diff --git a/spec/isc/semantics_spec.rb b/spec/isc/semantics_spec.rb new file mode 100644 index 00000000..fc01075d --- /dev/null +++ b/spec/isc/semantics_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +# WO80/A4 — normative rulings for the ISC semantic surface. Each vector +# pins the reference behavior; IS-1 v2 documents these as normative +# sections (the spec text accretes one section per ruled point). + +require "tmpdir" + +RSpec.describe "ISC semantics — parallel composition" do + def with_map(name, body) + dir = Dir.mktmpdir + File.write(File.join(dir, name), body) + Interscript.load_path.unshift(dir) + yield + ensure + Interscript.load_path.delete(dir) if dir + end + + it "rules: a parallel block is single-pass longest-match" do + with_map("tst-long-1.isc", <<~ISC) do + system "TST:xx-Latn:Latn:1" { + metadata { + authority_id tst + id 1 + language iso-639-2:xx + source_script Latn + destination_script Latn + } + stage main { parallel { + sub "b" "Y" + sub "ab" "X" + } } + } + ISC + expect(Interscript.transliterate("tst-long-1", "ab")).to eq("X") + end + end + + it "rules: duplicate rules in a parallel resolve last-declared-wins" do + with_map("tst-tie-1.isc", <<~ISC) do + system "TST:xx-Latn:Latn:2" { + metadata { + authority_id tst + id 2 + language iso-639-2:xx + source_script Latn + destination_script Latn + } + stage main { parallel { + sub "a" "1" + sub "a" "2" + } } + } + ISC + expect(Interscript.transliterate("tst-tie-1", "a")).to eq("2") + end + end +end