Repository navigation
fix: guard every integer cast that reads a document value (#37) - #44
Merged
Merged
Conversation
#37 named one cast. Probing for siblings found three, all the same shape, all aborting the whole conversion with exit 1 and no output file: line 143 w:ilvl/@w:val document the one filed line 172 w:lvl/@w:ilvl numbering.xml not filed line 400 w:gridSpan/@w:val document not filed Each contradicted the promise the stylesheet makes three lines above one of them: one malformed document must never stop a corpus conversion. The duplicate-w:numId and undefined-numId cases already keep that promise; these did not. In a batch the document is lost rather than degraded. Each now falls back exactly the way its own absent-attribute case already did: level 0, a bullet, a single cell. Output is unchanged on every well-formed document: 13 of 13 corpus documents byte-identical. Two engine constraints rule out the obvious spelling, and both are recorded rather than rediscovered: - `[@w:ilvl castable as xs:integer and xs:integer(@w:ilvl) eq $ilvl]` is wrong. XPath does not guarantee short-circuit evaluation of `and`, so the cast may still run and still throw. - `[A][B]` chained predicates are quadratic on this engine (recorded as phoenixmldb-xslt#10/#95; w:p[A][B] measured O(n**2)). So it is one predicate using `if ... then ... else false()`, where conditional evaluation is something the spec actually promises. Tests first, and they failed for the right reasons before the fix: three distinct line numbers in the thrown XQueryException, 143, 172 and 400. NonNumericIlvlInTheDocument_DegradesToLevelZero NonNumericIlvlInNumbering_DegradesToABullet NonNumericGridSpan_DegradesToASingleCell ListStylesheetTests.ToMarkdownAsync takes an optional numbering part now, which the second test needs to malform numbering.xml rather than the document. The C# side was checked and is clean: the only int.Parse reads docmd's own frontmatter output, never document input. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wZEgtuzaZPswykiGEBxbz
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.
Closes #37 — and #37 named one of three.
It was three casts, not one
markdown.xsltreads three integers straight out of the document, and all three aborted thewhole conversion on a value that would not cast — exit 1, no output file, so in a batch the
document is lost rather than degraded:
w:ilvl/@w:valw:lvl/@w:ilvlw:gridSpan/@w:valEach contradicted the promise the stylesheet makes three lines above one of them — one
malformed document must never stop a corpus conversion — which the duplicate-
w:numIdandundefined-
numIdcases already keep.Probing for siblings before fixing is the habit this repository earned the hard way: the "one
definition of what counts as text" refactor found five readers where the commit message had
claimed four, and the fifth meant a release silently dropped characters.
Each now falls back exactly as its own absent-attribute case already did: level 0, a bullet, a
single cell.
Two engine constraints rule out the obvious spelling
Both are recorded in the code rather than left to be rediscovered:
[@w:ilvl castable as xs:integer and xs:integer(@w:ilvl) eq $ilvl]is wrong. XPath doesnot guarantee short-circuit evaluation of
and, so the cast may still run and still throw.[A][B]chained predicates are quadratic on this engine (phoenixmldb-xslt#10/#95;w:p[A][B]measured O(n²)).So it is one predicate using
if … then … else false(), where conditional evaluation issomething the spec actually promises.
Verified
Tests first, and they failed for the right reasons: three distinct line numbers in the thrown
XQueryException— 143, 172 and 400.Performance: undetectable, not "unchanged"
The guards sit on hot paths —
docmd:ilvlruns per list paragraph, thew:lvlpredicate perparagraph × per level — on documents of ~46,000 paragraphs, so this was worth measuring rather
than assuming.
Guarded runs span 382k–404k (5.6%) and the baseline falls inside that range; the fastest
guarded run beats the unguarded baseline. The honest claim is that any effect is below this
machine's noise floor, not that there is none. Measured now deliberately, because the
performance work landing next release would otherwise mask a regression introduced here.
The C# side was checked and is clean: the only
int.Parsereads docmd's own frontmatter output,never document input.
🤖 Generated with Claude Code
https://claude.ai/code/session_018wZEgtuzaZPswykiGEBxbz