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.
This commit is contained in:
FABIOTESS
2026-08-19 17:38:23 +01:00
parent 05c21af117
commit efdfa39c15
2 changed files with 52 additions and 0 deletions
+17
View File
@@ -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
@@ -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