What's wrong
IsIbanAttribute's validator checks ^[A-Z]{2}[0-9]{2}[A-Z0-9]+$ (Semantics.Strings.Identifiers/IsIbanAttribute.cs:24). In .NET, $ without RegexOptions.Multiline also matches just before a final \n, so a value with a trailing newline passes the structural check.
Iban.MakeCanonical (Semantics.Strings.Identifiers/Iban.cs:21) strips only ' ', so the newline reaches the validator. The length gate (15–34) allows the extra character. PassesMod97 then feeds '\n' through ApplyChar as if it were a letter: c - 'A' + 10 gives −45. Whether the value passes then depends on the check digits, not on whether the IBAN is valid.
Reproduction (run against the built assembly)
Iban.Create<Iban>("GB67WEST12345698765432\n") -> ACCEPTED, stored as "GB67WEST12345698765432\n" (length 23)
Iban.Create<Iban>("GB67WEST12345698765432") -> ArgumentException (GB67 is not a valid IBAN)
Iban.Create<Iban>("GB82WEST12345698765432") -> ACCEPTED (the valid reference IBAN)
Iban.Create<Iban>("GB82WEST12345698765432\n") -> ArgumentException (valid IBAN rejected)
Why it matters
- A value with wrong check digits validates, and its stored canonical form contains a control character.
- A valid IBAN pasted with a trailing newline, which is common from text areas and files, is rejected with a misleading "mod-97 checksum failed".
- The identifiers spec (
docs/superpowers/specs/2026-07-01-concrete-semantic-strings-design.md) says canonicalization strips whitespace, but only spaces are stripped.
The same $ anchor appears in IsUuidAttribute.cs:23 and IsUlidAttribute.cs:23. Uuid and Ulid are protected today only because their MakeCanonical calls Trim(). The attributes are public, so a consumer type that uses them without trimming would inherit the hole.
Suggested fix / acceptance criteria
- Anchor the identifier patterns with
\z (or \A…\z) instead of $.
- Make
Iban.MakeCanonical strip all whitespace (char.IsWhiteSpace), as the spec says, so that "GB82 WEST 1234 5698 7654 32\n" canonicalizes to GB82WEST12345698765432 and validates.
- Tests:
- an invalid IBAN with a trailing
\n is rejected;
- a valid IBAN with a trailing
\n or \t canonicalizes and validates;
[IsUuid] / [IsUlid] on a type that doesn't trim reject a trailing \n.
What's wrong
IsIbanAttribute's validator checks^[A-Z]{2}[0-9]{2}[A-Z0-9]+$(Semantics.Strings.Identifiers/IsIbanAttribute.cs:24). In .NET,$withoutRegexOptions.Multilinealso matches just before a final\n, so a value with a trailing newline passes the structural check.Iban.MakeCanonical(Semantics.Strings.Identifiers/Iban.cs:21) strips only' ', so the newline reaches the validator. The length gate (15–34) allows the extra character.PassesMod97then feeds'\n'throughApplyCharas if it were a letter:c - 'A' + 10gives −45. Whether the value passes then depends on the check digits, not on whether the IBAN is valid.Reproduction (run against the built assembly)
Why it matters
docs/superpowers/specs/2026-07-01-concrete-semantic-strings-design.md) says canonicalization strips whitespace, but only spaces are stripped.The same
$anchor appears inIsUuidAttribute.cs:23andIsUlidAttribute.cs:23.UuidandUlidare protected today only because theirMakeCanonicalcallsTrim(). The attributes are public, so a consumer type that uses them without trimming would inherit the hole.Suggested fix / acceptance criteria
\z(or\A…\z) instead of$.Iban.MakeCanonicalstrip all whitespace (char.IsWhiteSpace), as the spec says, so that"GB82 WEST 1234 5698 7654 32\n"canonicalizes toGB82WEST12345698765432and validates.\nis rejected;\nor\tcanonicalizes and validates;[IsUuid]/[IsUlid]on a type that doesn't trim reject a trailing\n.