test(domains): cross-pack domain-validation lane - #482
Merged
Merged
Conversation
Adds tests/test_domain_validation_lane.py: a cross-cutting suite for the eight domain packs, covering the properties that only show up when packs are compared with one another or handed input they were never meant to see. What it pins: - every pack scores 1.0 on its own good frame, and the engine's reported layer/severity/check/repair for every rule matches its rules.yaml entry; - all declared numeric and length bounds are inclusive (lat/lon, Modbus register, soil pH's two nested bands, description length, runtime); - an 8x8 wrong-domain matrix: only the diagonal validates, everything off-diagonal is trust 0.0 / passed False; - no rule ever reports "passed" for a field that is absent, swept over every mapped column of every pack; - the exact arithmetic of _trust_score, per severity weight, plus three measured reasons the score is not comparable across domains; - subset-based reference sets never silently rewrite an unrecognised value (coerce logs "unresolvable"); - version / schema_version reach ValidationReport for all eight packs; - run_domain never mutates its input, verified with a SHA-256 digest over columns, index, dtypes and values, on both the unique and non-unique index paths. Behaviours the tests record as findings (current behaviour asserted, so a deliberate fix has one obvious place to update): - _check_regex stringifies a float64 column as "10000266.0", so FIN-008 and GS1-008 raise false findings for a CSV column with one blank cell, although retail/_integral_float_text already solves exactly this; - FIN-006 declares tolerance 0.01 but compares raw binary floats, so a one-cent imbalance is never within tolerance; - retail, energy and agriculture report "absent from our documented subset" as error severity where healthcare reports it as a warning; - MISSING_REQUIRED_FIELD is an in-band row-label sentinel, so a frame indexed by that literal string mis-scores; - a column_map entry naming an absent column or an unknown canonical field is silently ignored; - healthcare's describe() has no schema_version before resource detection.
Contributor
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lane: domain validation (Phase 8)
A cross-cutting suite for
src/freshdata/domains/— the eight packs, the sharedConfigDrivenValidatorengine, the registry and the bundled reference layer.The per-pack suites under
tests/domains/already check that each pack flags theright rows; this file covers what only shows up when the packs are compared with
one another, or handed input they were never meant to see.
One new file,
tests/test_domain_validation_lane.py(66 tests). No sourcechanges — the findings below are reported, not fixed, so the team can decide the
disposition of each. Every test asserts the behaviour that exists today, so a
deliberate fix has exactly one place to update.
Full suite:
pytest -m "not online and not large" -p no:randomly→6723 passed, 26 skipped, 0 failures, coverage 94.02% (gate 93%).
ruff check .passes repo-wide.Verified — these hold
(1.0, True); every off-diagonal cell is exactly(0.0, False).passedfor a field that is absentpassed. Zero offenders across all 8 packs.run_domainnever mutates its inputassert_frame_equal, which tolerates dtype drift. Holds for all 8 packs on dirty frames that actually trigger repairs, on both the unique-index and the non-unique-index (internal re-index) path.stop_lat/stop_lon, Modbusregister_address[0, 65535], the two nested soil-pH bands,product_description≤ 200,runtime_seconds≤ 86400, the 2-decimal amount scale — exact boundary passes, one step outside fails.id_fieldper pack; row count unchanged and the cell stays null.repair: coercereference rule (AG-007, AG-010, ENG-008) logsunresolvableand leaves the cell exactly as it was.version+schema_versiononValidationReport, into_dict()and insummary(). Healthcare versions each FHIR resource separately;finance_mode="tick"versions separately.AmbiguousFHIRResourceError, media →AmbiguousMediaTypeError, transport →DomainError, on every one of the other packs' frames.CONTRIBUTING_DOMAINS.mdstates. Transport single-file mode skips 10 of 14 rules and still scores 1.0.Findings
ConfigDrivenValidator._check_regexstringifies a float64 column with a.0suffix, so a numeric code column loaded from CSV with one blank cell fails a digit-only pattern. Hits both regex rules in the repo: GS1-008[0-9]{8}and FIN-008[A-Za-z0-9]{4,12}.10000266in anint64column passes; the same code in afloat64column becomes"10000266.0"and is flagged. Both are warning severity sopasseddoes not flip, but every row raises a false finding and the trust score drops. The repo already solves exactly this —retail/validator.py::_integral_float_text, whose docstring describes this same CSV-blank-cell scenario — so the intended behaviour is settled and the shared engine simply does not apply it. Test:test_regex_check_misreads_an_integral_float_column.bundled/uom_codes.jsonsays in its own_meta: "Curated subset … not the exhaustive UN/CEFACT list" (50 of ~1800 Rec-20 codes), yet GS1-006 is named "net_content_uom is a valid UN/CEFACT code" and is error severity — a real Rec-20 code outside the subset setspassed=Falseand trust to 0.0.energy/quality_codes.json("NOT an exhaustive or vendor-specific quality model; specific historians may expose finer-grained sub-statuses") behaves the same via ENG-008, as do the curated agriculture unit sets. Healthcare is the counter-example and the model to follow: HC-C005 / HC-O004 / HC-O010 are warning severity and are named "… is in the documented common set". Nothing is mutated, so this is a wrong disposition, not corruption. Decision needed: make subset-gated reference rules warnings pack-wide, or complete the sets. Tests:test_absent_from_a_disclaimed_subset_is_reported_as_error,test_healthcare_treats_absence_from_a_subset_as_a_warning.domain_trust_scoreis not comparable across domains. Measured formula:clamp01(1 − Σ_violated weight(severity) × min(rows_violated / n_rows, 1)),weight = {error: 1.0, warning: 0.25, info: 0.05}, rounded to 4 dp; skipped and passed rules contribute nothing; a missing required column is charged a row fraction of 1.0 regardless of frame size. Three measured reasons it cannot be compared between packs: (a) the penalty is an unnormalised sum over rules, so denser packs fall faster — max accruable penalty is 21.00 for media (24 rules) but 6.75 for finance (9), i.e. the 0.0 floor is reached after ~5% of media's rules versus ~15% of finance's; (b) packs cover the same defect with different rule counts — one bad cell in four rows costs 0.25 in finance (one rule) but 0.3125 in agriculture (an error and a warning rule on soil pH); (c) severity conventions differ per pack for the same kind of check (finding 2). It also saturates: one fully-violated error rule and five fully-violated rules both score exactly 0.0, so it does not rank even within a pack at the low end. The repo documents none of this —CONTRIBUTING_DOMAINS.mdsays only "skipped rules never affect the trust score". Missing decision: is this an absolute, cross-pack data-quality index, or a within-pack/within-schema indicator? Tests:TestTrustScore(7 tests).skipped). In a 1000-row finance frame, droppingcurrencyscores 0.0 while one bad currency value scores 0.999 — a step, not something proportional to lost data. Correct as a conservative choice; recorded because it is the answer to "how do missing fields vs rule violations affect the score". Test:test_a_missing_column_costs_the_full_weight_and_silences_its_other_rules.MISSING_REQUIRED_FIELDis an in-band sentinel in the row-label channel._trust_scoredecides "table-level finding" by testing whether anyviolation_rowsentry equals that literal string, so a frame whose index carries that label has an ordinary one-row violation charged as a whole-table one (0.75 → 0.0). No built-in pack usesrepair: reject, so today the blast radius is the score alone — but the sentinel belongs onRuleResultas a flag. Test:test_missing_required_field_sentinel_collides_with_a_row_label.tolerance: 0.01but compares raw binary floats:abs(100.00 − 99.99)is0.010000000000005116, so a one-cent rounding difference — exactly what a 0.01 tolerance exists to absorb — is reported as an error. The tolerance admits nothing at its own boundary. Fix would be to round the difference to the rule's scale before comparing. Test:test_balanced_tolerance_never_admits_a_one_cent_imbalance.column_mapfails quietly in three distinct ways, none of which raises or logs a diagnostic: a typo'd source column is dropped and detection silently falls through to the normal exact/alias search, so the user gets a different column than they asked for withmethod: "exact"in the log; an unknown canonical field ("currency_code"instead of"currency") is never consulted, because detection only iterates the pack's own field names; two columns mapped to one canonical field silently keeps only the last, because the override dict is inverted. Validatingcolumn_mapkeys againstcanonical_fieldsand its values againstdf.columnswould cost one loop. Tests:TestColumnMapOverrides(6 tests).describe()has no stable shape for healthcare before resource detection. Every other pack answers with the same eight keys; an un-activatedHealthcareValidatoranswers with a five-key placeholder, so a caller building an audit trail fromdescribe()["schema_version"]raisesKeyErrorfor healthcare and only for healthcare. Documented by anotekey, so arguably deliberate — but it is an unannounced shape change in a JSON audit surface. Test:test_describe_is_uniform_across_packs_except_unresolved_healthcare.media_typeswaps the effective rule set but not theschema_version— both sub-schemas reporteidr-ddex-2024, unlike healthcare (per-resource) and finance (finance-tick/2025.06).describe()reportsmedia_type;ValidationReportdoes not, so a stored report identifies the sub-schema only indirectly, via theMD-C*/MD-R*rule-id prefix. Test:test_media_sub_schema_is_not_distinguished_by_schema_version.run_domainre-indexes internally and maps findings back to the caller's labels, so a duplicated label identifies a group of rows rather than the flagged row: findings["a", "b"]wheredf.loc[["a", "b"]]selects three rows. Labels are always valid; they are just not unique. Test:test_reported_row_labels_are_translated_back_but_stay_ambiguous.Suspected, then disproved
descriptionalias." The alias match is real — retail'sproduct_descriptiondoes bind to finance'sdescriptioncolumn, and GS1-007 genuinely returnspassedon it. Butgtinis absent, GS1-001 fires withMISSING_REQUIRED_FIELD, and the frame comes backpassed=False, trust 0.0. Partial alias luck never adds up to a validation. Pinned astest_a_shared_column_name_alone_never_produces_a_pass.re.fullmatch(r"txn_?id", "Txn ID")fails, sodetect_columnsleavestransaction_idunmapped for the repo's ownmessy_financefixture. This was my harness error, not a defect:fd.cleannormalises column labels before the pack runs (_normalized_column_map,cfg.column_names), so through the public entry point"Txn ID"is alreadytxn_idand the alias matches. Directget_validator(...).detect_columns(raw_df)is a lower-level surface that does not get that normalisation.run_domain's shallow copy for a non-unique index leaks the RangeIndex back to the caller."df.copy(deep=False)gives the copy its own axes, so assigningworking.indexdoes not touch the input. Verified by digest for all 8 packs: the input's index, dtypes and values are byte-identical afterwards.MISSING_REQUIRED_FIELDrow label could get a row dropped byrepair: reject." No built-in pack declaresrejectanywhere (coerceandflag_onlyonly), so finding 5 is confined to the score today.re.fullmatch, socredit_limitandsalary_creditare both left alone while a barecrbinds and is logged withmethod: "regex".Notes
exact/case_insensitive/regex/override/missing), one entry per canonical field, with no gaps — that audit log is what makes findings 7 and the by-design "a swappedcolumn_mapvalidates cleanly" case reviewable rather than silent.versionandsourcein_meta, and every pack'sreference_sources()stays JSON-serialisable.