Skip to content

Add automated data validation for pull requests - #115

Open
jag3773 wants to merge 2 commits into
masterfrom
ci/validate-data
Open

jag3773 wants to merge 2 commits into
masterfrom
ci/validate-data

Conversation

@jag3773

@jag3773 jag3773 commented Nov 11, 2026

Copy link
Copy Markdown
Member

Why

This project has no automated checks, so every pull request has had to be verified by hand. With few people able to review, that is slow, and it is how #108 came to wait eighteen months. This adds a small set of checks that run on every pull request and on pushes to master.

What it checks

Any of these failing fails the run:

  • every tracked XML file is well-formed;
  • wlc/ has 39 books and 23,213 verses, and every word has a lemma, a morph code and a unique five-character id;
  • MAPM/MAPM.xml has balanced verse markers and the same verse inventory as wlc/, apart from Josh 21:36–37, which MAM's text does not have;
  • no operating-system junk files (Thumbs.db, .DS_Store, desktop.ini) are tracked.

What it reports

On a pull request it also writes a data change summary to the job summary: which words in wlc/ changed and in which field (text, lemma, morph, n), any word ids added or removed, and how many MAPM/ verses changed. The summary never fails the run; it is there so a reviewer can see at a glance what a change touches. For #108 it would have been one row:

verse id field before after
Ps.93.4 19AHF n 2 1.0.0

Also in this pull request

The four Thumbs.db files are removed and such files are ignored from now on, as osjc proposed in #111. Without that the new junk-file check would fail on master.

How it was tested

Run locally with the same script:

This pull request is the first real run of the workflow on GitHub.

Notes

  • The script (.github/scripts/validate_data.py) uses only the Python standard library, so it can be run locally with python3 .github/scripts/validate_data.py, or with --base <checkout of master> to see the change summary.
  • The workflow uses the pull_request trigger with contents: read only, so pull requests from forks get a read-only token and no secrets.
  • It does not validate wlc/ against the OSIS schema, because the word id attribute is not in that schema and the files do not pass it today. Morph codes are checked for shape, not against the full code tables; that would be a good later addition.

🤖 Generated with Claude Code

Add a GitHub Actions workflow that runs on every pull request and on
pushes to master. It checks that:

- every tracked XML file is well-formed
- wlc/ has 39 books and 23,213 verses, and every word has a lemma, a
  morph code and a unique five-character id
- MAPM/MAPM.xml has balanced verse markers and the same verse inventory
  as wlc/, apart from Josh 21:36-37, which MAM's text does not have
- no operating-system junk files are tracked

On pull requests it also writes a summary of which words and verses
changed to the job summary, to make review quicker. The summary never
fails the build.

The script uses only the Python standard library. The workflow uses the
pull_request trigger with a read-only token, so it is safe for pull
requests from forks.

Remove the four Thumbs.db files and ignore such files from now on, as
proposed in #111, so that the new check passes.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@jag3773

jag3773 commented Nov 11, 2026

Copy link
Copy Markdown
Member Author

Review of 6623bd0a18d98accf74e7e5a8897d4a850f37ad6 against d4265dfa93b78d82765a557e12d965528c17c1a9.

Recommendation: address the six P2 correctness findings below before merging. Each was reproduced by running the unchanged validator against an isolated, Git-tracked copy of the complete dataset. They cover malformed VerseMap XML escaping validation, baseline comparison errors failing valid current data, misplaced words bypassing checks, six-character IDs passing the ID check, deletion of MAPM passing validation, and conflicting start/end attributes being overlooked.

What passed: the unchanged dataset validated with 23,213 WLC verses, 306,785 word IDs, and 23,211 MAPM verses. Separate mutations correctly failed for missing lemmas, duplicate word IDs, malformed morphology, unclosed MAPM verses, verse-inventory mismatches, tracked junk, and malformed XML outside the skipped WLC metadata. Reconstructing the Psalm 93:4 example produced the promised single n: 2 → 1.0.0 row. The first GitHub workflow run also succeeded.

Standards: no direct violations of documented repository standards were found. One maintainability concern overlaps the baseline-comparison defect: parse() and _quiet_parse() duplicate XML parsing with inconsistent error policies. Consolidating that policy would help prevent the two comparison paths from diverging.

Additional improvement: swapping two intact words within a verse reports “No word-level changes.” The records are keyed by word ID and contain no position, so the summary cannot expose a changed reading caused solely by reordering. Comparing the ordered word IDs per verse would address this. This is separate from the six findings because the PR's explicit field list does not promise position tracking.

Full OSIS schema validation and validation against the complete morphology-code tables are explicitly deferred in the PR and were not counted as defects.

Findings

1. [P2] Include VerseMap.xml in the tracked-XML check

validate_data.py:259

This excludes every file under wlc/, but load_wlc() also excludes VerseMap.xml at line 83. Consequently that tracked XML file is never parsed. Replacing its entire contents with <not-xml> still produces exit status 0 and a passing validation result on the complete dataset. This breaks the advertised guarantee that every tracked XML file is well-formed. Parse all tracked XML files before the specialized checks, or explicitly validate the metadata files omitted by load_wlc().

2. [P2] Keep errors in comparison data out of validation status

validate_data.py:88

report() calls load_wlc(base, check=False), but this unconditional parse() still appends failures to the global errors list. With a valid current checkout and a complete baseline containing only a malformed wlc/Obad.xml, the script exits 1 solely because of the baseline parse failure. A PR repairing malformed XML can therefore remain red, contrary to the promise that summary generation never fails the run. Use quiet parsing for baseline WLC data, as the MAPM comparison does, report unavailable comparisons, and keep summary failures separate from validation of the current revision.

3. [P2] Reject words outside recognized verse elements

validate_data.py:97

All word validation and summary indexing happen inside this verse traversal. Adding <w>בָּרָא</w> directly beneath the Obadiah chapter, before the first verse, passes validation despite having no ID, lemma, or morphology; the summary also reports no word-level changes. This leaves a gap in the guarantee that every word has the required attributes. Inspect all word elements and reject any that do not belong to a recognized verse, then validate and index their attributes.

4. [P2] Match the entire ID value, including a possible final newline

validate_data.py:104

The ID pattern uses $, which permits a match immediately before a final newline. Changing an existing id="31xeN" to id="31xeN&#10;" yields a six-character parsed value but still passes validation. Use WORD_ID.fullmatch(wid) to enforce the five-character requirement. The morphology, structure, and verse-ID checks use the same anchored-match approach and should also use complete-value matching. Python's documented behavior: https://docs.python.org/3/library/re.html#re.fullmatch

5. [P2] Fail validation when the current MAPM file is missing

validate_data.py:132

Returning None without recording an error causes main() to skip the MAPM verse-inventory comparison. Deleting the entire tracked MAPM/MAPM.xml file from an otherwise unchanged checkout still exits 0. The missing corpus cannot satisfy the advertised inventory agreement with WLC. When check=True, record a validation error for the absent file; permissive handling can remain for comparison-only baseline reads.

6. [P2] Reject milestones carrying both start and end attributes

validate_data.py:143

The if sID / elif eID structure silently ignores an eID on an element that also has sID. Adding eID="Gen.99.99" to the first sID="Gen.1.1" milestone, while retaining the original subsequent end marker, still passes validation. The conflicting end attribute is never checked, leaving a gap in the balanced-marker guarantee. Require exactly one of sID or eID on each verse milestone before processing it; this does not require full OSIS schema validation.

Address the six findings from the review of 6623bd0, each reproduced
first by mutating a copy of the dataset:

1. wlc/VerseMap.xml was never parsed, so a malformed copy passed. Every
   tracked XML file not already read by a specific check is now parsed
   for well-formedness.
2. A malformed file in the base revision failed the run. The base is now
   read quietly; the summary names what could not be compared, and any
   error while producing the summary is reported there instead of
   failing the run.
3. A <w> outside any verse skipped all checks. It is now an error.
4. The id, morph, n and verse-id patterns used "$", which accepts a
   trailing newline, so a six-character id passed. They now use
   fullmatch().
5. Deleting MAPM/MAPM.xml passed. A missing file is now an error.
6. A verse milestone with both sID and eID, or with neither, was not
   reported. Each milestone must now carry exactly one.

Also from the review:

- The change summary now lists verses whose words are the same but in a
  different order, which the id-keyed comparison could not show.
- parse() and _quiet_parse() are merged into one function with a quiet
  flag, so the two comparison paths share one error policy.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@jag3773

jag3773 commented Nov 11, 2026

Copy link
Copy Markdown
Member Author

Prepared by Claude (an AI assistant) working at my direction.

All six findings are addressed in 22484c0. Each was reproduced first against 6623bd0 by mutating a copy of the dataset, and each mutation now gives the expected result:

# Mutation 6623bd0 22484c0
1 wlc/VerseMap.xml replaced with <not-xml> passes fails: not well-formed
2 malformed wlc/Obad.xml in the base only fails passes; summary says the book was not compared
3 <w> placed under a chapter, outside any verse passes fails: word outside any verse
4 id="31xeN&#10;" passes fails: malformed id
5 MAPM/MAPM.xml deleted passes fails: file is missing
6 first sID="Gen.1.1" milestone also given an eID passes fails: both sID and eID

How each was fixed:

  1. Every tracked XML file that a specific check has not already read is now parsed for well-formedness.
  2. The base revision is read quietly, and an error while producing the summary is reported in the summary instead of failing the run.
  3. Any <w> that is not inside a verse is an error.
  4. The id, morph, n and verse-id patterns use fullmatch().
  5. A missing MAPM/MAPM.xml is an error for the current revision; the base may still lack it.
  6. Each verse milestone must carry exactly one of sID and eID. A milestone with neither is reported as well.

Also taken up from the review:

  • Word order. The summary now lists verses whose words are the same but in a different order. Swapping the first two words of Obad 1:1 reports that verse.
  • Parsing. parse() and _quiet_parse() are one function with a quiet flag.

Still passing after the change: the unchanged dataset (23,213 verses, 306,785 words, 23,211 MAPM verses), the single-row Ps 93:4 summary against the pre-merge base, and the controls for an empty lemma, a missing id, a duplicate id, a malformed morph, a malformed n, a tracked junk file, and malformed XML elsewhere in the repository.

Not changed: the mutation tests were run locally and are not added to the repository.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant