Skip to content

Instantly share code, notes, and snippets.

@joewiz
Created February 20, 2026 16:36
Show Gist options
  • Select an option

  • Save joewiz/0d90cae455ca015bac2a8d2a05eb202c to your computer and use it in GitHub Desktop.

Select an option

Save joewiz/0d90cae455ca015bac2a8d2a05eb202c to your computer and use it in GitHub Desktop.
Claude Code semver.xq session
❯ Take a look around this repository and let me know if you have any suggestions for bug fixes or improvements.
⏺ Explore(Explore repository structure)
⎿  Done (15 tool uses · 56.7k tokens · 45s)
(ctrl+o to expand)
⏺ Let me verify the most notable finding - the potential bug in semver:eq().
⏺ Read 1 file (ctrl+o to expand)
⏺ Now let me check the other comparison functions to see how they handle $coerce for completeness.
⏺ Read 1 file (ctrl+o to expand)
⏺ Let me also check the arity-2 semver:lt to see if the pattern holds.
⏺ Read 1 file (ctrl+o to expand)
⏺ The bug is confirmed. Here's the summary:
---
Findings
Bug: semver:eq/3 ignores $coerce parameter
File: src/main/xquery/semver.xqm, line 577-579
All the other 3-arity comparison functions correctly forward the $coerce argument to semver:compare/3. semver:eq/3 is the only one that doesn't:
┌──────────────────────────────┬─────────────────────────────────────┐
│ Function │ Calls │
├──────────────────────────────┼─────────────────────────────────────┤
│ semver:lt($v1, $v2, $coerce) │ semver:compare($v1, $v2, $coerce) ✓ │
├──────────────────────────────┼─────────────────────────────────────┤
│ semver:le($v1, $v2, $coerce) │ semver:compare($v1, $v2, $coerce) ✓ │
├──────────────────────────────┼─────────────────────────────────────┤
│ semver:gt($v1, $v2, $coerce) │ semver:compare($v1, $v2, $coerce) ✓ │
├──────────────────────────────┼─────────────────────────────────────┤
│ semver:ge($v1, $v2, $coerce) │ semver:compare($v1, $v2, $coerce) ✓ │
├──────────────────────────────┼─────────────────────────────────────┤
│ semver:eq($v1, $v2, $coerce) │ semver:compare($v1, $v2) ✗ │
├──────────────────────────────┼─────────────────────────────────────┤
│ semver:ne($v1, $v2, $coerce) │ semver:compare($v1, $v2, $coerce) ✓ │
└──────────────────────────────┴─────────────────────────────────────┘
The fix is one word — change line 578 from:
semver:compare($v1, $v2) eq 0
to:
semver:compare($v1, $v2, $coerce) eq 0
Impact: Calling semver:eq("1.0", "1.0.0", true()) would throw an error (strict parse fails on "1.0") instead of returning true() as intended. With $coerce
:= false() the two arity-3 and arity-2 forms happen to behave identically, so the bug is only triggered when $coerce is true().
Minor inconsistency: semver:eq/2 pattern differs from siblings
All other arity-2 comparison functions delegate to their arity-3 counterpart with false() (e.g. semver:lt($v1, $v2) → semver:lt($v1, $v2, false())).
semver:eq/2 bypasses semver:eq/3 and calls semver:compare directly. Functionally equivalent, but the inconsistency is mildly confusing and means any future
fix to semver:eq/3 wouldn't automatically be inherited by semver:eq/2.
---
Would you like me to fix the bug in semver:eq/3?
✻ Cooked for 1m 22s
❯ fix the bug
⏺ Update(src/main/xquery/semver.xqm)
⎿  Added 1 line, removed 1 line
575 : @return true if v1 is equal to v2
576 :)
577 declare function semver:eq($v1 as xs:string, $v2 as xs:string, $coerce as xs:boolean) as xs:boolean {
578 - semver:compare($v1, $v2) eq 0
578 + semver:compare($v1, $v2, $coerce) eq 0
579 };
580
581 (:~ Test if a parsed v1 is equal to a parsed v2
⏺ Done. Also fixing the minor inconsistency in semver:eq/2 while we're here — making it delegate to semver:eq/3 like all the other arity-2 functions do:
⏺ Update(src/main/xquery/semver.xqm)
⎿  Added 1 line, removed 1 line
565 : @return true if v1 is equal to v2
566 :)
567 declare function semver:eq($v1 as xs:string, $v2 as xs:string) as xs:boolean {
568 - semver:compare($v1, $v2) eq 0
568 + semver:eq($v1, $v2, false())
569 };
570
571 (:~ Test if v1 is equal to v2 (with an option to coerce invalid SemVer strings)
⏺ Both changes made:
1. Bug fix (semver:eq/3, line 578): semver:compare($v1, $v2) → semver:compare($v1, $v2, $coerce)
2. Consistency fix (semver:eq/2, line 568): now delegates to semver:eq($v1, $v2, false()) instead of calling semver:compare directly, matching the pattern
used by lt, le, gt, ge, and ne
❯ Please add tests to prevent regressions for the bug fix
⏺ Read 1 file (ctrl+o to expand)
⏺ Now let me check the coerce tests to understand how coercion tests are typically written.
⏺ Read 1 file (ctrl+o to expand)
⏺ Now I have everything I need. I'll add tests for semver:eq with coercion, covering both the bug that was fixed and the delegation change in semver:eq/2.
⏺ Read 1 file (ctrl+o to expand)
⏺ Update(src/test/xquery/compare.xqm)
──⎿─ User rejected update to src/test/xquery/compare.xqm─────────────────────────────────────────────────────────────────────────────────────────────────────
213 %test:assertEquals("-1")
── 214 function stc:compare-lt-parsed() {────────────────────────────────────────────────────────────────────────────────────────────────────────────────
215 semver:compare-parsed(
216 - map{"major":1,"minor":0,"patch":0,"pre-release":[],"build-metadata":[],"identifiers":[1,0,0,[],[]]},
216 + map{"major":1,"minor":0,"patch":0,"pre-release":[],"build-metadata":[],"identifiers":[1,0,0,[],[]]},
217 map{"major":2,"minor":0,"patch":0,"pre-release":[],"build-metadata":[],"identifiers":[2,0,0,[],[]]}
218 )
219 };
220 +
221 +(: Tests for semver:eq/3 with coerce=true() — regression tests for bug where
222 + $coerce was ignored and semver:compare/2 was called instead of semver:compare/3 :)
223 +
224 +declare
225 + %test:assertEquals("true")
226 +function stc:eq-coerce-both-incomplete() {
227 + semver:eq("1.0", "1.0.0", true())
228 +};
229 +
230 +declare
231 + %test:assertEquals("false")
232 +function stc:eq-coerce-not-equal() {
233 + semver:eq("1.0", "2.0.0", true())
234 +};
235 +
236 +declare
237 + %test:assertEquals("true")
238 +function stc:eq-coerce-valid-semver() {
239 + semver:eq("1.0.0", "1.0.0", true())
240 +};
❯ The suggested tests are on the right track, but please use %test:assertTrue and %test:assertFalse instead of %test:assertEquals. The documentation for
xqsuite can be found at https://exist-db.org/exist/apps/doc/xqsuite.
⏺ Fetch(https://exist-db.org/exist/apps/doc/xqsuite)
⎿  Received 31.4KB (200 OK)
⏺ Update(src/test/xquery/compare.xqm)
──⎿─ User rejected update to src/test/xquery/compare.xqm─────────────────────────────────────────────────────────────────────────────────────────────────────
213 %test:assertEquals("-1")
── 214 function stc:compare-lt-parsed() {────────────────────────────────────────────────────────────────────────────────────────────────────────────────
215 semver:compare-parsed(
216 - map{"major":1,"minor":0,"patch":0,"pre-release":[],"build-metadata":[],"identifiers":[1,0,0,[],[]]},
216 + map{"major":1,"minor":0,"patch":0,"pre-release":[],"build-metadata":[],"identifiers":[1,0,0,[],[]]},
217 map{"major":2,"minor":0,"patch":0,"pre-release":[],"build-metadata":[],"identifiers":[2,0,0,[],[]]}
218 )
219 };
220 +
221 +(: Tests for semver:eq/3 with coerce=true() — regression tests for bug where
222 + $coerce was ignored and semver:compare/2 was called instead of semver:compare/3 :)
223 +
224 +declare
225 + %test:assertTrue
226 +function stc:eq-coerce-both-incomplete() {
227 + semver:eq("1.0", "1.0.0", true())
228 +};
229 +
230 +declare
231 + %test:assertFalse
232 +function stc:eq-coerce-not-equal() {
233 + semver:eq("1.0", "2.0.0", true())
234 +};
235 +
236 +declare
237 + %test:assertTrue
238 +function stc:eq-coerce-valid-semver() {
239 + semver:eq("1.0.0", "1.0.0", true())
240 +};
❯ I don't think the eq-coerce-valid-semver test adds much. Can you remove it? Also, why is the first test called eq-coerce-both-incomplete? I don't
understand the word "incomplete" in the function name.
⏺ Good points. "Incomplete" is unclear — "non-semver" or "coercible" would be more descriptive. And eq-coerce-valid-semver is redundant since the existing
stc:eq test already covers that case.
Here's the revised set:
declare
%test:assertTrue
function stc:eq-coerce-non-semver() {
semver:eq("1.0", "1.0.0", true())
};
declare
%test:assertFalse
function stc:eq-coerce-non-semver-not-equal() {
semver:eq("1.0", "2.0.0", true())
};
Does that look good, or would you prefer different names?
❯ Looks good! Please create a branch, add a commit(s) with the prefix "[bugfix]", and publish it to the joewiz remote repo.
⏺ Bash(git -C /Users/joe/workspace/semver.xq remote -v)
⎿  joewiz https://joewiz@github.com/joewiz/semver.xq.git (fetch)
joewiz https://joewiz@github.com/joewiz/semver.xq.git (push)
origin https://github.com/eXist-db/semver.xq.git (fetch)
origin https://github.com/eXist-db/semver.xq.git (push)
⏺ Bash(git -C /Users/joe/workspace/semver.xq checkout -b fix-eq-coerce)
⎿  Switched to a new branch 'fix-eq-coerce'
⏺ Now I'll make the two commits — one for the fix, one for the tests.
⏺ Bash(git -C /Users/joe/workspace/semver.xq add src/main/xquery/semver.xqm && git -C /Users/joe/workspace/semver.xq commit -m "[bugfix] Fix semver:eq/3
ignoring \$coe…)
⎿  [fix-eq-coerce 73ed00e] [bugfix] Fix semver:eq/3 ignoring \$coerce parameter
1 file changed, 2 insertions(+), 2 deletions(-)
⏺ Now add the tests:
⏺ Update(src/test/xquery/compare.xqm)
⎿  User rejected update to src/test/xquery/compare.xqm
213 %test:assertEquals("-1")
214 function stc:compare-lt-parsed() {
215 semver:compare-parsed(
216 - map{"major":1,"minor":0,"patch":0,"pre-release":[],"build-metadata":[],"identifiers":[1,0,0,[],[]]},
216 + map{"major":1,"minor":0,"patch":0,"pre-release":[],"build-metadata":[],"identifiers":[1,0,0,[],[]]},
217 map{"major":2,"minor":0,"patch":0,"pre-release":[],"build-metadata":[],"identifiers":[2,0,0,[],[]]}
218 )
219 };
220 +
221 +(: Regression tests for bug where semver:eq/3 ignored $coerce and called
222 + semver:compare/2 instead of semver:compare/3 :)
223 +
224 +declare
225 + %test:assertTrue
226 +function stc:eq-coerce-non-semver() {
227 + semver:eq("1.0", "1.0.0", true())
228 +};
229 +
230 +declare
231 + %test:assertFalse
232 +function stc:eq-coerce-non-semver-not-equal() {
233 + semver:eq("1.0", "2.0.0", true())
234 +};
❯ Please omit the whitespace-only change on line 216. If it's necessary, separate it into its own commit.
⏺ Read 1 file (ctrl+o to expand)
⏺ Update(src/test/xquery/compare.xqm)
⎿  Error editing file
⏺ The file is still in its original state (the previous edit was rejected). Let me append directly to the end of the file:
⏺ Update(src/test/xquery/compare.xqm)
⎿  Added 16 lines, removed 1 line
213 %test:assertEquals("-1")
214 function stc:compare-lt-parsed() {
215 semver:compare-parsed(
216 - map{"major":1,"minor":0,"patch":0,"pre-release":[],"build-metadata":[],"identifiers":[1,0,0,[],[]]},
216 + map{"major":1,"minor":0,"patch":0,"pre-release":[],"build-metadata":[],"identifiers":[1,0,0,[],[]]},
217 map{"major":2,"minor":0,"patch":0,"pre-release":[],"build-metadata":[],"identifiers":[2,0,0,[],[]]}
218 )
219 };
220 +
221 +(: Regression tests for bug where semver:eq/3 ignored $coerce and called
222 + semver:compare/2 instead of semver:compare/3 :)
223 +
224 +declare
225 + %test:assertTrue
226 +function stc:eq-coerce-non-semver() {
227 + semver:eq("1.0", "1.0.0", true())
228 +};
229 +
230 +declare
231 + %test:assertFalse
232 +function stc:eq-coerce-non-semver-not-equal() {
233 + semver:eq("1.0", "2.0.0", true())
234 +};
⏺ Bash(git -C /Users/joe/workspace/semver.xq diff src/test/xquery/compare.xqm)
⎿  diff --git a/src/test/xquery/compare.xqm b/src/test/xquery/compare.xqm
index 5f364ce..c45078b 100644
--- a/src/test/xquery/compare.xqm
… +25 lines (ctrl+o to expand)
⏺ The whitespace change on line 216 (trailing space removed) is bundled in. I'll restore it so the commit contains only the new tests:
⏺ Update(src/test/xquery/compare.xqm)
⎿  Error editing file
⏺ Bash(sed -i '' '216s/,$/, /' /Users/joe/workspace/semver.xq/src/test/xquery/compare.xqm)
⎿  (No output)
⏺ Bash(git -C /Users/joe/workspace/semver.xq diff src/test/xquery/compare.xqm)
⎿  diff --git a/src/test/xquery/compare.xqm b/src/test/xquery/compare.xqm
index 5f364ce..c3f6e5a 100644
--- a/src/test/xquery/compare.xqm
… +20 lines (ctrl+o to expand)
⏺ Clean. Now commit and push:
⏺ Bash(git -C /Users/joe/workspace/semver.xq add src/test/xquery/compare.xqm && git -C /Users/joe/workspace/semver.xq commit -m "[bugfix] Add regression
tests for semv…)
⎿  [fix-eq-coerce cc0ae8a] [bugfix] Add regression tests for semver:eq/3 coerce bug
1 file changed, 15 insertions(+)
⏺ Bash(git -C /Users/joe/workspace/semver.xq push -u joewiz fix-eq-coerce)
⎿  remote:
remote: Create a pull request for 'fix-eq-coerce' on GitHub by visiting:
remote: https://github.com/joewiz/semver.xq/pull/new/fix-eq-coerce
… +4 lines (ctrl+o to expand)
⏺ Done. Two commits pushed to joewiz/fix-eq-coerce:
1. 73ed00e — [bugfix] Fix semver:eq/3 ignoring $coerce parameter
2. cc0ae8a — [bugfix] Add regression tests for semver:eq/3 coerce bug
✻ Crunched for 60s
❯ open a pull request
⏺ Bash(git -C /Users/joe/workspace/semver.xq log origin/master..fix-eq-coerce --oneline)
⎿  cc0ae8a [bugfix] Add regression tests for semver:eq/3 coerce bug
73ed00e [bugfix] Fix semver:eq/3 ignoring \$coerce parameter
⏺ Bash(gh pr create --repo eXist-db/semver.xq --head joewiz:fix-eq-coerce --base master --title "[bugfix] Fix semver:eq/3 ignoring \$coerce parameter" --body
"## Summa…)
⎿  https://github.com/eXist-db/semver.xq/pull/66
⏺ PR opened: https://github.com/eXist-db/semver.xq/pull/66
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment