0.0.1.9000’s ceiling section named 48 rules as genuinely reachable with more work and published that as a limit rather than a target. These are those handlers, plus four rules that ceiling had filed as undecidable and that became reachable once the shipped CT carried term-level NCI codes. 52 rules moved, across 8 mechanics — counting rules overstates the work, so both numbers are given. Every handler recomputes the problem from the data and declines to emit an expression when the recomputation shows the remedy is not decided by it.
variable_name_format (25 rules: AD0013, AD0062, AD0063, AD0065, AD0068, AD0073, AD0074, AD0094, AD0185—AD0193, AD0213, AD0215, AD0273, AD0299, AD0303, AD0314, AD1018, AD1019) — an ADaM variable name that breaks the rule’s own name template. The rule quotes the template (TRTxxP, CRITy(FL/FN), (R2)AyLO, *ORGy), so the legal names are enumerable: .expand_name_template() expands it to every name it permits and the repair is the one that differs from the flagged name by zero-padding alone (TRT1P → TRT01P, AVALCAT01 → AVALCAT1). A rename changes no values. Refused when the index is 0, when it is 10 against a [1-9] rule (that is a renumbering, and what the parameter means is a study decision), when the padded name would collide with an existing column, and — for AD0013 — when the name is over 8 characters, because shortening it is a naming decision. Only the illegal-character substitution is emitted there.ts_val_format (9 rules: SD1215, SD1217, SD1219, SD1221, SD2245—SD2249) — a TSVAL in the wrong format for its parameter. Deterministic only when the recorded value re-expresses itself: a date that parses under exactly one known format ("01JAN2021" → "2021-01-01"), an integer wrapped in spaces or separators, or a duration that states its own unit ("18 YEARS" → "P18Y"). A bare "52" for LENGTH is refused: weeks, months and years are all in the protocol and none of them is in the data. That is the same restraint that keeps SD0029/SD0026 unhandled.duplicate_ts_record (8 rules: SD1214, SD1216, SD1218, SD1220, SD1222, SD1224, SD1225, SD1227) — a Trial Summary parameter with more than one record. Deterministic only when the records are exact duplicates once the sequence variable is set aside; if they disagree on TSVAL, which one the protocol meant is not in the data.define_missing_entry (2 rules: SD0060, SD1063) — a variable, or a whole dataset, in the data but not in define.xml. The skeleton specification row is recomputed from the column: type from its class, length from the longest value it actually holds. The label is left NA where the column carries none, the same convention missing_variable and missing_ts_param already use.type_mismatch (SD0059) — define.xml declares one type and the column holds another. Only the lossless conversion is emitted (character digits to integer, an integer column to character). A numeric column read as text because one record says "UNKNOWN" is refused: deleting that value, or changing define.xml to agree with it, are both decisions about the data.datetime_component (2 rules: AD0044, AD0045) — a --DT or --TM that disagrees with its --DTM. The repaired value is a substring of the --DTM already recorded, so nothing is invented. Refused when any disagreeing record has no ISO 8601 --DTM to read from — no partial expression is emitted. The direction is the ADaM derivation and the suggestion says so on every row.non_ascii (SD1029) — non-ASCII or non-printable characters in a value. Deterministic only for characters whose ASCII equivalent is not a choice: a non-breaking space is a space, a typographic quote is a quote, an en dash is a hyphen. An accented letter or a CJK character is not transliterated, and the suggestion names the offending code points instead — respelling a term changes the term, which is data.code_decode_pair (4 rules: CT2003, CT2006, SD0040, SD1043) — a --TEST that is not the CDISC term sharing its --TESTCD’s NCI concept code. Deterministic in that direction only: a --TESTCD outside CT is a sponsor term whose long name only the study knows, and it is left alone.sdtm_ct now carries term-level NCI concept codescode on sdtm_ct: the NCI concept code of each term, not only of its codelist. CDISC gives a --TESTCD term and its --TEST term the same code, which is exactly what code_decode_pair needs and exactly what 0.0.1.9000’s ceiling named as the reason those four rules were out of reach.sdtm_ct.rda itself, 223 KB → 285 KB). Judged worth it: code/decode disagreement is a routine submission finding and the remedy really is a lookup. R CMD check still finishes at 0 notes.Measured against the 1,840 shipped CDISCPILOT01 findings:
SD0029, SD0021, SD0026, SD1076, SD2236, SD1131, CT2002, SD0006) and no new class covers any of them. The test suite asserts the count is exactly 0 rather than leaving it unsaid.drop_variable, on the 28 SD1076 findings, and it still declines all 28. Unchanged.Counted by data-raw/make-rule-catalogue.R on every rebuild:
TSVAL parameters whose value is an FDA SRS or NDF-RT term, and the SDTM implementation guide’s own variable type list.SD0029, SD0026 and SD0021 — 1,790 of the 1,840 real findings — remain human_judgement. That judgement was not reversed to inflate coverage. Nothing in these eight handlers invents a value: every repaired string is a substring of, or a re-spacing of, something already in the data, a controlled terminology lookup, or a column rename.p21_rules: the Pinnacle 21 rule catalogue, 537 rules (306 SD, 225 AD, 6 CT), keyed by Pinnacle 21 id and carrying the Publisher (FDA) id, the canonical message and description, the category and the severity (423 Error, 113 Warning, 1 Notice). Built reproducibly by data-raw/make-rule-catalogue.R from the Pinnacle 21 Community reports distributed in phuse-org/phuse-scripts (MIT licence).rule_info() looks rules up by id.suggest_fix() classifies a finding by looking its rule id up in the catalogue first and only then by matching the report’s message text. The new class_source column records which path fired ("catalogue" or "message_regex"). The message path is unchanged and still handles rules the catalogue does not have, so no finding is classified worse than in 0.0.0.9000; an unrecognised finding still degrades to fix_class = "human_judgement" with deterministic = FALSE.required_value_null (Pinnacle 21 SD0002 and friends): the variable exists but required records are empty. The count and the affected USUBJIDs are recomputed from the data; no expression is emitted, because the missing values are study data.suggest_fix() could recompute; the next section takes that to 79, and the section above it to 131.data-raw/make-rule-catalogue.R against newer reports to refresh it.The previous release’s own limit was that widening coverage needs more fix handlers, not more rules. These are those handlers. Every one recomputes the problem from the data and declines to emit an expression when the recomputation shows the remedy is not decided by the data.
drop_variable (7 rules: SD1076, SD1078, SD1073, SD1074, SD1075, SD0058, AD0252) - a variable the standard says should not be in the dataset. Deterministic only when the variable holds no data; a populated prohibited variable usually belongs in SUPPQUAL, which is a decision about the study’s data, so the handler reports the populated count and stops.missing_ts_param (35 rules: SD2201-SD2235, SD1226) - a required Trial Summary parameter has no record. The record’s structure is fully determined and the shipped CT confirms whether the TSPARMCD is a real term; TSPARM and TSVAL come from the protocol and are emitted as NA, exactly as the value of a missing required variable is.domain_value (SD0004) - DOMAIN is set from the dataset name, which is what the rule itself defines as correct; a split dataset (LBCH, LBHE) still yields the right two-character code.studyid_mismatch (SD1005) - STUDYID is set to the value DM carries, and only when DM carries exactly one.variable_order (SD1079) - columns are reordered to the specification’s order, which changes no values. Refused outright when the specification does not cover every column, because a partial order is a guess.label_mismatch (3 rules: SD0063, SD0063A, AD0018) - the label is set from the specification. Refused when the specification declares no label (p21bridge does not ship the SDTM/ADaM standard labels) or when that label is over the 40-character transport limit.split_length (SD1116) - split datasets are harmonised to the longest value any of them actually holds, so they can be recombined without truncation.key_not_unique gains SD1038 and now renumbers a --SEQ variable within the other key the message names rather than within itself, which is what “Non-unique value for TSSEQ within TSPARMCD” actually asks for.read_findings() now resolves the Variables/Values pairing that a real Pinnacle 21 Details sheet uses. Those sheets write the placeholder VARIABLE in Variables and put the variable’s name in Values ("VARIABLE, LABEL" against "ANL01FL, Analysis Flag 01"). Before this, every finding of that shape resolved to a variable literally named "VARIABLE" - in the shipped CDISCPILOT01 report that is 28 of the 1,840 findings, which is every finding any new handler covers, so nothing could be recomputed from a real report.inst/extdata/cdiscpilot01_datasets.rds: the real CDISCPILOT01 columns Pinnacle 21 flagged under SD1076, from the same MIT-licensed source, so the handlers are tested against real data and not an imitation of it.Measured, not estimated, against the 1,840 shipped CDISCPILOT01 findings:
drop_variable, on all 28 SD1076 findings.VS.EPOCH on 29,643 of 29,643 records, CM.VISIT on 7,510 of 7,510 - so dropping any of them would delete real values. Zero fix expressions are emitted for that report, which is the correct answer, not a gap.Counted by data-raw/make-rule-catalogue.R by explicit pattern, so it is re-runnable and arguable:
TSVAL parameters whose value is a dictionary or CT term, and the SDTM IG’s own variable type list. The shipped CT export also drops term-level NCI codes, which is what checking a --TESTCD/--TEST pair against each other would need.TRT1P to TRT01P), 9 TSVAL format rules, 8 “Multiple parameter records” in TS that are exact duplicates, 2 define.xml presence rules, 2 *DT/*TM values derivable from their *DTM, and non-ASCII characters in --TEST/--TESTCD.sdtm_ct: CDISC SDTM controlled terminology, release 2026-03-27, all 1,208 codelists and 45,566 submission values, with extensibility. Nothing is subset away; only the columns validation does not use (definition, synonyms, NCI preferred term) are dropped, which keeps the installed data under 250 KB with xz compression.ct_codelist(), ct_check() and ct_release().suggest_fix() resolves a controlled-terminology finding against the study specification when it declares a codelist, and otherwise against sdtm_ct - from the codelist named in the finding message, or failing that from a codelist whose submission value is the variable name. The evidence names which source and which release was used.ct_release() in the study documentation, and rebuild with data-raw/make-ct-dictionary.R to move.SEX, AESEV, NY). It can pick the wrong codelist for a variable that breaks that convention, which is why the evidence always names the codelist it used.inst/extdata/cdiscpilot01_findings.csv: 1,840 real Pinnacle 21 findings against CDISCPILOT01, from the Details sheet of the same MIT-licensed report the catalogue was built from.SD0002 was a missing-variable finding; the real SD0002 is a null value in a required variable). It now uses real rules where a real rule covers the planted defect, and deliberately unknown ids (SD1310, SD1234, DD0011, OD0018) where the message path is being exercised. It has 12 findings, up from 11.read_findings() now recognises Pinnacle 21 ID as a rule-id column, which is what real report Details sheets call it. Version 0.0.0.9000 could not read an unmodified Pinnacle 21 export.read_findings() reads a Pinnacle 21 style validation report from .csv, .xlsx or .xls into one normalised tibble, mapping the column names used by different report vintages onto rule_id, severity, domain, variable, message and count, and deriving rule_class from the SD/AD/CT/DD/OD rule-id namespaces.trace_findings() resolves each finding to the dataset object, the variable within it and the specification row that declares it, with an explicit resolved flag and a reason for anything that could not be traced.suggest_fix() proposes a concrete remedy for six deterministic finding classes (missing required variable, variable length, controlled terminology, ISO 8601 dates, key uniqueness, referential integrity), including the R expression that fixes it where that is safely derivable, and labels everything else as needing human judgement.finding_summary() counts findings by severity, domain and rule class..csv and .xlsx), example datasets and an example specification ship in inst/extdata, so every example and test runs offline.