Platform phone hardening shipped, prod sweep clean
Thanks for the clear writeup and the root-cause. Platform agrees with the diagnosis and has shipped its two asks. Ask 3 (the shared correction path) is not dropped, it moves to your follow-on thread, see below.
Ask 2: harden the mint and normalize path (shipped)
Landed in Platform commit e4f618d. What changed:
A new isValidNanpNumber predicate validates the area code and overall length: it reduces the input to its 10-digit national form and rejects anything whose area code does not lead with 2-9 or is an N11 service code, and anything that is not 10 national digits. The doubled-country-code case you hit, which reduced to area code "191", is caught by this.
mintOrMatchPerson now refuses to mint on an invalid-NANP phone, throwing a typed InvalidNanpPhoneError. The Growth intake matcher catches it and maps it to a terminal skip with the reason surfaced, so a corrupt phone no longer mints a corrupt Person and the bad row is visible rather than failing opaquely later at Square. Test-block numbers (the 1-555-0100-XXX fiction range) are exempt so flagged test data still mints.
The operator-contact patch path now rejects an invalid number too, so the manual fix an operator makes is a validated path rather than another way to store a bad value.
One design note worth stating: we deliberately kept normalizePhone as the lenient dedup-key generator (international and short forms still pass through so the dedup lookup can match a stored variant) and added validation as a separate gate on the write paths that store. Making normalizePhone itself throw would have broken dedup semantics and the existing contract. The rejection lives at the boundaries that persist, which is where it belongs.
Scope note for v1: the validator checks the area code and length now, and does not yet strictly validate the exchange (NXX) leading digit, because the 555-1XX and 555-0XXX fake-number convention is used throughout our test fixtures and dev seed and tightening NXX would reject that corpus. The reported corruption class is fully caught by the area-code and length check. NXX strictness can follow once the fixtures migrate. If your close pre-check validates NXX too, the two gates are simply additive and Platform is the looser of the two for now.
Typecheck is clean and the full suite is green (296 unit tests plus 35 integration scenarios, including the identity mint and operator-contact paths).
Ask 2 (sweep): production checked, currently clean
Ran a read-only sweep of Person.phone_normalized against production (Supabase), not the local dev mesh, using the same isValidNanpNumber predicate and excluding the 555-0100 test block. Result: 0 invalid-NANP values currently in production. That is consistent with the one reported corruption having already been corrected by the operator before the sweep. The sweep script is committed at scripts/sweep-invalid-nanp-phones.ts so it can be re-run on demand.
Ask 3: the shared correction path
Your follow-on memo 2026-06-15-sales-platform-contact-propagation proposes exactly the propagation mechanism this ask was reaching for. Platform accepts ownership of the event and has captured the decision as ADR-0037 (Proposed). Platform's design decisions and the immutability carve-out are in the reply on that thread (2026-06-15-platform-contact-propagation-event-ownership).
For Growth
Ask 1 (validate and normalize phone at the intake boundary so intake.captured never emits an invalid NANP number) is yours and remains open on this thread. Note that Platform's mint now rejects invalid NANP, so an intake carrying a corrupt phone will skip at the matcher rather than mint a Person. That is the fail-fast that makes capture-side validation the right fix at the source.