@pipeworx/mcp-edge-case-museum 0.1.0 → 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,58 +0,0 @@
1
- {
2
- "id": "parseint-stops-at-first-invalid-char-vs-number",
3
- "title": "parseInt(\"1e3\") is 1, but Number(\"1e3\") is 1000",
4
- "category": "integers",
5
- "assumption": "parseInt and Number agree on what a numeric string means; parseInt is just 'the integer version'.",
6
- "input": {
7
- "display": "parseInt(\"1e3\") === 1 while Number(\"1e3\") === 1000",
8
- "description": "Converting a user-entered or CSV-sourced numeric string that happens to be in scientific notation (common in spreadsheet exports of large or small numbers)."
9
- },
10
- "what_goes_wrong": "Switching a 'parse this numeric string' call from `Number()` to `parseInt()` (e.g. 'because I want an integer') silently truncates any value written in scientific notation to just its leading digit, turning 1000 into 1 with no error.",
11
- "why": "`parseInt` parses a string by reading characters strictly left to right and stopping at the first character that cannot extend a valid integer literal in the given radix -- it has no concept of an exponent, so it stops at the \"e\". `Number()` instead parses the whole string against ECMA-262's StringNumericLiteral grammar (similar to, but not the same as, source-code numeric literals: it allows surrounding whitespace and \"Infinity\", and rejects numeric separators and the `n` BigInt suffix), which does understand exponents, so it parses \"1e3\" as 1 * 10^3 = 1000. The two are not interchangeable string-to-number converters.",
12
- "affects": [
13
- "javascript"
14
- ],
15
- "repros": [
16
- {
17
- "language": "javascript",
18
- "code": "console.log(parseInt(\"1e3\"), Number(\"1e3\"));\nconsole.log(parseInt(\"3.14\"), Number(\"3.14\"));\nconsole.log(parseInt(\"1,000\"), Number(\"1,000\"));\n",
19
- "expected_output": "1 1000\n3 3.14\n1 NaN",
20
- "verified_with": "Node.js v22.22.2"
21
- },
22
- {
23
- "language": "python",
24
- "code": "# Python's int() rejects scientific notation strings outright rather than truncating.\ntry:\n int(\"1e3\")\nexcept ValueError as e:\n print(\"int('1e3') ValueError:\", e)\nprint(float(\"1e3\")) # the float constructor does understand exponents\n",
25
- "expected_output": "int('1e3') ValueError: invalid literal for int() with base 10: '1e3'\n1000.0",
26
- "verified_with": "Python 3.14.2"
27
- }
28
- ],
29
- "fix": "Use `Number()` (and then validate/round) to parse a numeric string that might contain an exponent, decimals, or thousands separators; reserve `parseInt` for strings you know are plain digit sequences in a known radix.",
30
- "related": [
31
- "parseint-number-argument-scientific-notation",
32
- "parseint-null-radix36-digit-soup"
33
- ],
34
- "sources": [
35
- {
36
- "title": "MDN: parseInt()",
37
- "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt"
38
- },
39
- {
40
- "title": "ECMA-262: ToNumber applied to the String type",
41
- "url": "https://tc39.es/ecma262/#sec-tonumber-applied-to-the-string-type"
42
- }
43
- ],
44
- "tags": [
45
- "parsing",
46
- "validation",
47
- "csv"
48
- ],
49
- "test_cases": [
50
- {
51
- "inputs": [
52
- "1e3"
53
- ],
54
- "relation": "no_crash",
55
- "note": "Decide explicitly whether a numeric-string parser should understand scientific notation; don't silently truncate at the exponent."
56
- }
57
- ]
58
- }
@@ -1,56 +0,0 @@
1
- {
2
- "id": "python-int-accepts-fullwidth-unicode-digits",
3
- "title": "int(\"42\") works -- those are fullwidth Unicode digits, not the ASCII characters they look like",
4
- "category": "integers",
5
- "assumption": "int() only accepts the ASCII digits 0-9.",
6
- "input": {
7
- "display": "int(\"\\uff14\\uff12\") == 42 (U+FF14 FULLWIDTH DIGIT FOUR, U+FF12 FULLWIDTH DIGIT TWO)",
8
- "description": "User input copy-pasted from a source using fullwidth characters (common in East Asian text contexts, or a sneaky/naughty-string test input), or any other Unicode script's decimal digits (Arabic-Indic, Devanagari, etc.) submitted to a form field."
9
- },
10
- "what_goes_wrong": "A validator that checks `s.isdigit()` or `re.fullmatch(r'\\d+', s)` and then calls `int(s)` happily accepts fullwidth, Arabic-Indic, Devanagari and other Unicode digits (both checks are Unicode-aware for str), while a check anchored to `[0-9]` rejects the same input that `int()` would accept -- so two layers of the same form can disagree about what a number is.",
11
- "why": "Python's `int()` (and `float()`) accepts any character with the Unicode general category Nd ('decimal digit'), which covers fullwidth forms (U+FF10-FF19), Arabic-Indic, Devanagari and dozens of other scripts' 0-9, and maps each to its decimal value; it also strips any Unicode whitespace around the number. This is documented, deliberate internationalisation in the string-to-number conversion, not in the language's source parser: `x = 42` in source code is a SyntaxError. Python's regex `\\d` is likewise Unicode-aware for str patterns unless you pass `re.ASCII`. And the sets do not line up: `'²'.isdigit()` is True for superscript two, but it is not Nd, so `int('²')` raises.",
12
- "affects": [
13
- "python"
14
- ],
15
- "repros": [
16
- {
17
- "language": "python",
18
- "code": "import re\nfullwidth = \"\\uff14\\uff12\" # looks like \"42\"\nprint(repr(fullwidth), \"->\", int(fullwidth))\nprint(\"matches [0-9]+:\", bool(re.fullmatch(r\"[0-9]+\", fullwidth)))\nprint(\"matches \\\\d+:\", bool(re.fullmatch(r\"\\d+\", fullwidth)), \"| with re.ASCII:\", bool(re.fullmatch(r\"\\d+\", fullwidth, re.ASCII)))\nsup = \"\\u00b2\" # SUPERSCRIPT TWO\ntry:\n int(sup)\nexcept ValueError as e:\n print(repr(sup), \"isdigit():\", sup.isdigit(), \"| int() ->\", e)",
19
- "expected_output": "'42' -> 42\nmatches [0-9]+: False\nmatches \\d+: True | with re.ASCII: False\n'²' isdigit(): True | int() -> invalid literal for int() with base 10: '²'",
20
- "verified_with": "Python 3.14.2"
21
- },
22
- {
23
- "language": "javascript",
24
- "code": "// Number() and parseInt() do NOT extend digit recognition past ASCII 0-9,\n// so the same fullwidth string fails to parse at all in JavaScript.\nconst fullwidth = \"\\uff14\\uff12\";\nconsole.log(Number(fullwidth), parseInt(fullwidth, 10));\n",
25
- "expected_output": "NaN NaN",
26
- "verified_with": "Node.js v22.22.2"
27
- }
28
- ],
29
- "fix": "If you need ASCII digits only, say so explicitly: `s.isascii() and s.isdigit()`, `re.fullmatch(r'[0-9]+', s)` or `re.fullmatch(r'\\d+', s, re.ASCII)`, and run `int()` on the same string you validated. If you want to accept other scripts' digits, normalise them deliberately (e.g. `str(int(s))`) before storing or passing the value to systems that only understand ASCII.",
30
- "related": [
31
- "python-int-accepts-underscores-and-whitespace"
32
- ],
33
- "sources": [
34
- {
35
- "title": "Unicode Standard: General Category Values (Nd = Decimal Number)",
36
- "url": "https://www.unicode.org/reports/tr44/#General_Category_Values"
37
- },
38
- {
39
- "title": "Python docs: int() constructor",
40
- "url": "https://docs.python.org/3/library/functions.html#int"
41
- },
42
- {
43
- "title": "Python docs: numeric types (int/float accept digits with the Unicode Nd property)",
44
- "url": "https://docs.python.org/3/library/stdtypes.html#numeric-types-int-float-complex"
45
- }
46
- ],
47
- "tags": [
48
- "parsing",
49
- "validation",
50
- "i18n",
51
- "forms",
52
- "normalization",
53
- "security"
54
- ],
55
- "test_cases": []
56
- }
@@ -1,48 +0,0 @@
1
- {
2
- "id": "python-int-accepts-underscores-and-whitespace",
3
- "title": "int(\" 42 \") and int(\"1_000\") both just work",
4
- "category": "integers",
5
- "assumption": "int() only accepts exactly what you could type as an integer literal in source code, with nothing extra around it.",
6
- "input": {
7
- "display": "int(\" 42 \") == 42; int(\"1_000\") == 1000",
8
- "description": "Converting a number pulled from a config file, environment variable, or user-pasted value that has incidental surrounding whitespace, or a number formatted with underscore thousands-separators (a format Python itself started allowing in source literals in 3.6)."
9
- },
10
- "what_goes_wrong": "A validation routine that assumes `int()` will reject anything that isn't a bare run of digits lets through both leading/trailing whitespace and underscore-separated digit groups, silently accepting input shapes the developer never intended to allow.",
11
- "why": "`int()`'s string-to-integer conversion strips leading and trailing whitespace before parsing, by design, and that means any Unicode whitespace (tabs, newlines, U+3000 IDEOGRAPHIC SPACE), not just spaces. Separately, since Python 3.6 (PEP 515), the integer literal grammar allows a single underscore between digits as a visual separator (`1_000`), and `int()`'s string parsing follows that same grammar, so it accepts underscore-separated digit strings too -- as long as the underscores are singly placed between digits, not leading, trailing, or doubled.",
12
- "affects": [
13
- "python"
14
- ],
15
- "repros": [
16
- {
17
- "language": "python",
18
- "code": "print(int(\" 42 \"))\nprint(int(\"1_000\"))\ntry:\n int(\"_1000\")\nexcept ValueError as e:\n print(\"leading underscore rejected:\", e)\n",
19
- "expected_output": "42\n1000\nleading underscore rejected: invalid literal for int() with base 10: '_1000'",
20
- "verified_with": "Python 3.14.2"
21
- },
22
- {
23
- "language": "javascript",
24
- "code": "// Number() also trims surrounding whitespace. JavaScript's numeric separators\n// (1_000, ES2021, the same single-underscore-between-digits rule as Python)\n// exist only in source-code literals, never in string parsing.\nconsole.log(Number(\" 42 \")); // 42: whitespace is trimmed here too\nconsole.log(Number(\"1_000\")); // NaN: Number() does not accept separators in strings\nconsole.log(parseInt(\"1_000\")); // 1: parseInt stops at the underscore\nconsole.log(1_000); // 1000: but the literal is fine",
25
- "expected_output": "42\nNaN\n1\n1000",
26
- "verified_with": "Node.js v22.22.2"
27
- }
28
- ],
29
- "fix": "Don't assume `int()` validates format as strictly as a literal; if you need to reject whitespace or separators, check the string's shape explicitly (e.g. a regex) before calling `int()`.",
30
- "related": [
31
- "python-int-accepts-fullwidth-unicode-digits"
32
- ],
33
- "sources": [
34
- {
35
- "title": "Python docs: int() constructor",
36
- "url": "https://docs.python.org/3/library/functions.html#int"
37
- },
38
- {
39
- "title": "PEP 515 -- Underscores in Numeric Literals",
40
- "url": "https://peps.python.org/pep-0515/"
41
- }
42
- ],
43
- "tags": [
44
- "parsing",
45
- "validation"
46
- ],
47
- "test_cases": []
48
- }
@@ -1,49 +0,0 @@
1
- {
2
- "id": "python-unbounded-int-vs-fixed-width-wrap",
3
- "title": "Python's int never overflows -- until you explicitly ask for a fixed-width one",
4
- "category": "integers",
5
- "assumption": "An integer type has some maximum size, and adding to it eventually overflows or errors.",
6
- "input": {
7
- "display": "struct.pack(\"i\", 2**31) raises; ctypes.c_int32(2**31).value wraps to -2147483648",
8
- "description": "Serializing a Python int into a fixed-width binary format (a network protocol, a C-compatible struct, a file format) where the field is a 32-bit signed integer, after the value has grown past what that field can hold."
9
- },
10
- "what_goes_wrong": "Code that works fine for years because Python's `int` just keeps growing starts failing (or, worse, silently wraps to a negative number) the moment that same value has to be packed into a fixed-width field for serialization, networking, or C interop -- and which of those two happens depends entirely on which API you used to do the packing.",
11
- "why": "Python's built-in `int` is arbitrary-precision: it has no fixed bit width and never overflows on its own. Fixed-width behavior only appears when you explicitly convert to one -- `struct.pack` with a format like `\"i\"` (the platform's C `int`, signed 32-bit on all mainstream platforms) validates the range and raises `struct.error` if the value doesn't fit, while `ctypes.c_int32` instead performs a real two's-complement modulo-2^32 wraparound, silently producing a negative value with no error at all. The two standard-library tools for 'give this a fixed width' disagree on whether out-of-range is an error or a wraparound.",
12
- "affects": [
13
- "python"
14
- ],
15
- "repros": [
16
- {
17
- "language": "python",
18
- "code": "import struct\nimport ctypes\n\ntry:\n struct.pack(\"i\", 2**31)\nexcept struct.error as e:\n print(\"struct.pack raises:\", e)\n\nprint(\"ctypes.c_int32 wraps silently:\", ctypes.c_int32(2**31).value)\nprint(\"plain python int, unaffected:\", 2**31)\n",
19
- "expected_output": "struct.pack raises: 'i' format requires -2147483648 <= number <= 2147483647\nctypes.c_int32 wraps silently: -2147483648\nplain python int, unaffected: 2147483648",
20
- "verified_with": "Python 3.14.2"
21
- },
22
- {
23
- "language": "javascript",
24
- "code": "// JS has no arbitrary-precision integer type by default (Number is a double),\n// but a typed array shows the same silent-wrap behavior as ctypes.c_int32.\nconst a = new Int32Array(1);\na[0] = 2 ** 31;\nconsole.log(\"Int32Array wraps silently:\", a[0]);\n",
25
- "expected_output": "Int32Array wraps silently: -2147483648",
26
- "verified_with": "Node.js v22.22.2"
27
- }
28
- ],
29
- "fix": "When serializing a Python int into a fixed-width field, validate the range yourself first (or use `struct.pack` and handle the `struct.error`) rather than reaching for `ctypes`, whose wraparound is silent and easy to miss in a code review.",
30
- "related": [
31
- "bitwise-32-bit-truncation"
32
- ],
33
- "sources": [
34
- {
35
- "title": "Python docs: struct -- Format Characters",
36
- "url": "https://docs.python.org/3/library/struct.html#format-characters"
37
- },
38
- {
39
- "title": "Python docs: ctypes -- Fundamental data types",
40
- "url": "https://docs.python.org/3/library/ctypes.html#fundamental-data-types"
41
- }
42
- ],
43
- "tags": [
44
- "validation",
45
- "parsing",
46
- "database"
47
- ],
48
- "test_cases": []
49
- }
@@ -1,54 +0,0 @@
1
- {
2
- "id": "address-with-no-street-number",
3
- "title": "A rural route, a PO box, and a named lot all have an address with no street number at all",
4
- "category": "names-addresses",
5
- "assumption": "Every valid street address begins with a number.",
6
- "input": {
7
- "display": "\"Rural Route 2\", \"PO Box 445\", \"Lot 7 Orchard Lane\"",
8
- "description": "An address-line validator that requires the first token to be numeric, used on a shipping or billing form."
9
- },
10
- "what_goes_wrong": "Entirely legitimate, deliverable addresses are rejected because they don't start with digits — rural routes, PO boxes, and named-lot rural properties are all real addressing conventions with no house number.",
11
- "why": "A leading street number is just one of several addressing schemes in common use; postal systems also deliver to rural route numbers, post office boxes, and named lots or stations that never had a house number assigned at all. A regex that hard-requires a leading digit encodes 'suburban street address' as if it were the only valid addressing format, when it is only one of several.",
12
- "affects": [
13
- "any address validator requiring a leading street number"
14
- ],
15
- "repros": [
16
- {
17
- "language": "python",
18
- "code": "import re\npattern = re.compile(r\"^\\d+\\s\")\nfor addr in [\"Rural Route 2\", \"PO Box 445\", \"Lot 7 Orchard Lane\"]:\n print(addr, bool(pattern.match(addr)))\n",
19
- "expected_output": "Rural Route 2 False\nPO Box 445 False\nLot 7 Orchard Lane False",
20
- "verified_with": "Python 3.14.2"
21
- },
22
- {
23
- "language": "javascript",
24
- "code": "const pattern = /^\\d+\\s/;\nfor (const addr of [\"Rural Route 2\", \"PO Box 445\", \"Lot 7 Orchard Lane\"]) {\n console.log(addr, pattern.test(addr));\n}\n",
25
- "expected_output": "Rural Route 2 false\nPO Box 445 false\nLot 7 Orchard Lane false",
26
- "verified_with": "Node.js v22.22.2"
27
- }
28
- ],
29
- "fix": "Don't require an address to start with a number; if you need structured address fields, make the house-number field optional and let 'address line 1' accept free text, which is how postal services themselves treat it.",
30
- "related": [
31
- "house-number-suffix-collapsed-by-digit-extraction"
32
- ],
33
- "sources": [
34
- {
35
- "title": "Michael Tandy: Falsehoods Programmers Believe About Addresses",
36
- "url": "https://www.mjt.me.uk/posts/falsehoods-programmers-believe-about-addresses/"
37
- }
38
- ],
39
- "tags": [
40
- "addresses",
41
- "validation",
42
- "forms",
43
- "parsing"
44
- ],
45
- "test_cases": [
46
- {
47
- "inputs": [
48
- "PO Box 445"
49
- ],
50
- "relation": "no_crash",
51
- "note": "A PO Box is a deliverable address with no street number; your form should accept it."
52
- }
53
- ]
54
- }
@@ -1,55 +0,0 @@
1
- {
2
- "id": "ascii-regex-rejects-accented-and-hyphenated-names",
3
- "title": "[A-Za-z ]+ rejects \"Anne-Marie\", \"O'Brien\", and anyone with an accent in their name",
4
- "category": "names-addresses",
5
- "assumption": "A simple letters-and-spaces regex is a reasonable baseline check for 'is this a real name'.",
6
- "input": {
7
- "display": "\"Zo\\u00eb Navarro-Pe\\u00f1a\" (invented), \"Anne-Marie\", \"O'Brien\"",
8
- "description": "A 'name must contain only letters' validator on a signup or checkout form, written against the ASCII alphabet."
9
- },
10
- "what_goes_wrong": "Entirely ordinary names are rejected outright: an accented letter, a hyphen joining two given names, or an apostrophe in a surname all fail a check that was only ever tested against plain ASCII names.",
11
- "why": "The character class [A-Za-z] matches only the 26 unaccented Latin letters in each case; it has no entries for accented Latin letters, hyphens, apostrophes, or any non-Latin script at all. Any name that uses punctuation that is a normal part of that name, or any letter outside the ASCII range, fails the match even though the input is a perfectly ordinary name.",
12
- "affects": [
13
- "any regex validator built on [A-Za-z] or similar ascii-only classes"
14
- ],
15
- "repros": [
16
- {
17
- "language": "python",
18
- "code": "import re\npattern = re.compile(r\"^[A-Za-z ]+$\")\nfor name in [\"Zo\\u00eb Navarro-Pe\\u00f1a\", \"Anne-Marie\", \"O'Brien\", \"Jose Luis\"]:\n print(name, bool(pattern.match(name)))\n",
19
- "expected_output": "Zoë Navarro-Peña False\nAnne-Marie False\nO'Brien False\nJose Luis True",
20
- "verified_with": "Python 3.14.2"
21
- },
22
- {
23
- "language": "javascript",
24
- "code": "const pattern = /^[A-Za-z ]+$/;\nfor (const name of [\"Zo\\u00eb Navarro-Pe\\u00f1a\", \"Anne-Marie\", \"O'Brien\", \"Jose Luis\"]) {\n console.log(name, pattern.test(name));\n}\n",
25
- "expected_output": "Zoë Navarro-Peña false\nAnne-Marie false\nO'Brien false\nJose Luis true",
26
- "verified_with": "Node.js v22.22.2"
27
- }
28
- ],
29
- "fix": "Don't validate names against a character allowlist at all if you can avoid it; if you must reject something, reject on length or control characters, not on script or punctuation — a name field should accept Unicode letters, marks, hyphens, and apostrophes at minimum.",
30
- "related": [
31
- "title-case-mangles-mixed-case-and-apostrophes"
32
- ],
33
- "sources": [
34
- {
35
- "title": "Patrick McKenzie: Falsehoods Programmers Believe About Names",
36
- "url": "https://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/"
37
- }
38
- ],
39
- "tags": [
40
- "names",
41
- "validation",
42
- "forms",
43
- "signup",
44
- "i18n"
45
- ],
46
- "test_cases": [
47
- {
48
- "inputs": [
49
- "Zoë Navarro-Peña"
50
- ],
51
- "relation": "no_crash",
52
- "note": "Your name validator should accept this; if it's built on an ASCII letter class, it won't."
53
- }
54
- ]
55
- }
@@ -1,55 +0,0 @@
1
- {
2
- "id": "house-number-suffix-collapsed-by-digit-extraction",
3
- "title": "\"221B Baker Street\" and \"221 Baker Street\" extract to the same house number",
4
- "category": "names-addresses",
5
- "assumption": "Pulling the leading digits off an address line gives you a unique house-number key.",
6
- "input": {
7
- "display": "\"221B Baker Street\" vs \"221 Baker Street\"; \"12\\u00bd Main Street\" vs \"12 Main Street\"",
8
- "description": "A geocoding or dedup step that extracts 'the house number' from an address line with a leading-digits regex, to compare or key addresses."
9
- },
10
- "what_goes_wrong": "Two genuinely different addresses — a subdivided unit \"221B\" and the plain \"221\" — collapse to the identical extracted number \"221\", silently merging two different physical locations in any system that keys on the extracted value.",
11
- "why": "A leading-digit regex like ^(\\d+) stops at the first non-digit character, so it captures only \"221\" and discards the trailing letter suffix \"B\" — which is not decoration: it typically marks a separate property created by subdividing or infilling, and is what distinguishes it from plain 221. The same problem affects fractional house numbers like \"12½\", where the regex captures \"12\" and silently drops the ½ that distinguishes it from plain \"12\".",
12
- "affects": [
13
- "any 'extract leading digits as the house number' parser"
14
- ],
15
- "repros": [
16
- {
17
- "language": "python",
18
- "code": "import re\ndef house_number(addr):\n m = re.match(r\"^(\\d+)\", addr)\n return m.group(1) if m else None\n\nfor a in [\"221B Baker Street\", \"221 Baker Street\", \"12\\u00bd Main Street\", \"12 Main Street\"]:\n print(a, \"->\", house_number(a))\n",
19
- "expected_output": "221B Baker Street -> 221\n221 Baker Street -> 221\n12½ Main Street -> 12\n12 Main Street -> 12",
20
- "verified_with": "Python 3.14.2"
21
- },
22
- {
23
- "language": "javascript",
24
- "code": "function houseNumber(addr) {\n const m = addr.match(/^(\\d+)/);\n return m ? m[1] : null;\n}\nfor (const a of [\"221B Baker Street\", \"221 Baker Street\", \"12\\u00bd Main Street\", \"12 Main Street\"]) {\n console.log(a, \"->\", houseNumber(a));\n}\n",
25
- "expected_output": "221B Baker Street -> 221\n221 Baker Street -> 221\n12½ Main Street -> 12\n12 Main Street -> 12",
26
- "verified_with": "Node.js v22.22.2"
27
- }
28
- ],
29
- "fix": "Keep the full house-number token, including any letter suffix or fraction, as the key, instead of truncating to the leading digits; treat \"221\" and \"221B\" as different addresses unless you've confirmed otherwise.",
30
- "related": [
31
- "address-with-no-street-number"
32
- ],
33
- "sources": [
34
- {
35
- "title": "Michael Tandy: Falsehoods Programmers Believe About Addresses",
36
- "url": "https://www.mjt.me.uk/posts/falsehoods-programmers-believe-about-addresses/"
37
- }
38
- ],
39
- "tags": [
40
- "addresses",
41
- "parsing",
42
- "validation",
43
- "database"
44
- ],
45
- "test_cases": [
46
- {
47
- "inputs": [
48
- "221B Baker Street",
49
- "221 Baker Street"
50
- ],
51
- "relation": "not_equal",
52
- "note": "These are two different addresses; your house-number extraction should not treat them as the same key."
53
- }
54
- ]
55
- }
@@ -1,56 +0,0 @@
1
- {
2
- "id": "mononym-fails-first-last-required",
3
- "title": "A one-word name fails 'first and last name required' — because it IS one word, correctly",
4
- "category": "names-addresses",
5
- "assumption": "Every person has both a first name and a last name to put in two separate required fields.",
6
- "input": {
7
- "display": "\"Wulandari\" (an invented single-word legal name, no space)",
8
- "description": "A signup, shipping, or ID-verification form with separate required 'First Name' and 'Last Name' fields, applied to someone who legally has one name only."
9
- },
10
- "what_goes_wrong": "The validator rejects the whole name as incomplete, forcing the person to invent a fake last name, or re-type their one name into both boxes, just to get past the form.",
11
- "why": "Single-word names are a real, legally recognized naming convention: many Indonesians are registered with one name only, and the pattern occurs in other cultures too. A validator that REQUIRES a 'last' component after splitting on whitespace will always reject a single-word name, because there is nothing to split; the assumption that a name has two parts is false for this input, not a formatting mistake to be corrected.",
12
- "affects": [
13
- "any 'first + last required' form validator"
14
- ],
15
- "repros": [
16
- {
17
- "language": "python",
18
- "code": "def validate_two_part_name(name):\n parts = name.split(\" \")\n if len(parts) < 2:\n raise ValueError(\"first and last name are both required\")\n return True\n\ntry:\n validate_two_part_name(\"Wulandari\")\nexcept ValueError as e:\n print(\"rejected:\", e)\n",
19
- "expected_output": "rejected: first and last name are both required",
20
- "verified_with": "Python 3.14.2"
21
- },
22
- {
23
- "language": "javascript",
24
- "code": "function validateTwoPartName(name) {\n const parts = name.split(\" \");\n if (parts.length < 2) throw new Error(\"first and last name are both required\");\n return true;\n}\ntry {\n validateTwoPartName(\"Wulandari\");\n} catch (e) {\n console.log(\"rejected:\", e.message);\n}\n",
25
- "expected_output": "rejected: first and last name are both required",
26
- "verified_with": "Node.js v22.22.2"
27
- }
28
- ],
29
- "fix": "Collect a single 'full name' field (or make the family-name field explicitly optional) instead of hard-requiring two parts; let the person tell you how their name is structured rather than inferring it from word count.",
30
- "related": [
31
- "split-surname-on-last-space-loses-particle",
32
- "single-letter-surname-fails-min-length"
33
- ],
34
- "sources": [
35
- {
36
- "title": "Patrick McKenzie: Falsehoods Programmers Believe About Names",
37
- "url": "https://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/"
38
- }
39
- ],
40
- "tags": [
41
- "names",
42
- "forms",
43
- "validation",
44
- "signup",
45
- "i18n"
46
- ],
47
- "test_cases": [
48
- {
49
- "inputs": [
50
- "Wulandari"
51
- ],
52
- "relation": "no_crash",
53
- "note": "Your signup form should accept a single-word legal name without forcing a fake second word."
54
- }
55
- ]
56
- }
@@ -1,64 +0,0 @@
1
- {
2
- "id": "naive-lowercase-breaks-greek-final-sigma",
3
- "title": "Build your own per-letter lowercase table and Greek \"ΟΔΥΣΣΕΥΣ\" lowercases to the wrong letter at the end",
4
- "category": "names-addresses",
5
- "assumption": "Lowercasing a string is just mapping each character to its lowercase equivalent, independent of its neighbors.",
6
- "input": {
7
- "display": "\"ΟΔΥΣΣΕΥΣ\" (Greek, 'ODYSSEUS')",
8
- "description": "A hand-rolled case-folding table (common in custom search normalizers, or a simple transliteration helper) that maps each character independently, rather than calling the language's real lowercasing routine."
9
- },
10
- "what_goes_wrong": "A search-index normalizer built this way produces a lowercase form that disagrees with the one str.lower() / toLowerCase() produces, so a lookup built against one and compared against the other silently fails to match at the end of every Greek word ending in capital sigma.",
11
- "why": "Greek has two lowercase forms of sigma: σ in the middle of a word and ς (final sigma) at the end of a word — which form is correct depends on the character's POSITION in the word, not just its identity. Unicode's SpecialCasing rules encode that context-sensitive mapping, and both Python's str.lower() and JavaScript's toLowerCase() correctly apply it. A naive table that maps Σ to σ unconditionally, character by character with no notion of 'end of word', can never produce the final form, so it disagrees with the correct, built-in result precisely where a word ends in sigma.",
12
- "affects": [
13
- "any hand-rolled per-character case table (no word-context)",
14
- "python str.lower (correctly context-sensitive)",
15
- "javascript toLowerCase (correctly context-sensitive)"
16
- ],
17
- "repros": [
18
- {
19
- "language": "python",
20
- "code": "naive_map = {\"\\u03a3\": \"\\u03c3\"}\ndef naive_lower(s):\n return \"\".join(naive_map.get(c, c.lower()) for c in s)\n\nword = \"\\u039f\\u0394\\u03a5\\u03a3\\u03a3\\u0395\\u03a5\\u03a3\"\nprint(naive_lower(word) == word.lower())\nprint(naive_lower(word))\nprint(word.lower())\n",
21
- "expected_output": "False\nοδυσσευσ\nοδυσσευς",
22
- "verified_with": "Python 3.14.2"
23
- },
24
- {
25
- "language": "javascript",
26
- "code": "const naiveMap = { \"\\u03a3\": \"\\u03c3\" };\nfunction naiveLower(s) {\n return [...s].map((c) => naiveMap[c] ?? c.toLowerCase()).join(\"\");\n}\nconst word = \"\\u039f\\u0394\\u03a5\\u03a3\\u03a3\\u0395\\u03a5\\u03a3\";\nconsole.log(naiveLower(word) === word.toLowerCase());\nconsole.log(naiveLower(word));\nconsole.log(word.toLowerCase());\n",
27
- "expected_output": "false\nοδυσσευσ\nοδυσσευς",
28
- "verified_with": "Node.js v22.22.2"
29
- }
30
- ],
31
- "fix": "Never build a custom per-character case-folding table for a script you haven't checked for context-sensitive rules; use the language's built-in Unicode-aware lower()/toLowerCase() (or ICU) and only post-process it if you have a specific, tested reason to.",
32
- "related": [],
33
- "sources": [
34
- {
35
- "title": "Unicode SpecialCasing.txt (Final_Sigma condition)",
36
- "url": "https://www.unicode.org/Public/UCD/latest/ucd/SpecialCasing.txt"
37
- },
38
- {
39
- "title": "Python docs: str.lower()",
40
- "url": "https://docs.python.org/3/library/stdtypes.html#str.lower"
41
- },
42
- {
43
- "title": "ECMAScript spec: String.prototype.toLowerCase",
44
- "url": "https://tc39.es/ecma262/#sec-string.prototype.tolowercase"
45
- }
46
- ],
47
- "tags": [
48
- "names",
49
- "i18n",
50
- "normalization",
51
- "search",
52
- "database"
53
- ],
54
- "test_cases": [
55
- {
56
- "inputs": [
57
- "ΟΔΥΣΣΕΥΣ"
58
- ],
59
- "relation": "returns",
60
- "expected": "οδυσσευς",
61
- "note": "If you built your own per-character lowercase table, verify it against this: the correct result ends in final sigma (ς), not plain sigma (σ)."
62
- }
63
- ]
64
- }
@@ -1,58 +0,0 @@
1
- {
2
- "id": "null-surname-coerced-to-null-value",
3
- "title": "A surname that is literally the word \"Null\" gets cleaned away to an actual null",
4
- "category": "names-addresses",
5
- "assumption": "A string-cleaning step that treats the text 'null' as a sign of missing data is harmless.",
6
- "input": {
7
- "display": "\"Null\" (a real surname)",
8
- "description": "A 'sanitize this field' helper applied to a last-name input, written to catch forms that leak the literal word null/NULL as placeholder text for an empty field."
9
- },
10
- "what_goes_wrong": "A person whose actual surname is Null has their name replaced with an actual missing value, because the cleaning function cannot tell the difference between the placeholder string and the real data.",
11
- "why": "Some input pipelines add a defensive check (if the trimmed, lowercased field equals the text \"null\", treat it as empty) to guard against a template or export rendering the literal word when a field is unset. That check is a string comparison with no awareness that \"Null\" is also a real, if uncommon, surname, so it deletes a correct answer precisely because it looks like the sentinel. People with that surname have publicly described forms and systems rejecting their name or treating it as missing.",
12
- "affects": [
13
- "any 'treat the string null as empty' sanitizer"
14
- ],
15
- "repros": [
16
- {
17
- "language": "python",
18
- "code": "def clean_field(name):\n if not name or name.strip().lower() == \"null\":\n return None\n return name.strip()\n\nprint(clean_field(\"Null\"))\nprint(clean_field(\"O'Null\"))\n",
19
- "expected_output": "None\nO'Null",
20
- "verified_with": "Python 3.14.2"
21
- },
22
- {
23
- "language": "javascript",
24
- "code": "function cleanField(name) {\n if (!name || name.toLowerCase() === \"null\") return null;\n return name;\n}\nconsole.log(cleanField(\"Null\"));\nconsole.log(cleanField(\"O'Null\"));\n",
25
- "expected_output": "null\nO'Null",
26
- "verified_with": "Node.js v22.22.2"
27
- }
28
- ],
29
- "fix": "Never infer 'this field is empty' from its text content matching a sentinel word; use an explicit null/empty marker from the data layer (an actual null, not the string \"null\") so a real value can never collide with it.",
30
- "related": [],
31
- "sources": [
32
- {
33
- "title": "WIRED (2015): Hello, I'm Mr. Null. My Name Makes Me Invisible to Computers",
34
- "url": "https://www.wired.com/2015/11/null/"
35
- },
36
- {
37
- "title": "Patrick McKenzie: Falsehoods Programmers Believe About Names",
38
- "url": "https://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/"
39
- }
40
- ],
41
- "tags": [
42
- "names",
43
- "validation",
44
- "forms",
45
- "database",
46
- "parsing"
47
- ],
48
- "test_cases": [
49
- {
50
- "inputs": [
51
- "Null"
52
- ],
53
- "relation": "returns",
54
- "expected": "Null",
55
- "note": "A sanitizer that strips the literal word 'null' will fail this for a real person named Null."
56
- }
57
- ]
58
- }
@@ -1,55 +0,0 @@
1
- {
2
- "id": "single-letter-surname-fails-min-length",
3
- "title": "A 'name must be at least 2 characters' rule rejects a real, single-letter surname",
4
- "category": "names-addresses",
5
- "assumption": "No legitimate name is only one character long.",
6
- "input": {
7
- "display": "\"O\" (a one-letter surname)",
8
- "description": "A form validator's minimum-length rule applied to a surname field."
9
- },
10
- "what_goes_wrong": "A person whose surname genuinely is a single letter is rejected by a length check that has nothing to do with whether the name is real — it's purely a guess about how short a name 'should' be.",
11
- "why": "Single-character surnames exist in multiple naming traditions (for example, several Korean surnames romanize to a single letter), and a minimum-length rule has no way to distinguish a genuinely short name from a placeholder like \"x\" someone typed to get past the form. The rule enforces an assumption about length that is simply false for this input, not a check that catches bad data.",
12
- "affects": [
13
- "any 'minimum name length' validator"
14
- ],
15
- "repros": [
16
- {
17
- "language": "python",
18
- "code": "def validate_name(name):\n if len(name) < 2:\n raise ValueError(\"name must be at least 2 characters\")\n return True\n\nfor name in [\"Yu\", \"O\"]:\n try:\n print(name, validate_name(name))\n except ValueError as e:\n print(name, \"rejected:\", e)\n",
19
- "expected_output": "Yu True\nO rejected: name must be at least 2 characters",
20
- "verified_with": "Python 3.14.2"
21
- },
22
- {
23
- "language": "javascript",
24
- "code": "function validateName(name) {\n if (name.length < 2) throw new Error(\"name must be at least 2 characters\");\n return true;\n}\nfor (const name of [\"Yu\", \"O\"]) {\n try {\n console.log(name, validateName(name));\n } catch (e) {\n console.log(name, \"rejected:\", e.message);\n }\n}\n",
25
- "expected_output": "Yu true\nO rejected: name must be at least 2 characters",
26
- "verified_with": "Node.js v22.22.2"
27
- }
28
- ],
29
- "fix": "Don't impose a minimum name length unless you have a specific, documented reason to; if you need to catch empty or placeholder input, check for that directly (empty string, all the same character) rather than guessing a length threshold.",
30
- "related": [
31
- "mononym-fails-first-last-required"
32
- ],
33
- "sources": [
34
- {
35
- "title": "Patrick McKenzie: Falsehoods Programmers Believe About Names",
36
- "url": "https://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/"
37
- }
38
- ],
39
- "tags": [
40
- "names",
41
- "validation",
42
- "forms",
43
- "length validation",
44
- "i18n"
45
- ],
46
- "test_cases": [
47
- {
48
- "inputs": [
49
- "O"
50
- ],
51
- "relation": "no_crash",
52
- "note": "A real single-letter surname should not be rejected purely on length."
53
- }
54
- ]
55
- }