Objective Success Criteria
Why this code is needed
Today the MPI matcher sees only the identifiers that arrive natively in a single CCD or HL7 message. When a patient has records at multiple facilities, each message carries only one facility's MRN. The engine falls back to demographic rules (name, DOB, address), which have a confirmed mis-merge risk at rule 9 and an untested high-volume risk at rule 16.
Multi-ID enrichment fills that gap: before the MPI call fires, it reads the other facility MRNs that Mirth has already stored in reliance_patient_ids and adds them to the FHIR payload. The engine can then anchor the match on an identifier instead of demographics.
This project is intentionally additive. The additional identifiers sourced from reliance_patient_ids are merged alongside — not in place of — the FMRN value that IMAT already extracts natively from each message. Both the IMAT-extracted FMRN and the supplemental IDs reach the matcher together, giving the engine more to work with without removing any existing signal. We have not decided to move away from the IMAT-pushed FMRN; the current release only adds to it.
This also enables cross-pipeline FMRN matching. When a contributor's HL7v2 message is received and indexed, Mirth captures that facility's MRN in reliance_patient_ids during onboarding. When a CCD for the same patient arrives later via a different pipeline, Multi-ID enrichment reads that already-stored FMRN and injects it into the CCD's FHIR payload — allowing the MPI to match on the facility identifier and ID value that came from the HL7 feed, even though the CCD pipeline never saw them directly. Without enrichment, the CCD would match only on demographics; with it, the FMRN captured upstream in the onboarding flow bridges the two pipelines, making matches more precise and consistent across feed types.
The module also introduces facility_set_organization as a first-class concept on each message, sourced from reliance_msg_facility_set_org. This field is intended to govern facility set creation and control — replacing the current situation where a single contributor can produce many different sending_facility permutations that fragment their records across multiple facility sets. For example, Assante had over 50 distinct sending_facility values; facility_set_organization will consolidate all of those under a single authoritative identifier, giving the system one stable handle to use for facility set assignment and downstream processing.
Stage evidence
Matching moved off demographic rules and onto identifier rules
2,139-message intersection of Run 1 (flag off) and Run 2 (flag on), stage01.rel.imat.us, 2026-09-18. Amber flows are confirmed successful matches where the rule lookup fired before store 1100 indexed the PI — corrected by the retry fix now deployed.
| Metric | Run 1 — flag off (baseline) | Run 2 — flag on (Multi-ID) | What it means |
|---|---|---|---|
| Messages compared | 2,139 (same messages in both runs) | Apples-to-apples intersection | |
| FMRNs pushed into matching payload | 0 | 2,628 | Multi-ID supplied identifiers the MPI never saw before |
| Messages where FMRN drove the match | 95 (3.8% of run) | 501 (22.4% of run) | 5× more messages matched via identifier rather than demographics |
| Messages matched by identifier rules (2 or 7) | 53 (2.1%) | 468 (20.9%) | Strong identifier-anchored rules absorbed traffic from weaker demographic rules |
| Existing patients re-routed to a different persona | — | 0 of 1,369 | No existing patient was incorrectly changed — safe to enable |
Rule 16 matches (the highest-volume demographic rule at ~30% of messages) moved to rule 2 (149 messages). Rule 20 matches moved to rule 7 (51 messages). These are the two identifier-anchored rules that absorbed the upgrade volume. The pattern is consistent: each demographic rule has a fixed identifier-tier partner, pending confirmation of rule definitions with IMAT.
Harness tests F_04b (sparse-demographic patient consolidated via supplemental FMRN alone) and M_02b (primary FMRN miss resolved by cross-reference FMRN) both passed, confirming the mechanism independently of the pressure test population.
Production targets
| Metric | Phase 1 baseline | Phase 2 target |
|---|---|---|
| IDs pushed / IDs available | 0 (flag off) | ≥ 95% |
| FMRN-utilized share | Measure in Phase 1 | ≥ 15% |
| Identifier rule (2 + 7) share | Measure in Phase 1 | ≥ 15% |
| Persona changes on pre-existing patients | 0 | 0 |
| Match rate regression | Phase 1 baseline | ≤ Phase 1 |
| Missing rule_fired on success | ≤ 1% | ≤ 1% (retry fix deployed) |
| Seconds per record | Measure in Phase 1 | ≤ 10% above Phase 1 |
What Multi-ID does not fix
- Weak-demographic merges when no FMRN exists — harness F_02b confirmed a mis-merge on name, DOB, and phone under rule 9.
- Silent SSN conflicts — harness D_01b merged an SSN match with a DOB more than a year off with no visible alert.
- MI extraction gap on SF2 CCD path — R_01, F_01a, M_05a all landed one tier low, likely MI not reaching the payload.
- HL7 FMRN recognition — X_01a with full identifiers landed on rule 7, suggesting the FMRN is not recognized on the HL7 path.
These exist with the flag off; Multi-ID reduces exposure to the first two by moving volume off demographic rules.
Worker Injection — where enrichment enters the normal process
The enrichment module wraps three points in mpi_and_emit_worker.py without changing the core MPI call. Every step degrades gracefully if its flag is off or a dependency is unavailable.
•
reliance_patient_ids — the additive facility MRNs extracted from the message•
reliance_msg_facility_set_org — the org identifier used to control facility set creationenhanced_id module — opens the DB, checks whether supplemental IDs exist for this message, and if so passes them to build_fhir() to merge into the FHIR payload before matchingsupplemental.pyissue_mpi_call()store 1100
outcome.pyrollback.pyGitLab: mpi_and_emit_v2 (enhancement_id)
① Pre-match enrichment — supplemental.py
Runs when ENABLE_MSG_METADATA_ENRICHMENT = True, before issue_mpi_call().
- Opens a warehouse connection (
metadata_conn, separate from the main connection). - Queries
reliance_patient_idsbymsg_id→ a list of(assigning_authority, type, patient_id)rows. - Filters by
MSG_METADATA_ALLOWED_ID_TYPES— currentlymrn / mr / fmrn → fmrn. SSN excluded by decision. - Merges FMRNs into
data_tags[FMRN_FIELD], deduping on(authority.upper(), id). A hard cap of 50 identifiers is enforced before the payload is passed to the MPI call — in practice most pipelines present 2–3 IDs; the cap prevents payload growth for edge-case contributors with large cross-reference lists. - Queries
reliance_msg_facility_set_orgbymsg_id→ setssending_org_idin the payload.
If either query fails, the error is logged and the process continues without the enriched identifiers — the MPI call still fires.
② Post-match recording — outcome.py
Runs after issue_mpi_call() returns, when ENABLE_MATCH_QUALITY_EVAL = True.
- Calls
_fetch_pi_rule_with_retry(pi): polls store 1100 every 1 s for up to 10 s untilrule_firedandmatch_typeappear. Store 1100 indexes a PI asynchronously, so a single immediate read races the index. - Calls
evaluate_fmrn_utilization()to setfmrn_utilizedandfmrn_available_unused. - Upserts one row into
warehouse.reliance_match_reconciliationwith 38 fields covering identifiers present, rule fired, match type, timing and enrichment flags.
③ Rollback capture — rollback.py
Runs after a successful match, when ENABLE_ROLLBACK = True.
- Writes one row to
utilities.rollback_pipeline: msg_id, pi (e.g.52309187.PI), mpid, imat_dest, psuri. - Reverse is via
rollback_script.py --msg-id <id>, which strips the.PIsuffix and callsextensions/mpi/transactions/delete.
What enrichment does NOT change
| Unchanged | Why |
|---|---|
| Native FHIR field parsing | Enrichment only appends identifiers; it never removes fields the original parse set |
issue_mpi_call() itself | The MPI call is identical; enrichment only changes the payload that goes in |
reliance_patient_ids | Read-only; Mirth owns writes to this table |
| Emit / indexing steps | Downstream is unaffected; enrichment is entirely pre-match |
Settings & Flags
All three feature flags ship off. A flag change takes effect on the next batch, about 1 minute, with no worker restart.
| Flag | Default | Effect when True | Phase on |
|---|---|---|---|
ENABLE_MSG_METADATA_ENRICHMENT | False | Pre-match FMRN injection from relational tables | Phase 2 |
ENABLE_MATCH_QUALITY_EVAL | False | Post-match store 1100 lookup + reconciliation write | Phase 1 |
ENABLE_ROLLBACK | False | Capture PI + source URI for per-message rollback | Phase 1 |
Tuning settings
| Setting | Default | Effect |
|---|---|---|
MSG_METADATA_ALLOWED_ID_TYPES | {"mrn":"fmrn","mr":"fmrn","fmrn":"fmrn"} | Allowlist of Mirth ID types that reach the matcher. SSN excluded by decision. |
PI_RULE_LOOKUP_TIMEOUT_S | 10 | Max seconds to poll store 1100 for rule_fired after a match |
PI_RULE_LOOKUP_INTERVAL_S | 1 | Seconds between store 1100 poll attempts |
ROLLBACK_DB_NAME | "utilities" | DB holding rollback_pipeline; must differ from warehouse |
FHIR_FACILITY_ID_CAP | 50 | Maximum number of facility identifiers that can be appended to the FHIR payload per message. Hard cap enforced before issue_mpi_call(). In practice most pipelines present 2–3 IDs; the cap exists to prevent unbounded payload growth for edge-case contributors with large cross-reference lists. |
Confirm flags after deploy
cd /opt/tools/mpi_and_emit_v2
python -c "
from enrichment.message_enrichment_pipeline import MessageEnricher
e = MessageEnricher()
print('metadata_enrichment_available:', e.metadata_enrichment_available)
print('match_quality_eval_available: ', e.match_quality_eval_available)
e.close()
"
# Phase 0 expected output:
# metadata_enrichment_available: False
# match_quality_eval_available: False
Database Tables
The enrichment module reads Mirth-owned tables and writes to tables it owns. Mirth-owned tables are never modified; the service user holds SELECT only on them.
Mirth-owned (read-only to enrichment)
| Table | DB | Key columns | What it stores |
|---|---|---|---|
reliance_patient_ids | warehouse | id PK, msg_id FK+IDX, assigning_authority, type, patient_id | One row per identifier Mirth extracted from each message. Multiple rows per msg_id when a CCD carries multiple MRNs. |
reliance_msg_facility_set_org | warehouse | msg_id PK+FK, facility_set_org | The facility/org Mirth attributed to the sending organization for this message. |
reliance_msg_changelog | warehouse | msg_id PK+FK, changelog, facility_set_org, filename_gen IDX | Mirth audit log. Not read by the enrichment code. |
MessageStore | warehouse | MsgId PK, MsgContent, MsgSource, MsgReceived | Core message store. Enrichment holds SELECT only. MsgContent can be NULLed only by rollback with --purge-raw-content, which requires a separate UPDATE grant withheld in production. |
Enrichment-owned (read/write)
| Table | DB | Key columns | What it stores |
|---|---|---|---|
reliance_match_reconciliation | warehouse | msg_id PK+FK, 37 further columns | One row per processed message: rule_fired, match_type, identifiers present (had_fmrn/ssn/dob/ln/fn/mi/ph/street_zip), ids_available, ids_pushed, fmrn_utilized, mpid_reassigned, pipeline timing, error_detail. The source of all match quality reports. |
reliance_persona_quality_flags | warehouse | mpid PK (composite with observed_at) | Persona-level snapshots written by the backfill: multi_ssn_persona, persona_shrank, high_facility_concentration. One row per persona per backfill run. |
rollback_pipeline | utilities | id PK AUTO_INCREMENT, msg_id UNIQUE, pi IDX | One row per matched message: pi, mpid, imat_dest, psuri, captured_at. rolled_back_at set to NOW() when rollback_script.py successfully reverses it. |
Stage-only (not deployed to production)
| Table | DB | What it stores |
|---|---|---|
pressure_test_snapshot | utilities | Copies of reconciliation rows per pressure-test run label, so the comparison survives the rollback between runs. |
Migration status
| Migration | Target DB | Status |
|---|---|---|
009_persona_quality_flags.sql | warehouse | Applied on stage |
010_rollback_pipeline.sql | utilities (also creates DB) | Applied on stage |
011_pressure_test_snapshot.sql | utilities | Stage only — do not apply to production |
Entity Relationship Diagram
Full schema including service user access lines. Orange dashed = SELECT only (stage also UPDATE/DELETE); teal = SELECT·INSERT·UPDATE·DELETE; blue = SELECT·INSERT·UPDATE·DELETE.
Users that interact with the code
Three actors touch the system: the Mirth service that populates the source tables, the reliance DB user that the worker and rollback run as, and the operator who runs rollback_script.py manually.
Mirth (populates source tables)
| Action | Tables written |
|---|---|
| CCD / HL7 channel processing | reliance_patient_ids, reliance_msg_facility_set_org, reliance_msg_changelog |
Enrichment reads these tables but never writes to them. Every prod channel must populate them before enrichment is enabled — verify with the SQL in the Production Prerequisites section of the main doc.
reliance@% (worker, backfill, rollback)
| Table | Grant | Reason |
|---|---|---|
warehouse.MessageStore | SELECT | Read MsgSource, MsgReceived for reconciliation |
warehouse.reliance_patient_ids | SELECT | Read-only; Mirth owns writes |
warehouse.reliance_msg_facility_set_org | SELECT | Read-only; Mirth owns writes |
warehouse.reliance_msg_changelog | SELECT | Read-only; Mirth owns writes |
warehouse.reliance_match_reconciliation | SELECT, INSERT, UPDATE, DELETE | Enrichment writes and rollback deletes |
warehouse.reliance_persona_quality_flags | SELECT, INSERT, UPDATE, DELETE | Backfill writes persona snapshots |
utilities.rollback_pipeline | SELECT, INSERT, UPDATE, DELETE | Worker inserts; rollback_script marks complete |
MessageStore and DELETE on reliance_patient_ids. Without them, --purge-raw-content and --delete-mirth-ids fail closed. These grants exist on stage for testing; they must not be applied to production.
Operator (rollback)
Runs rollback_script.py manually, always with --msg-id <id> or an explicit list. Never run bare in production — it would process every pending row in rollback_pipeline.
Also needs a RelianceApiCall session with access to:
extensions/mpi/transactions/delete— to remove PI records from store 1100/cache/deleterecord— to remove source records from all search stores- Read access to store 1100 and store 1101 (to verify cleanup)
Credentials file: /opt/search/appliance5/conf/.mariadb_connection.json (prod) or .mariadb_connection_local.json (stage), containing the reliance user and password. Must be chmod 600.
Release Plan
Four phases, one flag per phase, each started only when the previous gate passes. A flag change takes effect within about one minute with no restart.
| Phase | Enrichment | Quality Eval | Rollback | Duration | Exit gate |
|---|---|---|---|---|---|
| 0: Deploy dark | Off | Off | Off | Day 0–1 | Import check prints False False; no new worker errors; seconds per record unchanged over 24 h |
| 1: Measure baseline | Off | On | On | Days 1–14 | ≥ 99% of messages have a reconciliation row; missing-rule rows ≤ 1%; rollback proven on one test message; ≥ 2 backfill snapshots per active persona; baselines recorded |
| 2: Enable Multi-ID | On | On | On | From day 15 | All objective targets met at 24 h, 72 h, 7 days; any breach triggers flag rollback (Level 1) |
| 3: Steady state | On | On | On — until sign-off | From day 22 | Weekly match quality report reviewed; match rate increasing, splits decreasing, and stronger identifier-rule matching confirmed. Only then: turn ENABLE_ROLLBACK = False and drop utilities.rollback_pipeline table. Keep capture on if any doubt remains — it can always be re-enabled. |
PI recording — stays on until results are confirmed
When ENABLE_ROLLBACK = True, every matched message gets a row in utilities.rollback_pipeline recording its PI, MPID and source URI. This is the audit trail that enables a Level 4 data rollback if a wrong match is confirmed.
The PI value stored is the raw form from the MPI response (e.g. 52309187.PI). rollback_script.py strips the .PI suffix before calling the delete endpoint — confirmed working on stage after the fix in this release.
1. Set
ENABLE_ROLLBACK = False in settings.py.2. Verify no new rows are being written to
utilities.rollback_pipeline.3. Drop the
utilities.rollback_pipeline table (and optionally the utilities DB if no other tables remain).If results are ambiguous or a new concern arises later, rollback capture can be re-enabled at any time by setting
ENABLE_ROLLBACK = True and re-running migration 010_rollback_pipeline.sql.
Split / merge reporting
The backfill job runs every 2 hours and captures persona-level stats including persona_shrank (a possible split) and multi_ssn_persona (a possible erroneous merge). These feed the weekly report generated by generate_match_quality_report.py.
A split signal: distinct MPIDs per day rising while messages per persona falls. A merge signal: multi_ssn_persona rate above the Phase 1 baseline, or mpid_reassigned appearing for a message that previously matched correctly.
Rollback plan summary
| Level | Action | Takes effect | Use when |
|---|---|---|---|
| 1: Flag rollback | ENABLE_MSG_METADATA_ENRICHMENT = False | ~1 min | Any Phase 2 gate breach |
| 2: Full disable | All three flags False | ~1 min | Worker errors or slowdown from the release's own lookups |
| 3: Code revert | Redeploy tagged pre-release worker | Minutes | Level 2 does not restore normal behavior |
| 4: Data rollback | rollback_script.py --msg-id <id> then re-feed | Per message | Confirmed wrong match caused by an enriched identifier |
Level 1 — one-liner
sed -i.bak 's/^ENABLE_MSG_METADATA_ENRICHMENT *= *True/ENABLE_MSG_METADATA_ENRICHMENT = False/' \
/opt/tools/mpi_and_emit_v2/settings.py && grep ENABLE_MSG settings.py
Gate query (run daily from Phase 1)
SELECT DATE(processed_at) AS day,
COUNT(*) AS messages,
ROUND(100*SUM(pi IS NOT NULL)/COUNT(*), 2) AS matched_pct,
ROUND(100*SUM(pi IS NOT NULL AND rule_fired IS NULL)/COUNT(*), 2) AS missing_rule_pct,
ROUND(100*SUM(rule_fired IN (2,7))/COUNT(*), 2) AS identifier_rule_pct,
ROUND(100*SUM(fmrn_utilized = 1)/COUNT(*), 2) AS fmrn_utilized_pct,
ROUND(100*SUM(mpid_reassigned = 1)/COUNT(*), 3) AS reassigned_pct
FROM warehouse.reliance_match_reconciliation
WHERE processed_at >= CURDATE() - INTERVAL 14 DAY
GROUP BY DATE(processed_at) ORDER BY day;
Cron Updates
One cron addition (backfill) and two stage-only crons to remove before production deploy.
Add: persona backfill (every 2 hours)
0 */2 * * * root flock -n /var/lock/persona_backfill.lock \
/opt/tools/miniconda3/envs/python3_12/bin/python \
/opt/tools/mpi_and_emit_v2/enrichment/run_persona_backfill.py \
--lookback-hours 3 \
>> /opt/tools/mpi_and_emit_v2/enrichment/match_quality_eval/persona_backfill.log 2>&1
The flock -n wrapper is required. Stage runs show one persona-builder pass can take 17–67 minutes for 1,000–2,700 MPIDs, so overlap is possible without it. If the lock is already held, the new invocation exits silently and the next scheduled run takes over.
run_persona_backfill.py is missing from the stage enrichment/ directory as of the last log entry — the last three scheduled cron runs failed with "No such file or directory". Confirm the file is present on disk before deploying the cron.
Remove: stage-only pressure test crons
These must not appear in production crontabs:
# Remove these lines from crontab on stage before promoting to prod:
* * * * * /usr/bin/python3 /opt/tools/sql_query/sql_query.py stage_ccd_preasure_prod_tests 1000 >> ...
* * * * * /usr/bin/python3 /opt/tools/sql_query/sql_query.py stage_hl7_preasure_prod_tests 1000 >> ...
Backfill health check
grep -E "can't open|failed|Traceback|Backfill complete" \
/opt/tools/mpi_and_emit_v2/enrichment/match_quality_eval/persona_backfill.log | tail -20
Every successful run ends with Backfill complete: N processed, 0 failed. One run on 2026-09-16 failed 339 of 1,802 (19%) — cause unknown, all other runs showed 0 failures. Monitor this on Phase 1 day 1.
Expected throughput
| Date | MPIDs processed | Duration (approx) |
|---|---|---|
| 2026-09-14 | 253 | ~5 min |
| 2026-09-15 | 2,529 | ~48 min |
| 2026-09-17 | 2,728 | ~38 min |
| 2026-09-21 | 2,116 | ~67 min |
| 2026-09-23 | 582 | ~52 min |
Production will likely see larger windows. Measure during Phase 1 and confirm the 2-hour interval is sufficient.
Blocking and Non-Blocking Items
Blocking — must complete before deploy
| # | Item | Why it blocks |
|---|---|---|
| 1 | IMAT review and approval of the enricher_id branch | Changes the MPI submission path |
| 2 | Migrations 009 and 010 applied; prod reliance_match_reconciliation matches the stage schema (38 columns) | Reconciliation, persona and rollback writes fail without them |
| 3 | Every prod channel populates reliance_patient_ids and reliance_msg_facility_set_org | Without rows, enrichment silently adds nothing |
| 4 | reliance@'%' grants applied and prod credentials file verified as chmod 600 | Connections fail; every path degrades to off |
| 5 | Source deployed from enricher_id branch (not compiled zip); __pycache__ cleared; import check passes under prod interpreter | Stale compiled files or wrong Python can run old code silently |
| 6 | run_persona_backfill.py restored on disk; cron wrapped in flock | Phase 1 baselines depend on it; the stage cron is failing now |
| 7 | Production guard in rollback_script.py: refuse to run bare without --msg-id or an explicit list outside stage | Bare mode processes every pending row — up to --limit live patient records deleted |
| 8 | Rollback proven in production on one designated test message (dry run → real run → confirm PI gone from store 1100) | The PI delete failed silently for weeks on stage before the fix; production proof required |
| 9 | Persona-safety check on the pressure test intersection returns changed = 0 | Key safety claim currently rests on sampled rows |
| 10 | All three flags confirmed off in prod settings.py | Deploy must change nothing until Phase 1 |
Open Questions
Items marked with a status reflect answers received so far.
| Question | Status | Impact |
|---|---|---|
Do we have the privileges to make the necessary database changes ourselves — adding new tables to warehouse and creating the utilities DB — or does this require IMAT IT approval and implementation? |
Open |
What needs to happen: • Apply migration 009_persona_quality_flags.sql → adds warehouse.reliance_persona_quality_flags• Apply migration 010_rollback_pipeline.sql → creates the utilities database and adds utilities.rollback_pipeline• Apply reliance@'%' grants on both new tables (see the Users tab)If we do not have DDL privileges on warehouse or the ability to create new databases, IMAT IT will need to run both migrations before Phase 0 can proceed. This is a hard blocker — the worker will fail to write reconciliation and rollback rows without these tables in place.
|
Who has authority to create the reliance@'%' service user and apply the required grants — can we do this ourselves, or does it require IMAT IT? |
Open |
We believe we may be able to apply the grants ourselves, but this depends on whether the account running the migration has GRANT OPTION on the relevant databases. Needs confirmation before Phase 0:• Does our current DB admin account have GRANT OPTION on warehouse and utilities?• If the reliance user does not already exist in production, do we have CREATE USER privilege, or must IMAT IT provision it?• Who verifies the credentials file ( .mariadb_connection.json) is correctly permissioned (chmod 600) in the production environment?
|
Open |
| What is the CR approval and release ownership process — if the change request is approved, who releases the GitLab code to production, and who is assigned as owner for DB generation and ongoing maintenance? | • CR timing: What is the expected turnaround from CR submission to approval?
Rules Reference
Full rule set from production rules.xml, provided 2026-09-24. Rule 0 (exact re-match) is engine-level and does not appear in the file.
Rules 1, 2, 4, 6, 7, 9, 11, 12, 14, 18, 21, 23, 27, 28, 29, 30, 31, 32, 33
FMRN is present — match is identifier-driven. Multi-ID promotes messages onto these rules.
Rules 3, 5, 8, 10, 13, 15, 16, 17, 19, 20, 22, 24, 25, 26, 34, 36, 37
No FMRN — match rests on name, DOB, demographics. Mis-merge risk at rules 9, 16, 37.
Key rules for Multi-ID analysis
| Rule | Fields | Role in Multi-ID |
|---|---|---|
| 2 | FMRN · LN · FN · MI · DOB · gender · street_zip | Primary upgrade target — absorbs rule 16 transitions (149 messages). Full-strength identifier match with address. |
| 7 | FMRN · LN · FN · DOB · gender · street_zip | Secondary upgrade target — absorbs rule 20 transitions (51 messages). Same as rule 2 without MI. |
| 14 | FMRN · LN · FN · MI · DOB · gender | FMRN-anchored without address. Harness R_02 confirmed. |
| 18 | FMRN · LN · FN · DOB · gender | Primary supplemental-FMRN consolidation rule. Harness F_04b, M_02b confirmed. |
| 16 | LN · FN · MI · DOB · gender · street_zip | Demographic fallback — ~30% of Run 1 messages. Multi-ID promotes to rule 2. |
| 20 | LN · FN · DOB · gender · PH · street_zip | Demographic fallback — Multi-ID promotes to rule 7. |
| 34 | LN · FN · DOB · gender · street_zip | Demographic fallback — same as 16 without MI. |
| 37 | LN · FN · DOB · PH | Mis-merge confirmed — F_02b: two distinct patients merged. Candidate for tightening. |
| 9 | FMRN · LN · FN · DOB · gender · PH | FMRN-anchored with phone. Fired in M_05b. FMRN present — not the same weakness as rule 37. |
| 6 | FMRN · LN · FN · DOB · gender · SSN | Fired uniformly across M_06–M_12. Likely test-environment contamination, not a rule defect. |
| 0 | Exact re-match (engine-level) | Not in rules.xml. ~33% of Run 1 messages; drops to ~25% under Multi-ID as enriched payloads differ. |
Rule transition map (pressure test)
| From (no FMRN) | Fields | To (FMRN-anchored) | Fields added | Messages |
|---|---|---|---|---|
| Rule 16 | LN FN MI DOB gender street_zip | Rule 2 | + FMRN | 149 |
| Rule 20 | LN FN DOB gender PH street_zip | Rule 7 | + FMRN (− PH) | 51 |
| Rule 0 | Exact re-match | Rule 7 | Payload changed by FMRN injection | 52 |
| Rule 0 | Exact re-match | Rule 2 | Payload changed by FMRN injection | 52 |
| Rule 17 | LN FN MI DOB gender PH | Rule 2 | + FMRN (− PH + street_zip) | 21 |
| Rule 37 | LN FN DOB PH | Rule 7 | + FMRN + gender + street_zip | 20 |
| Rule 34 | LN FN DOB gender street_zip | Rule 7 | + FMRN | 9 |
Full rule listing
| Rule | FMRN | Fields matched |
|---|---|---|
| 1 | ✓ | FMRN LN FN MI DOB gender SSN |
| 2 | ✓ | FMRN LN FN MI DOB gender street_zip |
| 3 | — | LN FN MI DOB gender SSN street_zip |
| 4 | ✓ | FMRN LN FN MI DOB gender PH |
| 5 | — | LN FN MI DOB gender SSN PH |
| 6 | ✓ | FMRN LN FN DOB gender SSN |
| 7 | ✓ | FMRN LN FN DOB gender street_zip |
| 8 | — | LN FN DOB gender SSN street_zip |
| 9 | ✓ | FMRN LN FN DOB gender PH |
| 10 | — | LN FN DOB gender SSN PH |
| 11 | ✓ | FMRN LN FN DOB SSN |
| 12 | ✓ | FMRN LN FN DOB PH street_zip |
| 13 | — | LN FN DOB SSN PH street_zip |
| 14 | ✓ | FMRN LN FN MI DOB gender |
| 15 | — | LN FN MI DOB gender SSN |
| 16 | — | LN FN MI DOB gender street_zip |
| 17 | — | LN FN MI DOB gender PH |
| 18 | ✓ | FMRN LN FN DOB gender |
| 19 | — | LN FN DOB gender SSN |
| 20 | — | LN FN DOB gender PH street_zip |
| 21 | ✓ | FMRN LN DOB gender SSN |
| 22 | — | FN DOB gender SSN PH street_zip |
| 23 | ✓ | FMRN LN FN gender |
| 24 | — | FN DOB gender SSN street_zip |
| 25 | — | LN FN DOB SSN |
| 26 | — | LN FN DOB PH street_zip |
| 27 | ✓ | FMRN LN DOB gender PH street_zip |
| 28 | ✓ | FMRN LN DOB gender SSN PH |
| 29 | ✓ | FMRN LN FN SSN street_zip |
| 30 | ✓ | FMRN LN gender SSN street_zip |
| 31 | ✓ | FMRN LN FN gender SSN |
| 32 | ✓ | FMRN LN FN gender PH street_zip |
| 33 | ✓ | FMRN LN DOB gender |
| 34 | — | LN FN DOB gender street_zip |
| 36 | — | LN FN DOB street_zip |
| 37 | — | LN FN DOB PH |
Note: rule 35 is absent from the provided rules.xml. Rule 0 is engine-level and does not appear in the file.