From efdfa39c15c9eb10104cda1a868d74ea8293d8f7 Mon Sep 17 00:00:00 2001 From: FABIOTESS Date: Wed, 19 Aug 2026 17:38:23 +0100 Subject: [PATCH] fix(export): address review findings on the confidence typing fix 1. An absurd magnitude expanded instead of being rejected. xsd:decimal has no exponent notation, so the value has to be written out in full, and "1e100000000" is eleven characters that expand to a hundred million digits. "1e100000" already produced a 100,001 character string here. The export path continues past validation errors, so one malformed field could exhaust memory. Values beyond MAX_CONFIDENCE_EXPONENT are now omitted like any other unusable value. 1e-9 still round-trips. 2. Decimal keeps the sign of zero, so 0.0 and -0.0 serialised as "0" and "-0", which are two distinct RDF terms. That is exactly the duplicate this PR exists to remove, so zero is normalised. 4 further tests. --- semantica/export/rdf_exporter.py | 17 +++++++++ .../export/test_confidence_literal_typing.py | 35 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/semantica/export/rdf_exporter.py b/semantica/export/rdf_exporter.py index ed1828bb..0e575ea6 100644 --- a/semantica/export/rdf_exporter.py +++ b/semantica/export/rdf_exporter.py @@ -63,6 +63,11 @@ DEFAULT_RELATION_TYPE = f"{SEMANTICA_NS}related_to" #: xsd:float is 32 bit binary, and cannot represent 0.9 or 0.95 at all. CONFIDENCE_DATATYPE = "http://www.w3.org/2001/XMLSchema#decimal" +#: Largest power of ten a confidence may carry. xsd:decimal has no exponent +#: notation, so a value has to be written out in full, and a compact literal +#: such as "1e100000000" would expand to a hundred million digits. +MAX_CONFIDENCE_EXPONENT = 100 + def normalize_confidence(value: Any) -> Optional[str]: """ @@ -95,11 +100,23 @@ def normalize_confidence(value: Any) -> Optional[str]: if not decimal_value.is_finite(): return None + # xsd:decimal has no exponent notation, so writing one means expanding it. + # "1e100000000" is eleven characters that expand to a hundred million, and + # the export path continues past validation errors, so a single malformed + # field could exhaust memory. Nothing near this magnitude is a confidence. + if not -MAX_CONFIDENCE_EXPONENT <= decimal_value.adjusted() <= MAX_CONFIDENCE_EXPONENT: + return None + # `str(Decimal("0.00001"))` gives "0.00001", but a float that has already # been through repr can arrive as "1e-05", which xsd:decimal does not allow. formatted = format(decimal_value, "f") if "." in formatted: formatted = formatted.rstrip("0").rstrip(".") or "0" + + # Decimal keeps the sign of zero, so 0.0 and -0.0 would serialise as two + # distinct RDF terms and defeat the point of a canonical form. + if formatted.lstrip("-").strip("0.") == "": + formatted = "0" return formatted diff --git a/tests/export/test_confidence_literal_typing.py b/tests/export/test_confidence_literal_typing.py index 082f947c..f61ed953 100644 --- a/tests/export/test_confidence_literal_typing.py +++ b/tests/export/test_confidence_literal_typing.py @@ -175,3 +175,38 @@ def test_the_emitted_datatype_matches_the_shipped_vocabulary(): assert str(declared[0]) == CONFIDENCE_DATATYPE, ( f"vocabulary says {declared[0]}, serializers write {CONFIDENCE_DATATYPE}" ) + + +# ── Review findings on the first revision of this fix ──────────────────────── + +@pytest.mark.parametrize("huge", ["1e100000000", "1E1000000", "-1e999999", 10**400]) +def test_an_absurd_magnitude_is_rejected_not_expanded(huge): + """ + xsd:decimal has no exponent notation, so the value has to be written out. + "1e100000000" is eleven characters that expand to a hundred million digits, + and the export path continues past validation errors. + """ + from semantica.export.rdf_exporter import normalize_confidence + + assert normalize_confidence(huge) is None + + +def test_a_legitimately_small_confidence_is_still_accepted(): + from semantica.export.rdf_exporter import normalize_confidence + + assert normalize_confidence(1e-9) == "0.000000001" + + +def test_signed_zero_is_normalized(): + """0.0 and -0.0 would otherwise be two distinct RDF terms.""" + from semantica.export.rdf_exporter import normalize_confidence + + assert normalize_confidence(0.0) == normalize_confidence(-0.0) == "0" + + +def test_signed_zero_gives_one_term_across_serializers(): + positive = {n: _confidence_terms(g)[0] for n, g in _graphs(_kg(0.0)).items()} + negative = {n: _confidence_terms(g)[0] for n, g in _graphs(_kg(-0.0)).items()} + + assert set(positive.values()) | set(negative.values()) == set(positive.values()) + assert len(set(positive.values())) == 1