# HubSpot Contact Lead Email Validation

## Scope Guard

This change prevents the PingCAP HubSpot Contact-to-Lead inbound Flow from
passing blank or malformed email values into Lead writes. It preserves the
existing remote-id shell-create behavior, including both default-owner and
system-owner branches.

In scope:

- generic L0 Flow decision operators that match or negate a string against a
  supplied RE2 pattern;
- a generic L0 connector-mapping option that omits raw blank overwrite values
  for explicitly named target fields from a Flow's create or update image;
- L2 Flow metadata that routes malformed nonblank email to a named custom
  error before lookup, mapping, or DML;
- omitting the direct blank Email assignment from the two Lead shell creates;
- structural contract coverage and final-pack metadata validation.

Out of scope:

- production deployment, event replay, event retry, or data repair;
- changes to HubSpot ingress, queue policy, worker retry policy, or Email field
  validation;
- logging, querying, or storing source email values.

## Research Evidence

The active runtime lineage is NexusCRM `5a70ec5041d76ac438dd6dd38ac038766ae53088`
with `nexuscrm-connectors v0.1.16`. Its Flow operators do not provide regex
matching, and its formula runtime rejects `REGEX`. A Flow custom error carries
its declared status code, is non-retriable, and connector `MarkFailed` retries
at most five times before the row becomes `dead`; route-level `skipped` is not
available as a Flow outcome.

The L2 Flow previously wrote `record.email` directly in both shell creates and
also maps Email for full and email-delta mapping actions. Root-cause review
showed that the active RecordCreate mapping path permits raw blank overwrite,
and that the email-delta mapper writes `mappingSourceRecord.email` from
`payload.propertyValue`, which can differ from the hydrated `record.email`.
The repaired contract therefore validates `mappingSourceRecord.email` and opts
both Lead mapping writes out of raw blank overwrites.

## Options

1. Approximate the Email regex with existing Flow string operators. Rejected:
   it cannot express the runtime Email contract exactly.
2. Use the Flow formula `REGEX` function. Rejected: the selected L0 runtime
   explicitly rejects it as unsupported.
3. Add generic `MatchesRegex` and `DoesNotMatchRegex` Flow decision operators,
   plus an explicit `skipBlankTargetFields` mapping-write option.
   Chosen: narrowly reusable L0 interpreter capabilities, with the HubSpot
   Email policy and route retained in L2.

## Contract

- `ChooseRemoteOrEmailLookup` first routes any nonblank
  `mappingSourceRecord.email` that does not match the core Email pattern to
  `FailMalformedEmail`; this covers the actual full and email-delta mapping
  write source.
- `FailMalformedEmail` emits `HUBSPOT_CONTACT_EMAIL_INVALID`; it is observable
  in Flow/connector error state and is bounded by the existing five-attempt
  connector failure policy.
- Blank email continues to reach `ChooseLeadShellOrFail` when a HubSpot id is
  present. Neither Lead shell create contains a direct Email assignment, and
  both the creates and the post-create Lead mapping action set
  `skipBlankTargetFields=["Email"]` so raw blank Email never enters the DML
  write image while other mapping fields retain their existing clear semantics.
- A valid email continues through the existing remote-id and email-resolution
  paths without a routing change.

## Dependency And Verification

The L2 change depends on NexusCRM PR #3636 at `ba8f82f3f`; it must update its
upstream runtime lineage after that L0 change merges and before release-class
use. Verification includes L0 unit coverage for both the actual action and
RecordCreate mapping write images, L2 Flow contract checks for
blank/malformed/valid values and both shell owner branches, the targeted
companion tests, and final-pack metadata audit. No environment mutation is part
of this work.
