Skip to content

Implement surgical repair for context conflicts#1

Open
ankit-kumar-22 wants to merge 1 commit into
Harshal96:mainfrom
ankit-kumar-22:ankit/feature_preserve_valid_sub_fields
Open

Implement surgical repair for context conflicts#1
ankit-kumar-22 wants to merge 1 commit into
Harshal96:mainfrom
ankit-kumar-22:ankit/feature_preserve_valid_sub_fields

Conversation

@ankit-kumar-22

Copy link
Copy Markdown

Replace coarse context repair that drops entire objects with surgical field-level repair that preserves valid sub-fields.

Changes:

  • _repair() now preserves email when only phone country conflicts
  • _repair() now preserves city/region/country when only postal code is invalid
  • Added make_address_for_city() to regenerate addresses with valid postal codes for a given city
  • Updated AddressProvider and ContactProvider to use preserved facts
  • Added 3 regression tests verifying surgical repair behavior

Before: Phone conflict → entire contact dropped (email + phone lost)
After: Phone conflict → only phone regenerated, email preserved

Test Evidence

  • Tested via custom script
#!/usr/bin/env python3
"""
End-to-end test of surgical repair mechanism.
Run: python test_surgical_repair_e2e.py
"""

from verisim import PersonRecord, Address, Contact, PhoneNumber, Verisim


def print_section(title: str):
    print(f"\n{'='*70}")
    print(f"  {title}")
    print(f"{'='*70}\n")


def test_1_email_preserved_on_phone_conflict():
    """Test: Phone country mismatch → email preserved, phone regenerated"""
    print_section("TEST 1: Email Preserved on Phone Country Conflict")
    
    v = Verisim(seed=1, locale="en_US")
    
    # User provides US address + Indian phone + specific email
    us_address = Address(
        line1="123 Main Street",
        city="San Francisco",
        region="California",
        region_code="CA",
        postal_code="94107",
        country="United States",
        country_code="US",
    )
    
    indian_phone = PhoneNumber(
        e164="+919876543210",
        national="98765 43210",
        country_code="IN",
        country_calling_code="+91",
    )
    
    user_email = "john.smith@example.com"
    contact = Contact(email=user_email, phone=indian_phone)
    
    print("INPUT:")
    print(f"  Address: {us_address.city}, {us_address.region_code}, {us_address.country_code}")
    print(f"  Email: {user_email}")
    print(f"  Phone: {indian_phone.e164} ({indian_phone.country_code})")
    print(f"  ❌ CONFLICT: Phone country (IN) ≠ Address country (US)")
    
    # Generate with repair mode
    record = v.generate(
        PersonRecord,
        context={"address": us_address, "contact": contact},
        mode="repair"
    )
    
    print("\nOUTPUT (after repair):")
    print(f"  Address: {record.address.city}, {record.address.region_code}, {record.address.country_code}")
    print(f"  Email: {record.contact.email}")
    print(f"  Phone: {record.contact.phone.e164} ({record.contact.phone.country_code})")
    
    # Verify
    print("\nVERIFICATION:")
    email_preserved = record.contact.email == user_email
    phone_regenerated = record.contact.phone.country_code == "US"
    phone_valid = record.contact.phone.e164.startswith("+1")
    
    print(f"  ✓ Email preserved: {email_preserved} (expected: True)")
    print(f"  ✓ Phone regenerated: {phone_regenerated} (expected: True)")
    print(f"  ✓ Phone valid format: {phone_valid} (expected: True)")
    
    assert email_preserved, "Email should be preserved!"
    assert phone_regenerated, "Phone country should be regenerated to US!"
    assert phone_valid, "Phone should have US calling code!"
    print("\n✅ TEST 1 PASSED\n")


def test_2_city_region_preserved_on_postal_code_conflict():
    """Test: Invalid postal code → city/region preserved, postal code regenerated"""
    print_section("TEST 2: City & Region Preserved on Invalid Postal Code")
    
    v = Verisim(seed=2, locale="en_US")
    
    # User provides address with invalid postal code
    user_city = "Austin"
    user_region_code = "TX"
    user_country_code = "US"
    
    address = Address(
        line1="456 Oak Avenue",
        city=user_city,
        region="Texas",
        region_code=user_region_code,
        postal_code="99999",  # ❌ Invalid for Austin
        country="United States",
        country_code=user_country_code,
    )
    
    # Get valid postal codes for Austin
    valid_codes = v.data.postal_codes_for_city(user_country_code, user_region_code, user_city)
    
    print("INPUT:")
    print(f"  City: {user_city}")
    print(f"  Region: {user_region_code}")
    print(f"  Country: {user_country_code}")
    print(f"  Postal Code: 99999")
    print(f"  ❌ CONFLICT: 99999 not in valid codes for {user_city}")
    print(f"  Valid codes for {user_city}: {valid_codes[:5]}... ({len(valid_codes)} total)")
    
    # Generate with repair mode
    record = v.generate(
        PersonRecord,
        context={"address": address},
        mode="repair"
    )
    
    print("\nOUTPUT (after repair):")
    print(f"  City: {record.address.city}")
    print(f"  Region: {record.address.region_code}")
    print(f"  Country: {record.address.country_code}")
    print(f"  Postal Code: {record.address.postal_code}")
    
    # Verify
    print("\nVERIFICATION:")
    city_preserved = record.address.city == user_city
    region_preserved = record.address.region_code == user_region_code
    country_preserved = record.address.country_code == user_country_code
    postal_valid = record.address.postal_code in valid_codes
    
    print(f"  ✓ City preserved: {city_preserved} (expected: True)")
    print(f"  ✓ Region preserved: {region_preserved} (expected: True)")
    print(f"  ✓ Country preserved: {country_preserved} (expected: True)")
    print(f"  ✓ Postal code valid: {postal_valid} (expected: True)")
    
    assert city_preserved, "City should be preserved!"
    assert region_preserved, "Region should be preserved!"
    assert country_preserved, "Country should be preserved!"
    assert postal_valid, f"Postal code should be in {valid_codes[:3]}..."
    print("\n✅ TEST 2 PASSED\n")


def test_3_both_email_and_phone_conflict():
    """Test: Both email domain AND phone country conflict → entire contact dropped"""
    print_section("TEST 3: Both Email & Phone Conflict → Entire Contact Regenerated")
    
    v = Verisim(seed=3, locale="en_US")
    
    # Generate a company first
    from verisim import CompanyRecord
    company = v.generate(CompanyRecord)
    
    us_address = Address(
        line1="789 Market Street",
        city="New York",
        region="New York",
        region_code="NY",
        postal_code="10001",
        country="United States",
        country_code="US",
    )
    
    # User provides email from WRONG company + Indian phone
    wrong_email = "employee@wrong-company.example.invalid"
    indian_phone = PhoneNumber(
        e164="+919876543210",
        national="98765 43210",
        country_code="IN",
        country_calling_code="+91",
    )
    contact = Contact(email=wrong_email, phone=indian_phone)
    
    print("INPUT:")
    print(f"  Company: {company.name} (domain: {company.domain})")
    print(f"  Address: {us_address.city}, {us_address.region_code}, {us_address.country_code}")
    print(f"  Email: {wrong_email}")
    print(f"  Phone: {indian_phone.e164} ({indian_phone.country_code})")
    print(f"  ❌ CONFLICT 1: Email domain doesn't match company domain")
    print(f"  ❌ CONFLICT 2: Phone country (IN) ≠ Address country (US)")
    
    # Generate with repair mode
    record = v.generate(
        PersonRecord,
        context={"address": us_address, "contact": contact, "company": company},
        mode="repair"
    )
    
    print("\nOUTPUT (after repair):")
    print(f"  Company: {record.company.name} (domain: {record.company.domain})")
    print(f"  Address: {record.address.city}, {record.address.region_code}, {record.address.country_code}")
    print(f"  Email: {record.contact.email}")
    print(f"  Phone: {record.contact.phone.e164} ({record.contact.phone.country_code})")
    
    # Verify
    print("\nVERIFICATION:")
    email_regenerated = record.contact.email != wrong_email
    email_matches_company = record.contact.email.endswith(f"@{company.domain}")
    phone_regenerated = record.contact.phone.country_code == "US"
    phone_valid = record.contact.phone.e164.startswith("+1")
    
    print(f"  ✓ Email regenerated: {email_regenerated} (expected: True)")
    print(f"  ✓ Email matches company: {email_matches_company} (expected: True)")
    print(f"  ✓ Phone regenerated: {phone_regenerated} (expected: True)")
    print(f"  ✓ Phone valid format: {phone_valid} (expected: True)")
    
    assert email_regenerated, "Email should be regenerated!"
    assert email_matches_company, f"Email should end with @{company.domain}!"
    assert phone_regenerated, "Phone should be regenerated to US!"
    assert phone_valid, "Phone should have US calling code!"
    print("\n✅ TEST 3 PASSED\n")


def test_4_before_and_after_comparison():
    """Visual comparison: old coarse repair vs new surgical repair"""
    print_section("TEST 4: Before & After Comparison")
    
    v = Verisim(seed=4, locale="en_US")
    
    user_email = "alice.johnson@example.com"
    user_phone = PhoneNumber(
        e164="+919876543210",
        national="98765 43210",
        country_code="IN",
        country_calling_code="+91",
    )
    
    us_address = Address(
        line1="100 Tech Street",
        city="Seattle",
        region="Washington",
        region_code="WA",
        postal_code="98101",
        country="United States",
        country_code="US",
    )
    
    contact = Contact(email=user_email, phone=user_phone)
    
    print("SCENARIO:")
    print(f"  User wants to keep email: {user_email}")
    print(f"  User provides Indian phone: {user_phone.e164}")
    print(f"  User provides US address: {us_address.city}, {us_address.region_code}")
    
    print("\nOLD BEHAVIOR (coarse repair):")
    print("  1. Detect: phone country (IN) ≠ address country (US)")
    print("  2. Action: repaired.pop('contact', None)")
    print("  3. Result: ENTIRE contact dropped (email + phone)")
    print("  4. Regenerate: NEW email + NEW phone (user's email LOST)")
    
    print("\nNEW BEHAVIOR (surgical repair):")
    print("  1. Detect: phone country (IN) ≠ address country (US)")
    print("  2. Action: preserve email, drop only phone")
    print("  3. Result: email preserved, phone regenerated")
    print("  4. Regenerate: ONLY phone (user's email KEPT)")
    
    # Generate with new repair mode
    record = v.generate(
        PersonRecord,
        context={"address": us_address, "contact": contact},
        mode="repair"
    )
    
    print("\nACTUAL RESULT:")
    print(f"  Email: {record.contact.email}")
    print(f"    → Matches input? {record.contact.email == user_email} ✓")
    print(f"  Phone: {record.contact.phone.e164}")
    print(f"    → Country: {record.contact.phone.country_code} (was IN, now US) ✓")
    
    assert record.contact.email == user_email, "Email should be preserved!"
    print("\n✅ TEST 4 PASSED\n")


def test_5_multiple_generations_same_seed():
    """Test: Same seed produces deterministic results"""
    print_section("TEST 5: Deterministic Generation with Same Seed")
    
    user_email = "test@example.com"
    user_phone = PhoneNumber(
        e164="+919876543210",
        national="98765 43210",
        country_code="IN",
        country_calling_code="+91",
    )
    
    us_address = Address(
        line1="200 Main Street",
        city="Boston",
        region="Massachusetts",
        region_code="MA",
        postal_code="02101",
        country="United States",
        country_code="US",
    )
    
    contact = Contact(email=user_email, phone=user_phone)
    
    print("INPUT (same for both generations):")
    print(f"  Email: {user_email}")
    print(f"  Phone: {user_phone.e164}")
    print(f"  Address: {us_address.city}, {us_address.region_code}")
    
    # Generate twice with same seed
    v1 = Verisim(seed=42, locale="en_US")
    record1 = v1.generate(
        PersonRecord,
        context={"address": us_address, "contact": contact},
        mode="repair"
    )
    
    v2 = Verisim(seed=42, locale="en_US")
    record2 = v2.generate(
        PersonRecord,
        context={"address": us_address, "contact": contact},
        mode="repair"
    )
    
    print("\nGENERATION 1 (seed=42):")
    print(f"  Phone: {record1.contact.phone.e164}")
    print(f"  Email: {record1.contact.email}")
    
    print("\nGENERATION 2 (seed=42):")
    print(f"  Phone: {record2.contact.phone.e164}")
    print(f"  Email: {record2.contact.email}")
    
    print("\nVERIFICATION:")
    phones_match = record1.contact.phone.e164 == record2.contact.phone.e164
    emails_match = record1.contact.email == record2.contact.email
    
    print(f"  ✓ Phones match: {phones_match} (expected: True)")
    print(f"  ✓ Emails match: {emails_match} (expected: True)")
    
    assert phones_match, "Same seed should produce same phone!"
    assert emails_match, "Same seed should produce same email!"
    print("\n✅ TEST 5 PASSED\n")


if __name__ == "__main__":
    print("\n" + "="*70)
    print("  SURGICAL REPAIR MECHANISM - END-TO-END TESTS")
    print("="*70)
    
    try:
        test_1_email_preserved_on_phone_conflict()
        test_2_city_region_preserved_on_postal_code_conflict()
        test_3_both_email_and_phone_conflict()
        test_4_before_and_after_comparison()
        test_5_multiple_generations_same_seed()
        
        print("\n" + "="*70)
        print("  ✅ ALL E2E TESTS PASSED!")
        print("="*70 + "\n")
    except AssertionError as e:
        print(f"\n❌ TEST FAILED: {e}\n")
        raise

OUTPUT

(verisim) LT-R47WQ32HX1:verisim ankitkumar$ python test_surgical_repair_e2e.py

======================================================================
  SURGICAL REPAIR MECHANISM - END-TO-END TESTS
======================================================================

======================================================================
  TEST 1: Email Preserved on Phone Country Conflict
======================================================================

INPUT:
  Address: San Francisco, CA, US
  Email: john.smith@example.com
  Phone: +919876543210 (IN)
  ❌ CONFLICT: Phone country (IN) ≠ Address country (US)

OUTPUT (after repair):
  Address: San Francisco, CA, US
  Email: john.smith@example.com
  Phone: +15555550000 (US)

VERIFICATION:
  ✓ Email preserved: True (expected: True)
  ✓ Phone regenerated: True (expected: True)
  ✓ Phone valid format: True (expected: True)

✅ TEST 1 PASSED


======================================================================
  TEST 2: City & Region Preserved on Invalid Postal Code
======================================================================

INPUT:
  City: Austin
  Region: TX
  Country: US
  Postal Code: 99999
  ❌ CONFLICT: 99999 not in valid codes for Austin
  Valid codes for Austin: ('73301', '73344', '78701', '78702', '78703')... (74 total)

OUTPUT (after repair):
  City: Austin
  Region: TX
  Country: US
  Postal Code: 78733

VERIFICATION:
  ✓ City preserved: True (expected: True)
  ✓ Region preserved: True (expected: True)
  ✓ Country preserved: True (expected: True)
  ✓ Postal code valid: True (expected: True)

✅ TEST 2 PASSED


======================================================================
  TEST 3: Both Email & Phone Conflict → Entire Contact Regenerated
======================================================================

INPUT:
  Company: Harbor Care Systems (domain: harbor-care-systems.example.invalid)
  Address: New York, NY, US
  Email: employee@wrong-company.example.invalid
  Phone: +919876543210 (IN)
  ❌ CONFLICT 1: Email domain doesn't match company domain
  ❌ CONFLICT 2: Phone country (IN) ≠ Address country (US)

OUTPUT (after repair):
  Company: Harbor Care Systems (domain: harbor-care-systems.example.invalid)
  Address: New York, NY, US
  Email: adia@harbor-care-systems.example.invalid
  Phone: +15555550000 (US)

VERIFICATION:
  ✓ Email regenerated: True (expected: True)
  ✓ Email matches company: True (expected: True)
  ✓ Phone regenerated: True (expected: True)
  ✓ Phone valid format: True (expected: True)

✅ TEST 3 PASSED


======================================================================
  TEST 4: Before & After Comparison
======================================================================

SCENARIO:
  User wants to keep email: alice.johnson@example.com
  User provides Indian phone: +919876543210
  User provides US address: Seattle, WA

OLD BEHAVIOR (coarse repair):
  1. Detect: phone country (IN) ≠ address country (US)
  2. Action: repaired.pop('contact', None)
  3. Result: ENTIRE contact dropped (email + phone)
  4. Regenerate: NEW email + NEW phone (user's email LOST)

NEW BEHAVIOR (surgical repair):
  1. Detect: phone country (IN) ≠ address country (US)
  2. Action: preserve email, drop only phone
  3. Result: email preserved, phone regenerated
  4. Regenerate: ONLY phone (user's email KEPT)

ACTUAL RESULT:
  Email: alice.johnson@example.com
    → Matches input? True ✓
  Phone: +15555550000
    → Country: US (was IN, now US) ✓

✅ TEST 4 PASSED


======================================================================
  TEST 5: Deterministic Generation with Same Seed
======================================================================

INPUT (same for both generations):
  Email: test@example.com
  Phone: +919876543210
  Address: Boston, MA

GENERATION 1 (seed=42):
  Phone: +15555550000
  Email: test@example.com

GENERATION 2 (seed=42):
  Phone: +15555550000
  Email: test@example.com

VERIFICATION:
  ✓ Phones match: True (expected: True)
  ✓ Emails match: True (expected: True)

✅ TEST 5 PASSED


======================================================================
  ✅ ALL E2E TESTS PASSED!
======================================================================

(verisim) LT-R47WQ32HX1:verisim ankitkumar$ 

Comment thread src/verisim/providers.py
phone = self._phone(state, country_code, address)
preserved_email = state.facts.pop("_preserved_contact_email", None)
preserved_phone = state.facts.pop("_preserved_contact_phone", None)
email = str(preserved_email) if preserved_email is not None else self._email(state, person)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For PersonRecord, the graph generates a company before contact, so this can produce a person whose email no longer matches their generated company domain

@Harshal96

Copy link
Copy Markdown
Owner

Can you run format and lint for the changes?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants