@pipeworx/mcp-edge-case-museum 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/README.md +17 -12
  2. package/bin/cli.js +55 -11
  3. package/lib/index.js +333 -0
  4. package/package.json +17 -16
  5. package/server.json +1 -1
  6. package/tools.json +98 -0
  7. package/src/bake.mjs +0 -112
  8. package/src/ecm-index-data.ts +0 -15
  9. package/src/exhibits/datetime/add-24-hours-vs-add-1-day-across-dst.json +0 -51
  10. package/src/exhibits/datetime/add-365-days-to-feb-29-not-next-feb-29.json +0 -49
  11. package/src/exhibits/datetime/age-calculator-feb29-birthday-crash-vs-silent.json +0 -52
  12. package/src/exhibits/datetime/century-leap-year-1900-not-leap.json +0 -68
  13. package/src/exhibits/datetime/datetime-utcnow-naive-deprecated.json +0 -39
  14. package/src/exhibits/datetime/dst-fall-back-ambiguous-fold.json +0 -51
  15. package/src/exhibits/datetime/dst-spring-forward-nonexistent-time.json +0 -57
  16. package/src/exhibits/datetime/fractional-utc-offsets-kathmandu-kolkata.json +0 -50
  17. package/src/exhibits/datetime/iso-week-year-vs-calendar-year.json +0 -48
  18. package/src/exhibits/datetime/js-date-field-overflow-rollover.json +0 -49
  19. package/src/exhibits/datetime/leap-second-rejected-vs-silently-invalid.json +0 -57
  20. package/src/exhibits/datetime/lord-howe-30-minute-dst.json +0 -47
  21. package/src/exhibits/datetime/midnight-that-doesnt-exist-sao-paulo.json +0 -53
  22. package/src/exhibits/datetime/python-naive-aware-comparison-typeerror.json +0 -46
  23. package/src/exhibits/datetime/samoa-skipped-day-2011.json +0 -50
  24. package/src/exhibits/datetime/southern-hemisphere-dst-reversed.json +0 -50
  25. package/src/exhibits/datetime/two-digit-year-century-pivot-mismatch.json +0 -49
  26. package/src/exhibits/datetime/year-2038-int32-overflow.json +0 -52
  27. package/src/exhibits/filenames/absolute-second-argument-resets-join.json +0 -62
  28. package/src/exhibits/filenames/bidi-override-extension-spoof.json +0 -58
  29. package/src/exhibits/filenames/case-insensitive-filesystem-pure-path-mismatch.json +0 -74
  30. package/src/exhibits/filenames/dotfile-has-no-extension.json +0 -61
  31. package/src/exhibits/filenames/extension-trap-double-extension.json +0 -62
  32. package/src/exhibits/filenames/filename-length-limits-bytes-not-chars.json +0 -66
  33. package/src/exhibits/filenames/leading-dash-filename-parsed-as-flag.json +0 -67
  34. package/src/exhibits/filenames/nfc-nfd-filename-lookup-mismatch.json +0 -68
  35. package/src/exhibits/filenames/ntfs-alternate-data-stream-colon.json +0 -59
  36. package/src/exhibits/filenames/ntpath-splitdrive-unc-paths.json +0 -58
  37. package/src/exhibits/filenames/nul-byte-in-filename-raises-at-open.json +0 -60
  38. package/src/exhibits/filenames/path-join-escapes-the-base-directory.json +0 -63
  39. package/src/exhibits/filenames/windows-backslash-traversal-ntpath.json +0 -62
  40. package/src/exhibits/filenames/windows-reserved-device-names.json +0 -73
  41. package/src/exhibits/filenames/windows-trailing-dot-space-stripped.json +0 -77
  42. package/src/exhibits/float-money/decimal-from-float-vs-string.json +0 -66
  43. package/src/exhibits/float-money/float-anchor-0-1-plus-0-2.json +0 -67
  44. package/src/exhibits/float-money/intl-number-format-de-de-roundtrip-misparsed.json +0 -62
  45. package/src/exhibits/float-money/iso4217-minor-units-not-always-two.json +0 -78
  46. package/src/exhibits/float-money/math-abs-negative-zero.json +0 -51
  47. package/src/exhibits/float-money/max-safe-integer-plus-two.json +0 -62
  48. package/src/exhibits/float-money/naive-loop-sum-order-dependence.json +0 -71
  49. package/src/exhibits/float-money/nan-breaks-equality-and-max-min.json +0 -70
  50. package/src/exhibits/float-money/negative-zero-balance-formatting.json +0 -74
  51. package/src/exhibits/float-money/number-epsilon-false-positive-small-magnitude.json +0 -52
  52. package/src/exhibits/float-money/parsefloat-comma-thousands-separator.json +0 -66
  53. package/src/exhibits/float-money/python-banker-rounding-vs-js-round-half-up.json +0 -65
  54. package/src/exhibits/float-money/python-round-2-675-float-imprecision.json +0 -64
  55. package/src/exhibits/float-money/splitting-100-dollars-three-ways-leaves-a-cent.json +0 -63
  56. package/src/exhibits/float-money/sum-ten-dimes-not-a-dollar.json +0 -77
  57. package/src/exhibits/float-money/tofixed-1-005-rounds-down-not-up.json +0 -63
  58. package/src/exhibits/float-money/tofixed-2-345-inconsistent-rounding.json +0 -64
  59. package/src/exhibits/integers/array-index-above-2-32-2-not-an-index.json +0 -42
  60. package/src/exhibits/integers/bigint-number-mixing-typeerror.json +0 -49
  61. package/src/exhibits/integers/bitwise-32-bit-truncation.json +0 -56
  62. package/src/exhibits/integers/bool-is-int-subclass-dict-key-collision.json +0 -58
  63. package/src/exhibits/integers/floor-div-mod-sign-python-vs-js.json +0 -48
  64. package/src/exhibits/integers/json-parse-loses-id-precision.json +0 -58
  65. package/src/exhibits/integers/max-safe-integer-plus-one-equals-plus-two.json +0 -58
  66. package/src/exhibits/integers/number-empty-and-whitespace-string-equals-zero.json +0 -55
  67. package/src/exhibits/integers/parseint-null-radix36-digit-soup.json +0 -57
  68. package/src/exhibits/integers/parseint-number-argument-scientific-notation.json +0 -48
  69. package/src/exhibits/integers/parseint-stops-at-first-invalid-char-vs-number.json +0 -58
  70. package/src/exhibits/integers/python-int-accepts-fullwidth-unicode-digits.json +0 -56
  71. package/src/exhibits/integers/python-int-accepts-underscores-and-whitespace.json +0 -48
  72. package/src/exhibits/integers/python-unbounded-int-vs-fixed-width-wrap.json +0 -49
  73. package/src/exhibits/names-addresses/address-with-no-street-number.json +0 -54
  74. package/src/exhibits/names-addresses/ascii-regex-rejects-accented-and-hyphenated-names.json +0 -55
  75. package/src/exhibits/names-addresses/house-number-suffix-collapsed-by-digit-extraction.json +0 -55
  76. package/src/exhibits/names-addresses/mononym-fails-first-last-required.json +0 -56
  77. package/src/exhibits/names-addresses/naive-lowercase-breaks-greek-final-sigma.json +0 -64
  78. package/src/exhibits/names-addresses/null-surname-coerced-to-null-value.json +0 -58
  79. package/src/exhibits/names-addresses/single-letter-surname-fails-min-length.json +0 -55
  80. package/src/exhibits/names-addresses/sort-by-surname-particle-convention-mismatch.json +0 -57
  81. package/src/exhibits/names-addresses/spanish-double-surname-treated-as-single.json +0 -55
  82. package/src/exhibits/names-addresses/split-surname-on-last-space-loses-particle.json +0 -59
  83. package/src/exhibits/names-addresses/title-case-mangles-mixed-case-and-apostrophes.json +0 -60
  84. package/src/exhibits/names-addresses/uk-canada-postal-code-assumed-numeric.json +0 -55
  85. package/src/exhibits/names-addresses/us-zip-code-leading-zero-lost-as-integer.json +0 -58
  86. package/src/exhibits/names-addresses/varchar-truncation-splits-name-mid-grapheme.json +0 -63
  87. package/src/exhibits/parsing/csv-crlf-trailing-blank-line.json +0 -87
  88. package/src/exhibits/parsing/csv-doubled-quote-escaping.json +0 -70
  89. package/src/exhibits/parsing/csv-formula-injection-excel.json +0 -71
  90. package/src/exhibits/parsing/csv-quoted-field-with-comma.json +0 -70
  91. package/src/exhibits/parsing/csv-quoted-field-with-newline.json +0 -70
  92. package/src/exhibits/parsing/csv-semicolon-delimited-european-excel.json +0 -70
  93. package/src/exhibits/parsing/csv-utf8-bom-first-header.json +0 -74
  94. package/src/exhibits/parsing/email-regex-rejects-valid-apostrophe-address.json +0 -68
  95. package/src/exhibits/parsing/json-big-integer-precision-loss.json +0 -65
  96. package/src/exhibits/parsing/json-duplicate-keys-last-wins.json +0 -64
  97. package/src/exhibits/parsing/json-lone-surrogate-roundtrip.json +0 -58
  98. package/src/exhibits/parsing/json-nan-infinity-not-valid-json.json +0 -54
  99. package/src/exhibits/parsing/url-backslash-treated-as-slash-whatwg.json +0 -60
  100. package/src/exhibits/parsing/url-urlparse-never-raises-on-garbage.json +0 -60
  101. package/src/exhibits/parsing/url-userinfo-trusted-looking-host.json +0 -60
  102. package/src/exhibits/sorting/accented-letters-sort-after-z.json +0 -63
  103. package/src/exhibits/sorting/byte-order-caps-before-lowercase.json +0 -66
  104. package/src/exhibits/sorting/case-insensitive-sort-lower-vs-casefold.json +0 -56
  105. package/src/exhibits/sorting/comparator-returns-boolean-js.json +0 -49
  106. package/src/exhibits/sorting/js-array-sort-stringifies-numbers.json +0 -68
  107. package/src/exhibits/sorting/locale-collation-de-vs-sv.json +0 -54
  108. package/src/exhibits/sorting/localecompare-numeric-option.json +0 -50
  109. package/src/exhibits/sorting/nan-in-sort.json +0 -59
  110. package/src/exhibits/sorting/natural-sort-breaks-lexicographic.json +0 -63
  111. package/src/exhibits/sorting/postgres-collation-documented.json +0 -61
  112. package/src/exhibits/sorting/python-cannot-compare-str-int.json +0 -70
  113. package/src/exhibits/sorting/reverse-sort-vs-reverse-after-sort-ties.json +0 -48
  114. package/src/exhibits/sorting/sorting-mixed-none-null.json +0 -56
  115. package/src/exhibits/sorting/typed-array-sort-is-numeric-by-default.json +0 -48
  116. package/src/exhibits/unicode/arabic-indic-digit-parses.json +0 -73
  117. package/src/exhibits/unicode/bidi-override-filename-spoof.json +0 -62
  118. package/src/exhibits/unicode/cyrillic-homoglyph-paypal-spoof.json +0 -67
  119. package/src/exhibits/unicode/fullwidth-bypasses-keyword-filter.json +0 -61
  120. package/src/exhibits/unicode/german-eszett-case-fold.json +0 -72
  121. package/src/exhibits/unicode/grapheme-vs-codepoint-length.json +0 -57
  122. package/src/exhibits/unicode/greek-final-sigma-casefold.json +0 -54
  123. package/src/exhibits/unicode/js-word-regex-ascii-only.json +0 -64
  124. package/src/exhibits/unicode/nfc-nfd-same-name-two-accounts.json +0 -70
  125. package/src/exhibits/unicode/nfkc-compatibility-folding.json +0 -63
  126. package/src/exhibits/unicode/truncation-orphans-combining-mark.json +0 -62
  127. package/src/exhibits/unicode/truncation-splits-surrogate-pair.json +0 -65
  128. package/src/exhibits/unicode/turkish-dotless-i-case-mapping.json +0 -72
  129. package/src/exhibits/unicode/zalgo-combining-mark-stacking.json +0 -57
  130. package/src/exhibits/unicode/zero-width-space-username.json +0 -62
  131. package/src/harness.mjs +0 -331
  132. package/src/index.ts +0 -686
  133. package/src/server.ts +0 -49
  134. package/src/verified.json +0 -977
  135. package/tsconfig.json +0 -18
@@ -1,50 +0,0 @@
1
- {
2
- "id": "localecompare-numeric-option",
3
- "title": "There's a one-word fix for natural sort in JavaScript, and it's off by default",
4
- "category": "sorting",
5
- "assumption": "Fixing \"file10 sorts before file2\" requires writing your own digit-splitting comparator.",
6
- "input": {
7
- "display": "[\"file10.txt\", \"file2.txt\", \"file1.txt\"].sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))",
8
- "description": "The same filename-sorting problem as the plain natural-sort exhibit, this time looking for the fix rather than the bug."
9
- },
10
- "what_goes_wrong": "Passing { numeric: true } as an option to localeCompare correctly returns [\"file1.txt\", \"file2.txt\", \"file10.txt\"] with no custom parsing code at all — but it is not the default for localeCompare, not the default for Array.prototype.sort(), and has no equivalent option anywhere in Python's standard library sorted(); Python's locale.strxfrm (the one built-in collation hook Python has) does not implement it either and returns the same wrong lexicographic order as a plain sort.",
11
- "why": "ECMA-402 (the Intl part of JavaScript) defines a numeric collation option (the Unicode locale extension \"kn\") that makes the collator compare runs of decimal digits by their numeric value instead of character by character, and String.prototype.localeCompare accepts the same options object as Intl.Collator. It is opt-in because it is a tailoring on top of the default Unicode Collation Algorithm ordering, not the default. Python ships no Unicode collation implementation: locale.strxfrm only applies whatever collation the operating system's locale provides, and the common C-library locales have no numeric-run option, so in Python the digit-run comparison has to be hand-written.",
12
- "affects": [
13
- "javascript"
14
- ],
15
- "repros": [
16
- {
17
- "language": "python",
18
- "code": "import locale\nfiles = [\"file10.txt\", \"file2.txt\", \"file1.txt\"]\nprint(sorted(files, key=locale.strxfrm))\n",
19
- "expected_output": "['file1.txt', 'file10.txt', 'file2.txt']",
20
- "verified_with": "Python 3.14.2"
21
- },
22
- {
23
- "language": "javascript",
24
- "code": "const files = [\"file10.txt\", \"file2.txt\", \"file1.txt\"];\nconsole.log(files.slice().sort((a, b) => a.localeCompare(b, undefined, { numeric: true })));\n",
25
- "expected_output": "[ 'file1.txt', 'file2.txt', 'file10.txt' ]",
26
- "verified_with": "Node.js v22.22.2"
27
- }
28
- ],
29
- "fix": "In JavaScript, pass { numeric: true } to localeCompare (or construct one Intl.Collator with that option and reuse its compare for the whole sort, which is faster). It only treats runs of digits as non-negative integers: signs, decimal points and thousands separators are still compared as characters. In Python, write a natural-sort key that splits each string on digit runs with re.split(r\"(\\d+)\", s) and converts the digit groups to int, since there is no built-in numeric collation to opt into.",
30
- "related": [
31
- "natural-sort-breaks-lexicographic"
32
- ],
33
- "sources": [
34
- {
35
- "title": "ECMA-402: Intl.Collator, the numeric option",
36
- "url": "https://tc39.es/ecma402/#sec-intl-collator-constructor"
37
- },
38
- {
39
- "title": "MDN: String.prototype.localeCompare()",
40
- "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare"
41
- }
42
- ],
43
- "tags": [
44
- "sorting",
45
- "comparison",
46
- "names",
47
- "display",
48
- "pagination"
49
- ]
50
- }
@@ -1,59 +0,0 @@
1
- {
2
- "id": "nan-in-sort",
3
- "title": "One NaN can leave the rest of the list unsorted",
4
- "category": "sorting",
5
- "assumption": "A NaN in a numeric list just ends up somewhere predictable, and every other value still gets sorted.",
6
- "input": {
7
- "display": "sorted([1, 2, 3, nan]), sorted([nan, 1, 2, 3]) and sorted([3, nan, 1])",
8
- "description": "A numeric column (a score, a computed ratio, a percentage with a 0/0 somewhere) that can legitimately contain NaN, sorted for a leaderboard or report."
9
- },
10
- "what_goes_wrong": "Where NaN lands depends on where it started ([1, 2, 3, nan] stays as is, [nan, 1, 2, 3] stays as is), and worse, sorted([3, nan, 1]) returns [3, nan, 1]: the numbers 3 and 1 are left out of order. JavaScript's sort with (a, b) => a - b returned [3, NaN, 1] for the same input in Node.js.",
11
- "why": "IEEE 754 makes every ordered comparison with NaN false, so to Python's sort NaN looks equal to everything (neither NaN < x nor x < NaN). In JavaScript, a - b is NaN whenever either side is NaN, and the spec turns a NaN comparator result into 0, which also means \"equal\". Equality with everything breaks transitivity: 3 \"equals\" NaN and NaN \"equals\" 1, but 3 is not equal to 1. Sorting algorithms rely on transitivity to avoid comparing every pair, so they can conclude [3, nan, 1] is already in order. Python's sort makes no promise for inconsistent comparisons, and the ECMAScript spec says the order is implementation-defined when the comparator is not consistent.",
12
- "affects": [
13
- "python",
14
- "javascript"
15
- ],
16
- "repros": [
17
- {
18
- "language": "python",
19
- "code": "nan = float(\"nan\")\nprint(sorted([1, 2, 3, nan]))\nprint(sorted([nan, 1, 2, 3]))\nprint(sorted([3, nan, 1]))\n",
20
- "expected_output": "[1, 2, 3, nan]\n[nan, 1, 2, 3]\n[3, nan, 1]",
21
- "verified_with": "Python 3.14.2"
22
- },
23
- {
24
- "language": "javascript",
25
- "code": "const byNum = (a, b) => a - b;\nconsole.log([3, 1, NaN, 2].sort(byNum));\nconsole.log([NaN, 3, 1, 2].sort(byNum));\nconsole.log([3, NaN, 1].sort(byNum));\n",
26
- "expected_output": "[ 1, 2, 3, NaN ]\n[ NaN, 1, 2, 3 ]\n[ 3, NaN, 1 ]",
27
- "verified_with": "Node.js v22.22.2"
28
- }
29
- ],
30
- "fix": "Remove or partition NaN before sorting rather than letting it reach the comparison. Decide whether NaN goes first, last, or is excluded, and encode that directly: in Python, key=lambda x: (math.isnan(x), x) puts NaN last; in JavaScript, a comparator that checks Number.isNaN() on both sides before subtracting.",
31
- "related": [
32
- "comparator-returns-boolean-js"
33
- ],
34
- "sources": [
35
- {
36
- "title": "Python docs: Sorting Techniques",
37
- "url": "https://docs.python.org/3/howto/sorting.html"
38
- },
39
- {
40
- "title": "Python docs: math.isnan",
41
- "url": "https://docs.python.org/3/library/math.html#math.isnan"
42
- },
43
- {
44
- "title": "ECMAScript spec: SortIndexedProperties (implementation-defined order for inconsistent comparators)",
45
- "url": "https://tc39.es/ecma262/#sec-sortindexedproperties"
46
- },
47
- {
48
- "title": "ECMAScript spec: CompareArrayElements (NaN result treated as +0)",
49
- "url": "https://tc39.es/ecma262/#sec-comparearrayelements"
50
- }
51
- ],
52
- "tags": [
53
- "sorting",
54
- "comparison",
55
- "display",
56
- "pagination"
57
- ],
58
- "test_cases": null
59
- }
@@ -1,63 +0,0 @@
1
- {
2
- "id": "natural-sort-breaks-lexicographic",
3
- "title": "\"file10.txt\" sorts before \"file2.txt\"",
4
- "category": "sorting",
5
- "assumption": "Sorting a list of filenames (or any strings with embedded numbers) alphabetically also puts them in numeric order for the number part.",
6
- "input": {
7
- "display": "[\"file10.txt\", \"file2.txt\", \"file1.txt\"]",
8
- "description": "A directory listing, a set of uploaded files (page-01.jpg, page-02.jpg, ..., page-10.jpg), or any auto-numbered set of names sorted with a plain string comparison."
9
- },
10
- "what_goes_wrong": "A plain sort returns [\"file1.txt\", \"file10.txt\", \"file2.txt\"] — \"file10.txt\" is placed between \"file1.txt\" and \"file2.txt\", because standard string comparison never looks at the embedded number as a number at all.",
11
- "why": "String comparison walks the two strings character by character until it finds a difference. \"file1\" and \"file10\" and \"file2\" all share the prefix \"file\"; comparing position by position, \"1\" < \"2\" decides \"file10\" < \"file2\" before the comparison ever reaches the second digit of \"10\" — the strings are compared as sequences of characters, not parsed into a text part and a numeric part the way a human reads them. This is true identically in Python and JavaScript, because both default to the same kind of character-by-character comparison; neither treats a run of digits as a single unit unless you tell it to.",
12
- "affects": [
13
- "python",
14
- "javascript"
15
- ],
16
- "repros": [
17
- {
18
- "language": "python",
19
- "code": "files = [\"file10.txt\", \"file2.txt\", \"file1.txt\"]\nprint(sorted(files))\n",
20
- "expected_output": "['file1.txt', 'file10.txt', 'file2.txt']",
21
- "verified_with": "Python 3.14.2"
22
- },
23
- {
24
- "language": "javascript",
25
- "code": "const files = [\"file10.txt\", \"file2.txt\", \"file1.txt\"];\nconsole.log(files.slice().sort());\n",
26
- "expected_output": "[ 'file1.txt', 'file10.txt', 'file2.txt' ]",
27
- "verified_with": "Node.js v22.22.2"
28
- }
29
- ],
30
- "fix": "Use a \"natural sort\": split each string into alternating text/number runs and compare the number runs numerically (JavaScript has this built into localeCompare's {numeric: true} option; Python's standard library has no equivalent and needs a hand-written key function, e.g. splitting on digit runs with re.split and converting the digit groups to int).",
31
- "related": [
32
- "js-array-sort-stringifies-numbers",
33
- "localecompare-numeric-option"
34
- ],
35
- "sources": [
36
- {
37
- "title": "Unicode Technical Standard #10: Unicode Collation Algorithm",
38
- "url": "https://www.unicode.org/reports/tr10/"
39
- },
40
- {
41
- "title": "Python docs: sorted() / list.sort()",
42
- "url": "https://docs.python.org/3/library/stdtypes.html#list.sort"
43
- }
44
- ],
45
- "tags": [
46
- "sorting",
47
- "comparison",
48
- "names",
49
- "display",
50
- "pagination"
51
- ],
52
- "test_cases": [
53
- {
54
- "inputs": [
55
- "file10.txt",
56
- "file2.txt"
57
- ],
58
- "relation": "returns",
59
- "expected": false,
60
- "note": "their_function(a, b) as a natural-order \"a sorts before b\" test should return false: file2 belongs before file10. Plain `a < b` returns true, which is the bug."
61
- }
62
- ]
63
- }
@@ -1,61 +0,0 @@
1
- {
2
- "id": "postgres-collation-documented",
3
- "title": "ORDER BY in Postgres can return a different row order than the exact same query on a different server",
4
- "category": "sorting",
5
- "assumption": "ORDER BY on a text column is a property of the SQL and the data, so the same query against the same rows always returns the same order.",
6
- "input": {
7
- "display": "SELECT name FROM users ORDER BY name;",
8
- "description": "A plain text ORDER BY on a column using the database's default collation, run against two Postgres instances whose cluster was initialized with different default collations (e.g. \"C\" vs \"en_US.utf8\" vs an ICU collation)."
9
- },
10
- "what_goes_wrong": "The identical query on identical rows returns a different order on a database whose collation is \"C\" (byte order: case-sensitive, uppercase before lowercase, accented letters after z) than on one using an \"en_US\" libc or ICU collation (a, ä, Z interleaved by letter). Restoring or migrating into a database with a different default collation, or upgrading the operating system's locale data under the same collation name, can change ORDER BY results without changing a row of data or a line of SQL.",
11
- "why": "Postgres orders text using a collation. Each text column has one, which defaults to the database's collation, and a query can override it with COLLATE. The database default is fixed when the database is created, and the actual ordering comes from either the C library's locale data (the libc provider) or ICU (supported since PostgreSQL 10). So the same SQL orders differently in databases with different collations, and even the same collation name, such as en_US.utf8, can order differently after an operating-system upgrade changes the C library's locale data (the glibc 2.28 change is the best-known case, and it also left existing text indexes inconsistent). A pg_dump made without --create does not carry the database's default collation, so restoring it into a database created with a different default changes every un-pinned text sort.",
12
- "affects": [
13
- "postgres"
14
- ],
15
- "repros": [
16
- {
17
- "language": "python",
18
- "code": "words = [\"a\", \"Z\", \"\\u00e4\"]\nprint(sorted(words))\n",
19
- "expected_output": "['Z', 'a', 'ä']",
20
- "verified_with": "Python 3.14.2"
21
- },
22
- {
23
- "language": "javascript",
24
- "code": "const words = [\"a\", \"Z\", \"\\u00e4\"];\nconsole.log(words.slice().sort());\nconsole.log(words.slice().sort((a, b) => a.localeCompare(b, \"en-US\")));\n",
25
- "expected_output": "[ 'Z', 'a', 'ä' ]\n[ 'a', 'ä', 'Z' ]",
26
- "verified_with": "Node.js v22.22.2"
27
- },
28
- {
29
- "language": "postgres",
30
- "status": "documented",
31
- "note": "SELECT name FROM (VALUES ('a'),('Z'),('ä')) AS t(name) ORDER BY name COLLATE \"C\"; returns Z, a, ä (UTF-8 byte order, matching the plain code-point sorts in the Python/JS repros above). The same query with COLLATE \"en-US-x-icu\" (available when PostgreSQL is built with ICU) returns a, ä, Z, matching the localeCompare repro above. Without a COLLATE clause the result depends on the database's collation, which you can see with SELECT datcollate FROM pg_database WHERE datname = current_database();",
32
- "source": "https://www.postgresql.org/docs/current/collation.html"
33
- }
34
- ],
35
- "fix": "Pin collation explicitly with COLLATE in any ORDER BY whose result order matters across environments (tests comparing row order, pagination cursors, anything exported and diffed). Check the database collation with SELECT datcollate FROM pg_database WHERE datname = current_database();. Treat a restore onto a server with different locale or ICU data, or an OS upgrade that changes glibc, as a behaviour change for every un-pinned text sort, and reindex text indexes after such an upgrade.",
36
- "related": [
37
- "accented-letters-sort-after-z",
38
- "byte-order-caps-before-lowercase"
39
- ],
40
- "sources": [
41
- {
42
- "title": "PostgreSQL docs: Collation Support",
43
- "url": "https://www.postgresql.org/docs/current/collation.html"
44
- },
45
- {
46
- "title": "PostgreSQL wiki: Locale data changes",
47
- "url": "https://wiki.postgresql.org/wiki/Locale_data_changes"
48
- },
49
- {
50
- "title": "PostgreSQL docs: pg_dump (--create)",
51
- "url": "https://www.postgresql.org/docs/current/app-pgdump.html"
52
- }
53
- ],
54
- "tags": [
55
- "sorting",
56
- "comparison",
57
- "database",
58
- "pagination",
59
- "i18n"
60
- ]
61
- }
@@ -1,70 +0,0 @@
1
- {
2
- "id": "python-cannot-compare-str-int",
3
- "title": "Python crashes sorting [2, \"10\", 1]; JavaScript quietly reshuffles it",
4
- "category": "sorting",
5
- "assumption": "A list with mixed number and numeric-string types is a data-cleanliness smell, not something that changes which language's sort crashes versus which one silently gives you a wrong-looking answer.",
6
- "input": {
7
- "display": "[2, \"10\", 1, \"3\"] — a list mixing actual numbers and numbers-as-strings",
8
- "description": "A column assembled from two sources that disagree on type — one API returns numeric IDs as JSON numbers, another returns them as strings — merged into one list and sorted without normalizing types first."
9
- },
10
- "what_goes_wrong": "Python's sorted([2, \"10\", 1, \"3\"]) raises TypeError: Python 3 has no ordering between int and str. JavaScript's [2, \"10\", 1, \"3\"].sort() runs with no error and returns [1, \"10\", 2, \"3\"], an order that is neither numeric nor meaningful, produced silently.",
11
- "why": "For a < b, Python first tries a.__lt__(b) and, if that returns NotImplemented, the reflected b.__gt__(a). int.__lt__ does not accept a str and str.__gt__ does not accept an int, so both return NotImplemented and Python raises TypeError rather than guess. (Python 2 did order mixed types, by type name; Python 3 removed that.) JavaScript's default Array.prototype.sort() converts every element with ToString before comparing, so 2 becomes \"2\" and the comparison is ordinary string comparison. By the time anything is compared there is no type mismatch left to object to.",
12
- "affects": [
13
- "python",
14
- "javascript"
15
- ],
16
- "repros": [
17
- {
18
- "language": "python",
19
- "code": "data = [2, \"10\", 1, \"3\"]\ntry:\n sorted(data)\n print(\"no error\")\nexcept TypeError:\n print(\"TypeError\")\n",
20
- "expected_output": "TypeError",
21
- "verified_with": "Python 3.14.2"
22
- },
23
- {
24
- "language": "javascript",
25
- "code": "const data = [2, \"10\", 1, \"3\"];\nconsole.log(data.slice().sort());\n",
26
- "expected_output": "[ 1, '10', 2, '3' ]",
27
- "verified_with": "Node.js v22.22.2"
28
- }
29
- ],
30
- "fix": "Normalize types before sorting a mixed collection — decide whether everything should be a number or everything a string, and convert explicitly, rather than relying on either language's comparison machinery to make that decision for you (one will refuse to run at all; the other will run and hand you an answer that looks plausible while meaning nothing).",
31
- "related": [
32
- "sorting-mixed-none-null",
33
- "js-array-sort-stringifies-numbers"
34
- ],
35
- "sources": [
36
- {
37
- "title": "Python docs: Comparisons",
38
- "url": "https://docs.python.org/3/reference/expressions.html#comparisons"
39
- },
40
- {
41
- "title": "Python docs: object.__lt__ and reflected comparison methods",
42
- "url": "https://docs.python.org/3/reference/datamodel.html#object.__lt__"
43
- },
44
- {
45
- "title": "ECMAScript spec: Array.prototype.sort",
46
- "url": "https://tc39.es/ecma262/#sec-array.prototype.sort"
47
- }
48
- ],
49
- "tags": [
50
- "sorting",
51
- "comparison",
52
- "database",
53
- "json",
54
- "parsing"
55
- ],
56
- "test_cases": [
57
- {
58
- "inputs": [
59
- [
60
- 2,
61
- "10",
62
- 1,
63
- "3"
64
- ]
65
- ],
66
- "relation": "no_crash",
67
- "note": "A sort over mixed-type input should normalize types first (for example convert everything to int, giving 1, 2, 3, 10 in numeric order) rather than raise TypeError (Python's sorted) or silently return string order (JavaScript's default sort)."
68
- }
69
- ]
70
- }
@@ -1,48 +0,0 @@
1
- {
2
- "id": "reverse-sort-vs-reverse-after-sort-ties",
3
- "title": "Sorting descending and \"sort ascending then reverse the list\" give different answers when there are ties",
4
- "category": "sorting",
5
- "assumption": "sorted(data, reverse=True) is exactly the same as list(reversed(sorted(data))) — descending sort and \"sort then flip the list\" are interchangeable.",
6
- "input": {
7
- "display": "[(\"a\",1), (\"b\",1), (\"c\",2)] sorted descending by the second element, two different ways",
8
- "description": "Showing the highest-scoring rows first, where some rows tie on score and the tiebreaker is \"whoever was already first stays first\" (e.g. insertion order, or an earlier sort by name)."
9
- },
10
- "what_goes_wrong": "sorted(data, key=..., reverse=True) returns [(\"c\",2), (\"a\",1), (\"b\",1)] — among the tied score-1 rows, \"a\" still comes before \"b\", matching their original order. list(reversed(sorted(data, key=...))) returns [(\"c\",2), (\"b\",1), (\"a\",1)] — the SAME tied rows now come out with \"b\" before \"a\", the opposite of their original order. The non-tied row (\"c\",2) ends up in the same place either way; only the ties disagree.",
11
- "why": "A stable sort keeps elements that compare equal in their original relative order. Python documents reverse=True as sorting \"as if each comparison were reversed\" while staying stable, so tied rows keep their input order, and a JavaScript comparator that negates the ascending comparison does the same. Reversing an already-sorted list flips every position, including the order inside each group of ties. The two only agree when there are no ties.",
12
- "affects": [
13
- "python",
14
- "javascript"
15
- ],
16
- "repros": [
17
- {
18
- "language": "python",
19
- "code": "data = [(\"a\", 1), (\"b\", 1), (\"c\", 2)]\nby_reverse_flag = sorted(data, key=lambda t: t[1], reverse=True)\nby_sort_then_reverse = list(reversed(sorted(data, key=lambda t: t[1])))\nprint(by_reverse_flag)\nprint(by_sort_then_reverse)\nprint(by_reverse_flag == by_sort_then_reverse)\n",
20
- "expected_output": "[('c', 2), ('a', 1), ('b', 1)]\n[('c', 2), ('b', 1), ('a', 1)]\nFalse",
21
- "verified_with": "Python 3.14.2"
22
- },
23
- {
24
- "language": "javascript",
25
- "code": "const data = [[\"a\", 1], [\"b\", 1], [\"c\", 2]];\nconst byReverseComparator = data.slice().sort((x, y) => y[1] - x[1]);\nconst bySortThenReverse = data.slice().sort((x, y) => x[1] - y[1]).reverse();\nconsole.log(JSON.stringify(byReverseComparator));\nconsole.log(JSON.stringify(bySortThenReverse));\nconsole.log(JSON.stringify(byReverseComparator) === JSON.stringify(bySortThenReverse));\n",
26
- "expected_output": "[[\"c\",2],[\"a\",1],[\"b\",1]]\n[[\"c\",2],[\"b\",1],[\"a\",1]]\nfalse",
27
- "verified_with": "Node.js v22.22.2"
28
- }
29
- ],
30
- "fix": "If tie order matters for a descending sort, use the language's actual descending sort (reverse=True in Python, or a comparator that negates the ascending comparison in JS) rather than sorting ascending and reversing the whole list afterward — they are not equivalent whenever ties exist.",
31
- "related": [],
32
- "sources": [
33
- {
34
- "title": "Python docs: Sorting HOW TO (stability, reverse)",
35
- "url": "https://docs.python.org/3/howto/sorting.html#sort-stability-and-complex-sorts"
36
- },
37
- {
38
- "title": "ECMA-262: Array.prototype.sort (stability requirement)",
39
- "url": "https://tc39.es/ecma262/#sec-array.prototype.sort"
40
- }
41
- ],
42
- "tags": [
43
- "sorting",
44
- "comparison",
45
- "pagination",
46
- "display"
47
- ]
48
- }
@@ -1,56 +0,0 @@
1
- {
2
- "id": "sorting-mixed-none-null",
3
- "title": "Python refuses to sort a list with a missing value; JavaScript sorts it two different ways depending on how you ask",
4
- "category": "sorting",
5
- "assumption": "A missing value (None/null/undefined) in a list of numbers sorts to a predictable, consistent place, or fails loudly the same way everywhere.",
6
- "input": {
7
- "display": "[3, None, 1] in Python; [3, null, 1, undefined, 2] in JavaScript",
8
- "description": "A numeric column from a database or API response where some rows legitimately have no value (an unset score, an optional field), sorted for a leaderboard or table."
9
- },
10
- "what_goes_wrong": "Python's sorted([3, None, 1]) raises TypeError. JavaScript's default sort compares elements as strings, so null sorts where the string \"null\" would (after the digits here), while undefined always goes to the very end. With a numeric comparator (a, b) => a - b, null behaves as 0 (Number(null) === 0) and moves to the front, and undefined is still last: the comparator is never called with undefined, so no comparator logic can move it.",
11
- "why": "Python's < raises TypeError when neither operand's comparison method supports the other type, and None has no ordering against int. In JavaScript, the default sort uses ToString (String(null) is \"null\"), while a subtraction comparator relies on numeric conversion, where Number(null) is 0. undefined never reaches the comparator: the spec's CompareArrayElements returns \"undefined goes after\" before calling it, so undefined values always end up after every other value (and array holes after those).",
12
- "affects": [
13
- "python",
14
- "javascript"
15
- ],
16
- "repros": [
17
- {
18
- "language": "python",
19
- "code": "data = [3, None, 1]\ntry:\n sorted(data)\n print(\"sorted without error\")\nexcept TypeError:\n print(\"TypeError\")\n",
20
- "expected_output": "TypeError",
21
- "verified_with": "Python 3.14.2"
22
- },
23
- {
24
- "language": "javascript",
25
- "code": "const data = [3, null, 1, undefined, 2];\nconsole.log(data.slice().sort());\nconsole.log(data.slice().sort((a, b) => a - b));\n",
26
- "expected_output": "[ 1, 2, 3, null, undefined ]\n[ null, 1, 2, 3, undefined ]",
27
- "verified_with": "Node.js v22.22.2"
28
- }
29
- ],
30
- "fix": "Decide explicitly where missing values belong (first, last, or filtered out) and partition or key for that before sorting, rather than letting None/null/undefined flow into the comparison — in JavaScript especially, don't assume your comparator controls where undefined ends up; it doesn't.",
31
- "related": [
32
- "python-cannot-compare-str-int",
33
- "nan-in-sort"
34
- ],
35
- "sources": [
36
- {
37
- "title": "ECMAScript spec: CompareArrayElements",
38
- "url": "https://tc39.es/ecma262/#sec-comparearrayelements"
39
- },
40
- {
41
- "title": "ECMAScript spec: Array.prototype.sort",
42
- "url": "https://tc39.es/ecma262/#sec-array.prototype.sort"
43
- },
44
- {
45
- "title": "Python docs: Comparisons",
46
- "url": "https://docs.python.org/3/reference/expressions.html#comparisons"
47
- }
48
- ],
49
- "tags": [
50
- "sorting",
51
- "comparison",
52
- "database",
53
- "pagination",
54
- "display"
55
- ]
56
- }
@@ -1,48 +0,0 @@
1
- {
2
- "id": "typed-array-sort-is-numeric-by-default",
3
- "title": "Int32Array.prototype.sort() sorts numerically by default; Array.prototype.sort() on the same numbers doesn't",
4
- "category": "sorting",
5
- "assumption": "sort() means the same thing on every array-like object in JavaScript — if plain arrays sort numbers as strings by default, typed arrays must too.",
6
- "input": {
7
- "display": "[10, 9, 1, 2].sort() vs Int32Array.from([10, 9, 1, 2]).sort()",
8
- "description": "Converting a plain array of numbers to a typed array (common when passing data to WebGL, audio buffers, or any binary-protocol code) and calling the same-named .sort() method on it with no comparator."
9
- },
10
- "what_goes_wrong": "The plain array's default sort() returns [1, 10, 2, 9] (string order, the usual trap). The typed array's default sort() on the identical numbers returns [1, 2, 9, 10] — correctly ascending numeric order — with no comparator passed at all. The exact same method name, called the same way, defaults to opposite comparison strategies depending only on which kind of array it's called on.",
11
- "why": "The ECMAScript spec gives %TypedArray%.prototype.sort its own default comparison, defined numerically: smaller values first, -0 before +0, and NaN (possible in Float32Array/Float64Array) at the end. Typed arrays can only hold numbers (or BigInts), so there is no need for the string fallback. Array.prototype.sort's default has to work for arrays holding any mix of values, and the spec defines it with ToString instead. Python's sorted() on a list of ints always compares the integer values, so there is no equivalent split.",
12
- "affects": [
13
- "javascript"
14
- ],
15
- "repros": [
16
- {
17
- "language": "python",
18
- "code": "data = [10, 9, 1, 2]\nprint(sorted(data))\n",
19
- "expected_output": "[1, 2, 9, 10]",
20
- "verified_with": "Python 3.14.2"
21
- },
22
- {
23
- "language": "javascript",
24
- "code": "const arr = [10, 9, 1, 2];\nconsole.log(arr.slice().sort());\nconst typed = Int32Array.from(arr);\ntyped.sort();\nconsole.log(Array.from(typed));\n",
25
- "expected_output": "[ 1, 10, 2, 9 ]\n[ 1, 2, 9, 10 ]",
26
- "verified_with": "Node.js v22.22.2"
27
- }
28
- ],
29
- "fix": "Don't assume .sort()'s default behavior transfers between plain arrays and typed arrays, or rely on that difference — pass an explicit numeric comparator to Array.prototype.sort() regardless, so the code reads the same way (and behaves the same way) whichever kind of array it ends up operating on.",
30
- "related": [
31
- "js-array-sort-stringifies-numbers"
32
- ],
33
- "sources": [
34
- {
35
- "title": "ECMA-262: %TypedArray%.prototype.sort",
36
- "url": "https://tc39.es/ecma262/#sec-%typedarray%.prototype.sort"
37
- },
38
- {
39
- "title": "MDN: TypedArray.prototype.sort()",
40
- "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/sort"
41
- }
42
- ],
43
- "tags": [
44
- "sorting",
45
- "comparison",
46
- "display"
47
- ]
48
- }
@@ -1,73 +0,0 @@
1
- {
2
- "id": "arabic-indic-digit-parses",
3
- "title": "Python's int(\"\\u0663\") is 3; JavaScript's Number(\"\\u0663\") is NaN",
4
- "category": "unicode",
5
- "assumption": "A \"digits only\" check (\\d or isdigit) and a numeric parser agree on what counts as a digit, and that agreement is the same in every language.",
6
- "input": {
7
- "display": "\"\\u0663\" (ARABIC-INDIC DIGIT THREE, from Arabic-script keyboards/locales)",
8
- "description": "A quantity, price, or phone-number field filled in using a non-Latin numeral system that is still, by Unicode's own definition, a decimal digit."
9
- },
10
- "what_goes_wrong": "In Python, \"\\u0663\".isdigit() is True, \"\\u0663\".isdecimal() is True, int(\"\\u0663\") == 3, and a bare regex r\"\\d\" matches it. In JavaScript, the ASCII-only /\\d/ regex does NOT match it, and both Number(\"\\u0663\") and parseInt(\"\\u0663\", 10) return NaN — the identical character is \"a valid number\" in one language's standard library and \"not a number at all\" in the other's.",
11
- "why": "ARABIC-INDIC DIGIT THREE has General_Category Nd and Numeric_Type=Decimal, exactly like ASCII \"3\": it is a real decimal digit in its own numeral system. Python's int() accepts strings made of any Unicode decimal digits (category Nd), so int(\"\\u0663\") is 3 and int(\"\\u0663\\u0661\") is 31, and the re module's \\d matches any Nd character for str patterns unless re.ASCII is set. In JavaScript, \\d is defined as exactly [0-9], with or without the u or v flag, and Number()/parseInt() only accept ASCII digits in their string grammar, so the same character is rejected.",
12
- "affects": [
13
- "python",
14
- "javascript"
15
- ],
16
- "repros": [
17
- {
18
- "language": "python",
19
- "code": "import re\ns = \"\\u0663\"\nprint(s.isdigit(), s.isdecimal())\nprint(int(s), int(s) == 3)\nprint(re.match(r\"^\\d+$\", s) is not None)\n",
20
- "expected_output": "True True\n3 True\nTrue",
21
- "verified_with": "Python 3.14.2"
22
- },
23
- {
24
- "language": "javascript",
25
- "code": "const s = \"\\u0663\";\nconsole.log(/^\\d+$/.test(s));\nconsole.log(Number(s), parseInt(s, 10));\n",
26
- "expected_output": "false\nNaN NaN",
27
- "verified_with": "Node.js v22.22.2"
28
- }
29
- ],
30
- "fix": "If you intend \"ASCII digits only\", say so explicitly (Python: re.match(r\"\\d+\", s, re.ASCII); JavaScript's \\d already is ASCII-only). If you intend to accept any script's digits, normalize non-ASCII decimal digits to ASCII before further processing (Python's unicodedata.decimal() per character, or NFKC for digits that have compatibility decompositions) rather than relying on int()/Number() to agree across languages — they don't.",
31
- "related": [
32
- "nfkc-compatibility-folding",
33
- "js-word-regex-ascii-only"
34
- ],
35
- "sources": [
36
- {
37
- "title": "UAX #44: Numeric_Type",
38
- "url": "https://www.unicode.org/reports/tr44/#Numeric_Type"
39
- },
40
- {
41
- "title": "ECMAScript spec: ToNumber applied to the String type",
42
- "url": "https://tc39.es/ecma262/#sec-tonumber-applied-to-the-string-type"
43
- },
44
- {
45
- "title": "ECMAScript spec: CharacterClassEscape (\\d)",
46
- "url": "https://tc39.es/ecma262/#sec-characterclassescape"
47
- },
48
- {
49
- "title": "Python docs: int()",
50
- "url": "https://docs.python.org/3/library/functions.html#int"
51
- },
52
- {
53
- "title": "Python docs: re (\\d)",
54
- "url": "https://docs.python.org/3/library/re.html#regular-expression-syntax"
55
- }
56
- ],
57
- "tags": [
58
- "validation",
59
- "forms",
60
- "parsing",
61
- "i18n",
62
- "money"
63
- ],
64
- "test_cases": [
65
- {
66
- "inputs": [
67
- "٣"
68
- ],
69
- "relation": "no_crash",
70
- "note": "A \"digits only\" validator should either accept this consistently in both Python and JS or reject it consistently in both — not one and not the other, by accident of which language's \\d it used."
71
- }
72
- ]
73
- }
@@ -1,62 +0,0 @@
1
- {
2
- "id": "bidi-override-filename-spoof",
3
- "title": "A filename that ends in \".exe\" can display as if it ends in \".png\"",
4
- "category": "unicode",
5
- "assumption": "What a filename's extension check says and what a user sees when they look at the filename are the same thing.",
6
- "input": {
7
- "display": "\"invoice\\u202egnp.exe\" (RIGHT-TO-LEFT OVERRIDE, U+202E, inserted before the reversed tail \"gnp.exe\")",
8
- "description": "An email attachment or download named by an attacker to look safe: a real file ending in a blocked extension, disguised by an embedded bidi control character."
9
- },
10
- "what_goes_wrong": "name.endswith(\".exe\") is True — the raw string genuinely ends in \".exe\" and a naive extension blocklist correctly flags it — but when this exact string is rendered by an OS file browser, email client, or terminal that honors bidi control characters, everything after U+202E is displayed right-to-left, so \"gnp.exe\" is painted in reverse as \"exe.png\": a human looking at the file sees what appears to be a harmless PNG, not the executable the code just correctly identified.",
11
- "why": "U+202E (RIGHT-TO-LEFT OVERRIDE) is a Unicode bidirectional formatting control: it tells a bidi-aware renderer to lay out everything that follows as if it were right-to-left text, purely for display, with no effect on the underlying code point sequence or on any string method. This is the classic \"Unicode RLO\" spoof used to disguise malware extensions in the wild: the code's view of the string (and any endswith/regex check on it) is accurate and unaffected, which is precisely why the attack works on humans rather than on code — the mismatch is between what is stored and what a bidi-rendering surface draws, not a parsing bug.",
12
- "affects": [
13
- "python",
14
- "javascript"
15
- ],
16
- "repros": [
17
- {
18
- "language": "python",
19
- "code": "name = \"invoice\\u202egnp.exe\"\nprint(len(name), name.endswith(\".exe\"))\nprint(name.encode(\"unicode_escape\"))\n",
20
- "expected_output": "15 True\nb'invoice\\\\u202egnp.exe'",
21
- "verified_with": "Python 3.14.2"
22
- },
23
- {
24
- "language": "javascript",
25
- "code": "const name = \"invoice\\u202egnp.exe\";\nconsole.log(name.length, name.endsWith(\".exe\"));\nconsole.log(JSON.stringify(name));\n",
26
- "expected_output": "15 true\n\"invoice‮gnp.exe\"",
27
- "verified_with": "Node.js v22.22.2"
28
- }
29
- ],
30
- "fix": "Strip or reject Unicode bidirectional format characters (U+202A-U+202E, U+2066-U+2069) from filenames and other security-relevant display strings at the point of upload/creation, and never trust a rendered filename's apparent extension — always resolve the extension from the raw code point sequence.",
31
- "related": [
32
- "zalgo-combining-mark-stacking"
33
- ],
34
- "sources": [
35
- {
36
- "title": "Unicode Standard Annex #9: Unicode Bidirectional Algorithm",
37
- "url": "https://www.unicode.org/reports/tr9/"
38
- },
39
- {
40
- "title": "Big List of Naughty Strings (MIT)",
41
- "url": "https://github.com/minimaxir/big-list-of-naughty-strings"
42
- }
43
- ],
44
- "tags": [
45
- "filename",
46
- "upload",
47
- "security",
48
- "display",
49
- "validation"
50
- ],
51
- "test_cases": [
52
- {
53
- "inputs": [
54
- "invoice‮gnp.exe",
55
- ".exe"
56
- ],
57
- "relation": "returns",
58
- "expected": true,
59
- "note": "their_function(name, ext) checking name.endsWith(ext) correctly returns true on the raw string — the vulnerability is that a human reading the rendered name will disagree with this correct answer."
60
- }
61
- ]
62
- }