From 1b65632c38a0e228bd8d6bf12790be937617c512 Mon Sep 17 00:00:00 2001 From: ecrum19 Date: Sat, 12 Sep 2026 12:16:27 +0200 Subject: [PATCH] Expect typed FORMAT values in the mutation fixture oracle The emitter now gives a single, non-missing Integer/Float FORMAT cell the same typed companion an INFO value has always had, so the fixture's census has to expect those triples. Without this the oracle counts only the INFO contribution and every expanded mutation test reports a MISMATCH on vcfc:fieldValueInteger -- including the unmutated-graph harness check, which is the one that must never fail. _format_typed_shape restates the typing rule over the fixture's own FORMAT cells rather than importing the emitter's helper, mirroring how the INFO counters are derived: an expectation computed by the code under test would only prove that code agrees with itself. It reuses the runner's widening rule so a payload longer than the FORMAT column is named the same way. The emitter and validator halves of this change are already on main, in 0b00b8f, whose message ("small conda config change") describes only part of what it carries: that commit also moved FORMAT value typing into vcf_rdfizer.py, added --shacl-ontology and RDFS inference to the SHACL layer, and corrected the retired shapes filename in docs/validation.md. Full suite: 647 tests, OK. Co-Authored-By: Claude Opus 5 --- test/validation_fixtures.py | 50 +++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/test/validation_fixtures.py b/test/validation_fixtures.py index e879188..40b3430 100644 --- a/test/validation_fixtures.py +++ b/test/validation_fixtures.py @@ -447,6 +447,55 @@ def _record_shape() -> dict: )) +def _declared_types(header_key: str) -> dict[str, str]: + """Map each declared INFO or FORMAT key to its VCF Type.""" + import re as _re + + declared: dict[str, str] = {} + for key, value in HEADER_LINES: + if key != header_key: + continue + fields = dict(_re.findall(r'(\w+)=("[^"]*"|[^,>]*)', value)) + ident = fields.get("ID", "").strip() + if ident: + declared[ident] = fields.get("Type", "String").strip('"') + return declared + + +def _format_typed_shape() -> dict: + """FORMAT cells that gain a typed companion, by the runner's own rule. + + The emitter types a single, non-missing Integer/Float FORMAT value exactly + as it types an INFO one, so the census has to expect those triples too. + """ + runner = _runner() + declared = _declared_types("FORMAT") + typed_int = typed_dec = 0 + for row in _record_rows(): + format_keys = row[8].split(":") if len(row) > 8 and row[8] else [] + payload_fields = [payload.split(":") if payload else [] for payload in row[9:]] + width = max( + [len(format_keys), *(len(fields) for fields in payload_fields)], default=0 + ) + for index in range(width): + key = (format_keys[index] + if index < len(format_keys) and format_keys[index] + else f"FIELD_{index + 1}") + for fields in payload_fields: + cell = fields[index] if index < len(fields) else "" + if not cell: + continue + kind = runner.typed_value_kind(cell, declared.get(key, "String")) + if kind == "Integer": + typed_int += 1 + elif kind == "Float": + typed_dec += 1 + return { + "formatTypedIntegerCount": typed_int, + "formatTypedDecimalCount": typed_dec, + } + + def _info_shape() -> dict: """INFO counters the census needs, mirroring the emitter's typing rules.""" declared = {} @@ -548,6 +597,7 @@ def parser_summary( "q06EligibleSiteCount": q06_eligible, "headerLineCount": len(HEADER_LINES), **_format_shape(), + **_format_typed_shape(), **_info_shape(), "fileFormat": FILE_FORMAT, "referenceGenome": REFERENCE_GENOME,