@privacyscrubber/mcp-server 2.1.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ps-pii-engine.js CHANGED
@@ -47,9 +47,9 @@ let REGEX_RULES = [
47
47
 
48
48
  // Professional IDs & Organizations
49
49
  { type: 'NAME', isContextName: true, isCorporateName: true, regex: /\b(?:[A-Z][A-Za-z0-9&.,'-]*[ \t\xA0]+){1,5}(?:Inc\.?|LLC|Corp\.?|Corporation|Ltd\.?|Limited|Co\.?|Company|Group|Holdings|Solutions|Services|Technologies|Logistics|Industries|Capital|Bank|Partners|LLP|PLLC)(?:\s+(?:LLC|Inc\.?|Corp\.?|Ltd\.?|USA|Group))?\b/g },
50
- { type: 'ID', regex: /\b(?:Employee|Emp|EE|Worker|Staff|File|Badge|Member|Advisor|Producer|Agent|Borrower)\s*(?:#|ID|No\.?|Number)[:\s#]*[A-Z0-9-]{3,15}\b/gi },
50
+ { type: 'ID', regex: /\b(?:Employee|Emp|EE|Worker|Staff|File|Badge|Member|Advisor|Producer|Agent|Borrower)\s*(?:#|ID|No\.?|Number)[:\s#]*([A-Z0-9-]{3,15})\b/gi },
51
51
  { type: 'ID', regex: /\b(?:Pay\s+Group[:#\s]+[A-Za-z0-9_-]{2,30}|(?:Cost\s+Center|Dept|Department)[:#\s]+[A-Za-z0-9_-]*\d[A-Za-z0-9_-]*)\b/gi },
52
- { type: 'ID', regex: /(?:\b(?:Box\s+d\b|d\.\s*(?:Control|#)?|d\s+Control)\s*(?:Control\s+)?(?:number|no\.?|#|num)?[:\s#]+|\bControl\s*(?:number|no\.?|#|num)?[:\s#]+|\bControl[:#]\s*)([A-Za-z0-9-]{3,30})/gi },
52
+ { type: 'ID', regex: /(?:\b(?:Box\s+d\b|d\.\s*(?:Control|#|number|no\.?|num)|d\s+Control)\s*(?:Control\s+)?(?:number|no\.?|#|num)?[:\s#]+|\bControl\s*(?:number|no\.?|#|num)?[:\s#]+|\bControl[:#]\s*)([A-Za-z0-9-]{3,30})/gi },
53
53
  { type: 'ID', regex: /\bEEID[ -]?\d{4,}\b/gi },
54
54
  { type: 'ID', regex: /\bEMP[-_]\d{3,}\b/gi },
55
55
 
@@ -92,7 +92,7 @@ let REGEX_RULES = [
92
92
  { type: 'ID', regex: /\b\d{2}\/\d{2}\/\d{4}\b/g },
93
93
 
94
94
  // Phone Numbers
95
- { type: 'PHONE', regex: /(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g },
95
+ { type: 'PHONE', regex: /(?<!\w)(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}(?:\s*(?:ext\.?|x)\s*\d{1,5})?\b/gi },
96
96
  { type: 'PHONE', regex: /\+\d{1,3}[ \t.-]+(?:\(?\d{1,4}\)?[ \t.-]+)?\d{2,8}(?:[ \t.-]+\d{2,8}){0,4}\b/g },
97
97
  { type: 'PHONE', regex: /\+?[1-9]\d{1,3}[\s.-]\(?\d{1,4}\)?[\s.-]\d{2,4}[\s.-]\d{4}/g },
98
98
  { type: 'PHONE', regex: /(?:\+44\s?7\d{3}|\(?07\d{3}\)?)\s?\d{3}\s?\d{3}\b/g },
@@ -109,16 +109,18 @@ let REGEX_RULES = [
109
109
  // Generalized Name Detection (First [Middle] Last) — max 1 middle word to avoid grabbing job titles
110
110
  // Middle word must be: a particle (van/de/etc.), a single initial (A. or A), or a capitalized word of ≥2 lowercase letters
111
111
  { type: 'NAME', isAggressiveName: true, regex: /(?<=^|[^\p{L}\p{N}_])(?:\p{Lu}[\p{Ll}'-]*\p{Ll}|\p{Lu}[\p{Ll}'-]*[\p{Lu}'-][\p{Ll}'-]*)(?:[ \t\xA0]+(?:\p{Lu}[\p{Ll}'-]*\p{Ll}|\p{Lu}[\p{Ll}'-]*[\p{Lu}'-][\p{Ll}'-]*|\p{Lu}\.?|van|von|de|di|da|la|le|del|du|der|van[ \t\xA0]+de|van[ \t\xA0]+der)){0,1}[ \t\xA0]+(?:\p{Lu}[\p{Ll}'-]*\p{Ll}|\p{Lu}[\p{Ll}'-]*[\p{Lu}'-][\p{Ll}'-]*)(?:'s)?(?=[^\p{L}\p{N}_]|$)(?![ \t\xA0]*:)/gu },
112
- // Payroll Format Names (e.g. "BARKER, KELLY", "BARKER, KELLY M", "DOE, JOHN M.")
113
- { type: 'NAME', regex: /\b[A-Z]{2,25},\s+[A-Z]{2,25}(?:\s+[A-Z]\.?|\s+[A-Z]{2,25})*\b/g },
112
+ // Payroll & Inverted Format Names (e.g. "BARKER, KELLY", "Vance-Soto, Eleanor", "Kaufman-Alvarez, Liam T.")
113
+ { type: 'NAME', regex: /\b[A-Z][a-zA-Z'’-]+(?:-[A-Za-z'’-]+)?,\s+[A-Z][a-zA-Z'’-]+(?:\s+[A-Z]\.?(?!\w)|\s+[A-Z][a-zA-Z'’-]+)*\b/g },
114
114
  // All-Caps Names (2–3 words, supporting single-letter middle initial e.g. "KELLY M BARKER", "JOHN M BARKER")
115
115
  { type: 'NAME', regex: /(?<=^|[^\p{L}\p{N}_])\p{Lu}{2,}(?:[\p{Lu}'-]*\p{Lu})?(?:[ \t\xA0]+(?:\p{Lu}\.?|[A-Z][a-z]+))?[ \t\xA0]+\p{Lu}{2,}(?:[\p{Lu}'-]*\p{Lu})?(?:'s)?(?=[^\p{L}\p{N}_]|$)(?![ \t\xA0]*:)/gu },
116
116
  // ALL-CAPS first + middle initial(s) with optional spaces + ALL-CAPS last name
117
117
  { type: 'NAME', regex: /(?<=^|[^\p{L}\p{N}_])\p{Lu}{2,}(?:[\p{Lu}'-]*\p{Lu})?(?:[ \t\xA0]*\p{Lu}\.)+[ \t\xA0]*\p{Lu}{2,}(?:[\p{Lu}'-]*\p{Lu})?(?:'s)?(?=[^\p{L}\p{N}_]|$)(?![ \t\xA0]*:)/gu },
118
118
  // ALL-CAPS first name + optional middle initial(s) with optional spaces + Mixed-Case last name
119
119
  { type: 'NAME', regex: /(?<=^|[^\p{L}\p{N}_])\p{Lu}{2,}(?:[\p{Lu}'-]*\p{Lu})?(?:(?:[ \t\xA0]*\p{Lu}\.)+[ \t\xA0]*|[ \t\xA0]+)(?:\p{Lu}\p{Ll}[\p{Ll}'-]*|\p{Lu}[\p{Ll}'-]*[\p{Lu}'-][\p{Ll}'-]*)(?:'s)?(?=[^\p{L}\p{N}_]|$)(?![ \t\xA0]*:)/gu },
120
- // Names with Honorifics (with or without period, supporting single or multi-word full names) (Unicode-safe)
121
- { type: 'NAME', regex: /(?<=^|[^\p{L}\p{N}_])(?:Mr|Mrs|Ms|Dr|Prof|Hon|Mr\.|Mrs\.|Ms\.|Dr\.|Prof\.|Hon\.)[ \t\xA0]+(?:\p{Lu}[\p{Ll}'-]*\p{Ll}|\p{Lu}[\p{Ll}'-]*[\p{Lu}'-][\p{Ll}'-]*)(?:[ \t\xA0]+(?:\p{Lu}[\p{Ll}'-]*\p{Ll}|\p{Lu}[\p{Ll}'-]*[\p{Lu}'-][\p{Ll}'-]*))?(?=[^\p{L}\p{N}_]|$)(?![ \t\xA0]*:)/gu },
120
+ // Names with Honorifics (with or without period, supporting middle initials and hyphenated surnames) (Unicode-safe)
121
+ { type: 'NAME', regex: /(?<=^|[^\p{L}\p{N}_])(?:Mr|Mrs|Ms|Dr|Prof|Hon|Rev|Judge|Atty|Doctor|Professor)(?:\.|\b)[ \t\xA0]+(?:\p{Lu}[\p{L}'-]+)(?:[ \t\xA0]+(?:\p{Lu}\.?|\p{Lu}[\p{L}'-]+)){0,2}(?:[ \t\xA0]+(?:\p{Lu}[\p{L}'-]+))(?=[^\p{L}\p{N}_]|$)(?![ \t\xA0]*:)/gu },
122
+ // Names with Quoted or Parenthetical Nicknames (e.g. Judith "Judy" Diaz, Robert (Bob) Smith)
123
+ { type: 'NAME', regex: /(?<=^|[^\p{L}\p{N}_])(?:\p{Lu}[\p{Ll}'-]+)[ \t\xA0]+(?:"[\p{Lu}\p{Ll}'-]+"|'[\p{Lu}\p{Ll}'-]+'|[“‘][\p{Lu}\p{Ll}'-]+[”’]|\([\p{Lu}\p{Ll}'-]+\))[ \t\xA0]+(?:\p{Lu}[\p{Ll}'-]+)(?:'s)?(?=[^\p{L}\p{N}_]|$)(?![ \t\xA0]*:)/gu },
122
124
 
123
125
  // W-2 Box c Employer Block (Name, Address, and Zip Code)
124
126
  { type: 'NAME', isContextName: true, regex: /(?:(?:\bBox\s+c\b|\bc\.\s*|\bc\s+(?=Employer))\s*(?:Employer(?:'s)?\s*)?(?:name[,\s]+address[,\s]+and\s+ZIP\s+code|name)?|(?:Employer(?:'s)?\s+name[,\s]+address[,\s]+and\s+ZIP\s+code))[\s:#]*([A-Za-z0-9&., \t\xA0'-]{2,45}?)(?=\r?\n|$|\s{3,}|\t|EIN|FEIN|Box|\d+\b|Wages|Federal|Social|Medicare)/gi },
@@ -129,7 +131,11 @@ let REGEX_RULES = [
129
131
  // Contextual Last Names (Last name, Surname, Family name)
130
132
  { type: 'NAME', isContextName: true, regex: /(?:(?:Employee(?:'s)?|Borrower(?:'s)?|Co-Borrower(?:'s)?|Applicant(?:'s)?|Candidate(?:'s)?|Worker(?:'s)?|Taxpayer(?:'s)?|Spouse(?:'s)?|Person(?:'s)?)\s+)?(?:Last\s+name|Surname|Family\s+name)[\s:#]+(?:\b|\b\s*)([A-Za-z'-]{2,30})/gi },
131
133
  // Contextual General Names (Employee, Principal Investigator, Physician, Doctor, etc.)
132
- { type: 'NAME', isContextName: true, regex: /(?:(?:Employee|Employer|Borrower|Co-Borrower|Applicant|Candidate|Worker|Taxpayer|Spouse|Client|Customer|Patient|Claimant|Insured|Tenant|Landlord|Buyer|Seller|Plaintiff|Defendant|Testator|Author)\s+(?:Name|Full\s+Name)|(?:Principal\s+Investigator|Investigator|Physician|Doctor|Supervisor|Reporting\s+To|Prepared\s+By|Attention|Attn|Contact(?:\s+Person)?|Author)(?:\s+Name)?|(?:Employee|Employer|Borrower|Client|Customer|Patient|Claimant|Insured|Doctor|Attn|Signatory|Witness|Tenant|Landlord|Buyer|Seller|Plaintiff|Defendant|Author)\s*[:=#])[:\s#]*(?:\b|\b\s*)([A-Z\p{Lu}][A-Za-z0-9&.,\s'-]{1,35}?)(?=\r?\n|$|\s{3,}|\t|Employee|Employer|Address|Phone|SSN|EIN|FEIN|Date|Pay|Rate|Tax|W-2|OMB|Copy|Box|Status)/giu },
134
+ { type: 'NAME', isContextName: true, regex: /(?:(?:Employee|Employer|Borrower|Co-Borrower|Applicant|Candidate|Worker|Taxpayer|Spouse|Client|Customer|Patient|Claimant|Insured|Tenant|Landlord|Buyer|Seller|Plaintiff|Defendant|Testator|Author)\s+(?:Name|Full\s+Name)|(?:Principal\s+Investigator|Investigator|Physician|Doctor|Supervisor|Reporting\s+To|Prepared\s+By|Attention|Attn|(?<!Emergency\s+|ICE\s+)Contact(?:\s+Person)?|Author)(?:\s+Name)?|(?:Employee|Employer|Borrower|Client|Customer|Patient|Claimant|Insured|Doctor|Attn|Signatory|Witness|Tenant|Landlord|Buyer|Seller|Plaintiff|Defendant|Author)\s*[:=#])[:\s#|]*(?:\b|\b\s*)([A-Z\p{Lu}][A-Za-z0-9&.,\s'"“”()-]{1,35}?)(?=\r?\n|$|\s{3,}|\t|Employee|Employer|Address|Phone|SSN|EIN|FEIN|Date|Pay|Rate|Tax|W-2|OMB|Copy|Box|Status|\|)/giu },
135
+ // Contextual Nicknames & Preferred Names (e.g. "Nickname: Judy", "Preferred Name: Alex", "AKA: Bob", "Goes by: Sam")
136
+ { type: 'NAME', isContextName: true, regex: /(?:(?:Patient(?:'s)?|Client(?:'s)?|Child(?:'s)?|Employee(?:'s)?|Person(?:'s)?)\s+)?(?:Nickname|Nick\s+Name|Preferred\s+(?:First\s+)?Name|Also\s+Known\s+As|AKA|Alias|Goes\s+by)[\s:#]+(?:\b|\b\s*)([A-Z\p{Lu}][A-Za-z0-9&.,\s'-]{1,30}?)(?=\r?\n|$|\s{3,}|\t|DOB|Date|Phone|Address|Emergency|Relationship|Notes|MRN|\()/giu },
137
+ // Emergency Contacts (Standard, Inverted Last First, Single Name, Table Cells, Extended Relationships, Brackets & Phone Lookahead)
138
+ { type: 'NAME', isContextName: true, regex: /(?:(?:^|[^\p{L}\p{N}_]|\|)\s*(?:In\s+Case\s+of\s+Emergency(?:\s*\(ICE\))?|ICE(?:[ \t\xA0]*(?:Contact|Notification|Person|Name))?|Emergency(?:[ \t\xA0]*(?:Contact|Notification|Person|Info|Details))?|Primary\s+Emergency\s+Contact|Secondary\s+Emergency\s+Contact|Alternate\s+Emergency\s+Contact|Person\s+to\s+notify\s+(?:in\s+case\s+of\s+emergency|in\s+emergency))(?:[ \t\xA0]*(?:[12#]|#\s*\d|\(\d\)))?(?:[ \t\xA0]*(?:Person|Name|Full\s+Name))?(?:[ \t\xA0]*(?:[\/&+-]|and)[ \t\xA0]*(?:Relationship|Relation|Proxy|Guarantor|Guardian))?(?:[ \t\xA0]*\((?:Primary|Secondary|Name|Relationship|Relation|Proxy|Guarantor|Legal\s+Guardian|Full\s+Name)\))?)[:=#|\/\t\s-]*[:=#|\/\t-][ \t\xA0]*([A-Z\p{Lu}][A-Za-z0-9&.'’]*(?:[ \t\xA0]+[A-Za-z0-9&.'’]+)*(?:,[ \t\xA0]*(?!(?:Relationship|Relation|Rel|Mother|Father|Spouse|Wife|Husband|Partner|Son|Daughter|Child|Parent|Brother|Sister|Sibling|Friend|Aunt|Uncle|Cousin|Guardian|Caregiver|Carer|Neighbor|Relative|Family|Grandmother|Grandfather|Grandparent|Grandson|Granddaughter|Stepmother|Stepfather|Stepson|Stepdaughter|Step-mother|Step-father|In-law|Mother-in-law|Father-in-law|Sister-in-law|Brother-in-law|Son-in-law|Daughter-in-law|Fiancé|Fiance|Fiancee|Significant\s+Other|Domestic\s+Partner|Life\s+Partner|Roommate|Colleague|Coworker|Co-worker|Legal\s+Guardian|Case\s+Worker|Caseworker|Social\s+Worker|Foster\s+Parent|POA|Power\s+of\s+Attorney|Healthcare\s+Proxy|Medical\s+Proxy|Proxy|Next\s+of\s+Kin|Aide)\b)[A-Z\p{Lu}][A-Za-z0-9&.'’]*(?:[ \t\xA0]+[A-Za-z0-9&.'’]+)*)?)(?=[ \t\xA0]*(?:\([A-Za-z0-9\s\/&':.+–-]+\)|\[[A-Za-z0-9\s\/&':.+–-]+\]|\{[A-Za-z0-9\s\/&':.+–-]+\}|[,\/|\-–—]{1,3}\s*(?:Relationship|Relation|Rel|Mother|Father|Spouse|Wife|Husband|Partner|Son|Daughter|Child|Parent|Brother|Sister|Sibling|Friend|Aunt|Uncle|Cousin|Guardian|Caregiver|Carer|Neighbor|Relative|Family|Grandmother|Grandfather|Grandparent|Grandson|Granddaughter|Stepmother|Stepfather|Stepson|Stepdaughter|Step-mother|Step-father|In-law|Mother-in-law|Father-in-law|Sister-in-law|Brother-in-law|Son-in-law|Daughter-in-law|Fiancé|Fiance|Fiancee|Significant\s+Other|Domestic\s+Partner|Life\s+Partner|Roommate|Colleague|Coworker|Co-worker|Legal\s+Guardian|Case\s+Worker|Caseworker|Social\s+Worker|Foster\s+Parent|POA|Power\s+of\s+Attorney|Healthcare\s+Proxy|Medical\s+Proxy|Proxy|Next\s+of\s+Kin|Aide)\b|[,\/|\-–—]?[ \t\xA0]*(?:Phone|Tel|Cell|Mobile|Work|Home|Office|Direct|Telephone)[:\s#-]*|[,\/|\-–—]?[ \t\xA0]*(?:Relationship|Relation|Rel)[:\s#-]*|[,\/|\-–—]?[ \t\xA0]*(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b|[,\/|\-–—]?[ \t\xA0]*\d{3}[-.\s]?\d{4}\b|[ \t\xA0]*(?:\||\r?\n|$|\s{2,}|\t)))/gui },
133
139
  // Box e shorthand
134
140
  { type: 'NAME', isContextName: true, regex: /\b(?:Box\s+e)\s*[:#-]\s*([A-Za-z0-9&.,\s'-]{2,40})/gi }
135
141
  ];
@@ -137,17 +143,29 @@ let REGEX_RULES = [
137
143
  let PROFILE_RULES = {
138
144
  general: [],
139
145
  legal: [
146
+ // Legal Party & Role-Prefixed Names (Claimant, Respondent, Plaintiff, Defendant, Petitioner, Appellant, Principal, Grantor, Attorney-in-Fact, Counsel)
147
+ { type: 'NAME', isContextName: true, regex: /\b(?:Registered\s+Representative|Claimant|Respondent|Plaintiff|Defendant|Petitioner|Appellant|Appellee|Principal|Grantor|Grantee|Trustee|Trustor|Fiduciary|Attorney-in-Fact|Designated\s+Attorney-in-Fact|Client\s*(?:\/\s*Principal)?|Lead\s+Counsel|Counsel|Managing\s+Partner|Escrow\s+(?:Trust\s+)?Officer)\s*[:#,]?\s*(?:(?:Dr|Mr|Mrs|Ms|Prof)\.?\s+)?([A-Z][a-zA-Z'’-]+(?:-[A-Z][a-zA-Z'’-]+)?(?:,\s+[A-Z][a-zA-Z'’-]+(?:\s+[A-Z]\.?)?|\s+(?:[A-Z]\.?\s+)?[A-Z][a-zA-Z'’-]+(?:-[A-Z][a-zA-Z'’-]+)?))/gi },
140
148
  { type: 'LEGAL', regex: /\bCASE[-_][A-Z0-9_-]{4,}\b/gi },
141
149
  { type: 'LEGAL', regex: /\bMATTER[-_][A-Z0-9_-]{4,}\b/gi },
142
150
  { type: 'LEGAL', regex: /\b[A-Z]{2,4}[- ]?\d{2}[- ]?\d{4,}\b/g },
143
- { type: 'PRIVILEGE', regex: /ATTORNEY[- ]CLIENT[- ]PRIVILEGE/gi },
151
+ { type: 'PRIVILEGE', regex: /\bATTORNEY[- ]CLIENT[- ]PRIVILEGE[DS]?\b/gi },
144
152
  { type: 'LEGAL', regex: /\b(?:BATES|PROD|PLTF|DEFT|CONF|EXHIBIT)[-_ ]?[A-Z0-9]{2,10}[-_ ]?\d{4,10}\b/gi },
145
- { type: 'LEGAL', regex: /\b(?:\d{1,2}:)?\d{2,4}[-_ ]?(?:CV|CR|MDL|BK|MC|MISC|MJ|PO|CA|AP|PR)[-_ ][A-Z0-9_-]{3,15}\b/gi },
146
- { type: 'ID', regex: /\b(?:STATE\s+BAR|BAR\s+(?:NO\.?|#|ID)|SBN|ATTORNEY\s+REG(?:ISTRATION)?)[:\s#-]*\d{4,8}\b/gi },
153
+ // Case / Proceeding / Arbitration Numbers (including OCR homoglyphs O/0, I/1, l/1)
154
+ { type: 'LEGAL', regex: /\b(?:Case|Matter|Arbitration|Docket|Proceeding)\s*(?:(?:No\.?|Number|#)[:\s#]*|[:#]\s*)([A-Za-z0-9_-]{4,25})\b/gi },
155
+ { type: 'LEGAL', regex: /\b(?:\d{1,2}:)?\d{2,4}[-_ ]?(?:CV|CR|MDL|BK|MC|MISC|MJ|PO|CA|AP|PR|AR)[-_ ][A-Za-z0-9_-]{3,15}\b/gi },
156
+ // Bar registration & CRD / Broker / Representative IDs (including OCR homoglyphs)
157
+ { type: 'ID', regex: /\b(?:STATE\s+BAR|BAR\s+(?:NO\.?|#|ID)|SBN|ATTORNEY\s+REG(?:ISTRATION)?)[:\s#-]*([0-9OIloA-Za-z]{4,10})\b/gi },
158
+ { type: 'ID', regex: /\b(?:CRD|CIK|Broker\s*ID|Rep(?:resentative)?\s*ID|Advisor\s*ID|Registration\s*(?:No\.?|#)?)[:\s#]*([0-9OIloA-Za-z]{5,12})\b/gi },
147
159
  { type: 'LEGAL', regex: /\b(?:RETAINER|ENGAGEMENT|REPRESENTATION)[-_: ]*(?:AGREEMENT|ID|REF)?[:\s#-]*[A-Z0-9_-]{4,20}\b/gi },
148
160
  { type: 'LEGAL', regex: /\bECLI:[A-Z]{2}:[A-Za-z0-9]{1,7}:\d{4}:[A-Za-z0-9.]{1,25}\b/g },
149
161
  { type: 'ID', regex: /\bPCT\/[A-Z]{2}\d{4}\/\d{6}\b/g },
150
- { type: 'ID', regex: /\b(?:TX|TXu|VA|VAu|PA|PAu|SR|SRu|SE|SEu|RE)\s*-?\d[-\d ]{5,9}\d\b/g }
162
+ { type: 'ID', regex: /\b(?:TX|TXu|VA|VAu|PA|PAu|SR|SRu|SE|SEu|RE)\s*-?\d[-\d ]{5,9}\d\b/g },
163
+ // Escrow / Trust / Settlement / Disputed Account Numbers & ABA Routing
164
+ { type: 'FINANCIAL', regex: /\b(?:Escrow\s+(?:Account|Acct|Deposit)|Trust\s+Account|Settlement\s+Account|Disputed\s+Account)\s*(?:Number|Num|No\.?|#)?[:\s#]+([A-Za-z0-9-]{5,25})\b/gi },
165
+ { type: 'FINANCIAL', regex: /\b(?:ABA\s+Routing|Routing\s+Transit|Fedwire\s+Routing)\s*(?:Number|Num|No\.?|#)?[:\s#]+(\d{9})\b/gi },
166
+ // Legal Financial Amounts (lookbehind + standalone)
167
+ { type: 'FINANCIAL', regex: /(?<=\b(?:Settlement(?:\s+Amount)?|Retainer(?:\s+Fee)?|Hourly\s+Rate|Damages(?:\s+Claimed)?|Legal\s+Fees?|Sanctions?|Judgement|Judgment|Disgorgement|Penalty|(?:Prejudgment\s+)?Interest|Escrow(?:\s+Funds?|\s+Amount)?|Fine|Restitution)(?:[\s:]+(?:of)?[\s:]*|[:\s]+))(?:(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\s*|[$€£¥₪₽₹]\s*)[0-9,.'’]*\d[KMB]?\b|\b\d[0-9,.'’]*\d?[KMB]?\s*(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\b|[$€£¥₪₽₹])|[0-9,.'’]{3,}\b)/gi },
168
+ { type: 'FINANCIAL', regex: /(?:(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\s*|[$€£¥₪₽₹]\s*)[0-9,.'’]*\d[KMB]?\b|\b\d[0-9,.'’]*\d?[KMB]?\s*(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\b|[$€£¥₪₽₹]))/gi }
151
169
  ],
152
170
  hr: [
153
171
  { type: 'NAME', isContextName: true, regex: /(?:Candidate|Applicant|Employee|Reporting To|Manager|Mentored by|Direct Report)[\s:]+([A-Z][a-z]*(?:\s+[A-Z][a-z]*)?)/g },
@@ -181,17 +199,26 @@ let PROFILE_RULES = {
181
199
  { type: 'ID', regex: /\b(?:FEIN|EIN|TIN|Tax\s*ID)[:\s#]*(\d{2}-\d{7})\b/gi },
182
200
  { type: 'ID', regex: /\b(?:CRD|CIK|Advisor\s*ID|Broker\s*ID)[:\s#]*(\d{5,10})\b/gi },
183
201
  { type: 'ID', regex: /\b(?:LEI|LEGAL[\s-]ENTITY[\s-]IDENTIFIER)[:\s#-]*[0-9A-Z]{20}\b/gi },
184
- { type: 'ID', regex: /\bBBG[0-9A-Z]{9}\b/g }
202
+ { type: 'ID', regex: /\bBBG[0-9A-Z]{9}\b/g },
203
+ { type: 'FINANCIAL', regex: /(?<=\b(?:Wire(?:\s+(?:Transfer|Amount))?|Transfer|Balance|Deposit|Withdrawal|Settlement|Principal|Interest|Remittance|Payout|Transaction(?:\s+Amount)?|Payment(?:\s+Amount)?|Total|Amount)[:\s]+)(?:(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\s*|[$€£¥₪₽₹]\s*)[0-9,.'’]*\d[KMB]?\b|\b\d[0-9,.'’]*\d?[KMB]?\s*(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\b|[$€£¥₪₽₹])|[0-9,.'’]{3,}\b)/gi }
185
204
  ],
186
205
  medical: [
187
- { type: 'PHI', regex: /\b(?:MRN|Patient ID|Medical Record (?:No\.?|Num(?:ber)?|#)|Patient (?:No\.?|Num(?:ber)?|#))[\s:#]+([A-Za-z0-9-]+)/gi },
206
+ { type: 'NAME', isContextName: true, regex: /\b(?:PATIENT|Pt\.?|LEGAL\s+GUARDIAN(?:\/PROXY)?|PROXY|EMERGENCY\s+CONTACT|ATTENDING\s+PHYSICIAN|ATTENDING|SURGEON|PHYSICIAN|PROVIDER|SUBSCRIBER|GUARDIAN)\s*[:#|]?\s*(?!\b(?:NPI|MRN|DOB|ID|DATE|RECORD|PHONE)\b)(?:(?:Dr|Mr|Mrs|Ms|Prof)\.?\s+)?([A-Z][a-zA-Z'’-]+(?:-[A-Z][a-zA-Z'’-]+)?(?:,\s+[A-Z][a-zA-Z'’-]+(?:\s+[A-Z]\.?)?|\s+(?:[A-Z]\.?\s+)?[A-Z][a-zA-Z'’-]+(?:-[A-Z][a-zA-Z'’-]+)?))/gi },
207
+ // HL7 v2 PID-5 Caret-Delimited Patient / Provider Names (e.g. "Guillermo^Maricela^C", "Vance-Soto^Eleanor^M")
208
+ { type: 'NAME', isContextName: true, isHL7Name: true, regex: /\b[A-Za-z'’-]{2,}\^[A-Za-z'’-]{2,}(?:\^[A-Za-z'’.-]+){0,4}\b/g },
209
+ // HL7 v2 Segment Delimited Dates (YYYYMMDD in PID-7, MSH-7, etc. e.g. "||19820412|F")
210
+ { type: 'DATE', regex: /(?<=[|:])(?:19|20)\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])(?=[|:\s])/g },
211
+ { type: 'PHI', regex: /\b(?:MRN|Patient\s+(?:ID|Number|No\.?|#)|Medical\s+Record\s+(?:No\.?|Num(?:ber)?|#)|EHR(?:\s*(?:ID|Record|Number|No\.?|#))?)[\s:#.]+([A-Za-z0-9\/-]+)/gi },
188
212
  { type: 'DATE', regex: /\b(?:DOB|Date of Birth|BIRTHDAY)[\s:]+([0-9./-]{6,10})\b/gi },
189
- { type: 'PHI', regex: /\bMRN[-_ ]*[A-Za-z0-9_-]{4,}\b/gi },
213
+ { type: 'PHI', regex: /\b(?:MRN|EHR)[-_ .]*[A-Za-z0-9\/-]{4,}\b/gi },
190
214
  { type: 'ID', regex: /\b(?:Insurance\s+(?:ID|No\.?|Number|#)|Policy(?:\s*(?:ID|No\.?|Number|#)|[:#])|Member\s*(?:ID|No\.?|Number|#|[:#])|Subscriber\s*(?:ID|No\.?|Number|#|[:#])|Group\s*(?:ID|No\.?|Number|#|[:#])|Plan\s*(?:ID|No\.?|Number|#|[:#])|Health(?:\s+Plan)?\s*(?:ID|No\.?|Number|#)|Rx\s*(?:ID|No\.?|Number|Group|BIN|PCN|#))[:\s#]+([A-Za-z0-9-]{3,30})\b/gi },
191
215
  { type: 'ID', regex: /\b(?:Health\s+Plan(?:\s+Beneficiary)?|Beneficiary(?:\s+No\.?|\s+Number)?|HPN)[:\s#]+([A-Za-z0-9-]+)/gi },
192
216
  { type: 'ID', regex: /\bHPN[-_][A-Za-z0-9-]+\b/gi },
193
217
  { type: 'ID', regex: /\b(?:BCB|BCBS|AETNA|CIGNA|UHC|HUMANA|MEDICARE|MEDICAID)[-_A-Za-z0-9]+\b/gi },
194
- { type: 'ID', regex: /\b(?:NPI|National Provider Identifier)[:\s#]*(\d{10})\b/gi },
218
+ { type: 'ID', regex: /\b(?:NPI|National\s+Provider\s+(?:Identifier|ID)|Provider\s+(?:NPI|ID)|Attending\s+(?:NPI|Provider\s+ID)|Physician\s+NPI)[:\s#.]*([0-9OIlo]{10})\b/gi },
219
+ { type: 'ID', regex: /\(01\)\d{14}(?:\(\d{2}\)[A-Za-z0-9-]+)+/g },
220
+ { type: 'LOCATION', regex: /\b(?:Pavilion|Ward|Bed|Room|Rm\.?|Suite|Bldg|Building|Floor|Wing|Tower|Unit)\s+[A-Za-z0-9-]+\b/gi },
221
+ { type: 'DATE', regex: /\b(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember))\s+\d{1,2}(?:st|nd|rd|th)?,?\s+\d{4}\b/gi },
195
222
  { type: 'ID', regex: /\b(?:Device\s+(?:Identifier|ID|Serial|No\.?|Number)|UDI)[:\s#]+([A-Za-z0-9-]+)/gi },
196
223
  { type: 'ID', regex: /\bUDI[-_][A-Za-z0-9-]+\b/gi },
197
224
  { type: 'ID', regex: /\b(?:Vehicle\s+(?:Serial|ID|Identification(?:\s+Number)?|No\.?|Number)|VIN)[:\s#]+([A-Za-z0-9-]+)/gi },
@@ -285,7 +312,7 @@ let PROFILE_RULES = {
285
312
  { type: 'ID', regex: /\bPARCEL[- ]?\d{5,15}\b/gi },
286
313
  { type: 'ID', regex: /\b(?:APN|Assessor(?:'s)?\s*Parcel(?:\s*Number)?|Parcel\s*(?:ID|Number|No\.?|#))[:\s#-]*(\d{2,5}[-\s]\d{2,5}[-\s]\d{2,5}(?:[-\s]\d{1,4})?|\d{6,15})\b/gi },
287
314
  { type: 'ID', regex: /\bTENANT[-_]ID[-_][0-9]{4,}\b/gi },
288
- { type: 'FINANCIAL', regex: /(?<=\b(?:RENT|LEASE|ESCROW)[\s:]+)(?:[$€£¥₪₽₹]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[0-9,.'’]{3,}\b/gi },
315
+ { type: 'FINANCIAL', regex: /(?<=\b(?:RENT|LEASE|ESCROW|Purchase\s+Price|Listing\s+Price|Asking\s+Price|Down\s+Payment|Mortgage(?:\s+Balance)?|Closing\s+Costs?|Appraisal(?:\s+Value)?)[\s:]+)(?:(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\s*|[$€£¥₪₽₹]\s*)[0-9,.'’]*\d[KMB]?\b|\b\d[0-9,.'’]*\d?[KMB]?\s*(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\b|[$€£¥₪₽₹])|[0-9,.'’]{3,}\b)/gi },
289
316
  { type: 'SECRET', regex: /\b(?:GATE|DOOR|LOBBY)[-_ ](?:CODE|PIN)[\s:]*\d{4,6}\b/gi },
290
317
  { type: 'ID', regex: /\b(?:Escrow\s*(?:Number|No\.?|#|Account|File|Order)|Escrow)[:\s#]*([A-Za-z0-9-]{5,18})\b/gi },
291
318
  { type: 'ID', regex: /\b(?:Title\s*Policy|Title\s*Order|Title\s*Commitment)\s*(?:#|ID|No\.?)?[:\s#]*([A-Z0-9-]{6,20})\b/gi },
@@ -396,7 +423,7 @@ let PROFILE_RULES = {
396
423
  { type: 'FINANCIAL', regex: /\b(?:Policy|Contract)\s*(?:No\.?|Number|#)[:\s]+[A-Z0-9][A-Z0-9\-]{3,14}\b/gi },
397
424
  { type: 'ID', regex: /\bCRD\s*#?\s*\d{4,8}\b/gi },
398
425
  { type: 'FINANCIAL', regex: /\b(?:Annual\s+(?:Distribution|Withdrawal)|RMD|Required\s+Minimum\s+Distribution)[:\s]+(?:[$€£¥₪₽₹]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.'’]+\b/gi },
399
- { type: 'FINANCIAL', regex: /\b(?:Portfolio|Market\s+Value|Net\s+Worth|AUM|Total\s+Assets)[:\s]+(?:[$€£¥₪₽₹]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.'’]+[KMB]?\b/gi },
426
+ { type: 'FINANCIAL', regex: /(?<=\b(?:Portfolio(?:\s+Value)?|Market\s+Value|Net\s+Worth|AUM|Total\s+Assets)[:\s]+)(?:(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\s*|[$€£¥₪₽₹]\s*)[0-9,.'’]*\d[KMB]?\b|\b\d[0-9,.'’]*\d?[KMB]?\s*(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\b|[$€£¥₪₽₹])|[0-9,.'’]{3,}\b)/gi },
400
427
  { type: 'FINANCIAL', regex: /\b(?:Roth\s+)?(?:IRA|401k|401\(k\)|403b|403\(b\)|SEP|SIMPLE)\s*(?:Account|Acct|Plan)?\s*(?:No|Number|#)?[:\s#]*[A-Z0-9]{4,15}\b/gi },
401
428
  { type: 'ID', regex: /\bCUSIP[:\s#]*([0-9A-Z]{9})\b/gi },
402
429
  { type: 'ID', regex: /\bISIN[:\s#]*([A-Z]{2}[0-9A-Z]{9}[0-9])\b/gi },
@@ -409,7 +436,7 @@ let PROFILE_RULES = {
409
436
  { type: 'FINANCIAL', regex: /\b(?:Loss|Claim\s+Amount|Claim\s+Settlement|Settlement|Reserve|Indemnity)[:\s]+(?:[$€£¥₪₽₹]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.'’]+\b/gi },
410
437
  { type: 'ID', regex: /\b(?:(?:Assigned\s+)?Adjuster\s+(?:ID|No)|Claim\s+Rep(?:\.|resentative)?)[:\s#]*[-A-Z0-9]{4,15}\b/gi },
411
438
  { type: 'ID', regex: /\bNAIC(?:\s+(?:Company\s+)?(?:Code|CoCode|No\.?|Number|#))?[:\s#]*(\d{5})\b/gi },
412
- { type: 'FINANCIAL', regex: /\b(?:Deductible|Premium|Coverage\s+Amount)[:\s]+(?:[$€£¥₪₽₹]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.'']+\b/gi },
439
+ { type: 'FINANCIAL', regex: /\b(?:Deductible|Premium|Coverage\s+Amount)[:\s]+(?:[$€£¥₪₽₹]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.'’\u2018\u2019]+\b/gi },
413
440
  { type: 'ID', regex: /\b[A-HJ-NPR-Z0-9]{17}\b/g },
414
441
  { type: 'ID', regex: /\b(?:Insured|Named\s+Insured)[:\s]+[A-Z][a-z]+(?:\s+[A-Z][a-z]+)+/g },
415
442
  { type: 'ID', regex: /\b(?:Agent|Producer)\s*(?:Code|No|ID)[:\s#]*[-A-Z0-9]{4,15}\b/gi },
@@ -421,15 +448,17 @@ let PROFILE_RULES = {
421
448
  accounting: [
422
449
  { type: 'ID', regex: /\b(?:EIN|FEIN|Tax\s+ID)[:\s#]*\d{2}-\d{7}\b/gi },
423
450
  { type: 'FINANCIAL', regex: /\b(?:AGI|Adjusted\s+Gross\s+Income|Taxable\s+Income|Total\s+Income)[:\s]+(?:[$€£¥₪₽₹]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.'’]+\b/gi },
424
- { type: 'FINANCIAL', regex: /\b(?:Form\s+|Withheld\s+)?Box\s+\d{1,2}[a-z]?(?:\s*:\s*[A-Z])?\s*[:\s]+(?:[$\u20ac\u00a3\u00a5\u20aa\u20bd\u20b9]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.\u2018\u2019]+\b/gi },
451
+ { type: 'FINANCIAL', regex: /\b(?:Form\s+|Withheld\s+)?Box\s+\d{1,2}[a-z]?(?:\s*:\s*[A-Z])?\s*[:\s]+(?:[$€£¥₪₽₹]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.'’\u2018\u2019]+\b/gi },
425
452
  { type: 'ID', regex: /\b(?:Form|Schedule)\s+(?:1040|1040-SR|W-2|W-4|1099-[A-Z]{1,4}|K-1|941|990|4562)\b/gi },
426
- { type: 'FINANCIAL', regex: /\b(?:Federal|State|Assessed)?\s*(?:Tax\s+Due|Balance\s+Due|Overpayment|Refund)[:\s]+(?:[$\u20ac\u00a3\u00a5\u20aa\u20bd\u20b9]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.\u2018\u2019]+\b/gi },
453
+ { type: 'FINANCIAL', regex: /\b(?:Federal|State|Assessed)?\s*(?:Tax\s+Due|Balance\s+Due|Overpayment|Refund)[:\s]+(?:[$€£¥₪₽₹]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.'’\u2018\u2019]+\b/gi },
427
454
  { type: 'ID', regex: /\b(?:State\s+Tax\s+ID|SUI|UI\s+Account\s+No)[:\s#]*[A-Z]{0,3}[-]?\d{4,15}\b/gi },
428
- { type: 'FINANCIAL', regex: /\b(?:Gross|Net|Medicare|Social\s+Security)\s+(?:Pay|Wages|Tips|Withholding)[:\s]+(?:[$\u20ac\u00a3\u00a5\u20aa\u20bd\u20b9]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.\u2018\u2019]+\b/gi },
455
+ { type: 'FINANCIAL', regex: /\b(?:Gross|Net|Medicare|Social\s+Security)\s+(?:Pay|Wages|Tips|Withholding)[:\s]+(?:[$€£¥₪₽₹]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.'’\u2018\u2019]+\b/gi },
429
456
  { type: 'ID', regex: /\b(?:CAF(?:\s+ID)?|(?:Practitioner|Tax\s+Preparer)\s+(?:PIN|ID)|PTIN)[:\s#]*[A-Z0-9-]{6,12}\b/gi },
430
457
  { type: 'ID', regex: /\bEFIN[:\s#]*\d{6}\b/gi },
431
458
  { type: 'ID', regex: /\bGL[-_\s]*(?:Account|Acct|Code)?[:\s#]*\d{4,10}\b/gi },
432
- { type: 'ID', regex: /\b(?:Cost\s+Center|CC)[-_\s]*(?:ID|Code|#)?[:\s#]*[A-Z0-9_-]*\d[A-Z0-9_-]*\b/gi }
459
+ { type: 'ID', regex: /\b(?:Cost\s+Center|CC)[-_\s]*(?:ID|Code|#)?[:\s#]*[A-Z0-9_-]*\d[A-Z0-9_-]*\b/gi },
460
+ { type: 'ID', regex: /\b(?:Invoice|Inv|Purchase\s+Order|PO|Bill|Check|Voucher|Remittance)\s*(?:Number|Num|No\.?|#)?[:\s#]+([A-Za-z0-9_-]{4,25})\b/gi },
461
+ { type: 'FINANCIAL', regex: /(?<=\b(?:Invoice(?:\s+Total)?|Amount\s+Due|Subtotal|Total\s+Due|Payment(?:\s+Amount)?|Ledger\s+Balance|Credit|Debit|Net\s+Amount|Total\s+Paid|Billed|Remittance)[:\s]+)(?:(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\s*|[$€£¥₪₽₹]\s*)[0-9,.'’]*\d[KMB]?\b|\b\d[0-9,.'’]*\d?[KMB]?\s*(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\b|[$€£¥₪₽₹])|[0-9,.'’]{3,}\b)/gi }
433
462
  ],
434
463
  pharma: [
435
464
  { type: 'ID', regex: /\b(?:Subject|Patient|Participant)\s+(?:ID|No)[:\s#]*[-A-Z0-9]{3,15}\b/gi },
@@ -588,7 +617,7 @@ const CORPORATE_SUFFIXES = new Set([
588
617
  let NOT_NAME_WORDS = new Set([
589
618
  'uk', 'eu', 'us', 'usa', 'dpa', 'ico', 'dpo', 'ciso', 'ssrn', 'elsevier', 'gdpr', 'ccpa', 'cpra', 'hipaa', 'soc2', 'iso27001', 'governance', 'regulatory', 'framework', 'mandate', 'mandates', 'guidance', 'statute', 'statutes', 'jurisdiction', 'jurisdictions', 'recital', 'article', 'articles', 'treatise', 'workstation', 'device', 'endpoint', 'blueprint', 'personal', 'cross-border', 'cross', 'border', 'transfer', 'retrieval', 'augmented', 'generation', 'limitation', 'discrimination', 'imposes', 'heavy', 'identifiable', 'bypasses', 'author', 'authors', 'contact', 'contacts', 'keywords', 'keyword', 'keys', 'key', 'separation', 'pre-print', 'preprint', 'abstract', 'structured', 'background', 'methods', 'results', 'discussion', 'conclusion', 'conclusions', 'introduction', 'overview', 'workflow', 'paradigm', 'nomenclature', 'repository', 'classification', 'phenotypes', 'biomedical', 'bioinformatics', 'identifiers', 'de-identification', 'sanitization', 'ephemeral', 'validation', 'validating', 'genomic', 'genomics', 'clinical', 'phenotype', 'target', 'corresponding', 'subject', 'biorxiv', 'medrxiv', 'arxiv', 'privacyscrubber', 'research', 'lead', 'architect', 'chief', 'founder', 'symbols', 'gene', 'genes', 'sample', 'barcode', 'pipeline', 'downstream', 'pedigree', 'trees', 'exposes', 'violates', 'protecting', 'anonymization', 'redacted', 'redaction', 'zero-trust', 'ztds',
590
619
  'conventional', 'model', 'models', 'potential', 'multi', 'million', 'dollar', 'dollars', 'surrogate', 'surrogates', 'geographic', 'subdivisions', 'subdivision', 'inference', 'response', 'responses', 'latency', 'benchmarks', 'benchmark', 'experimental', 'methodology', 'informatics', 'ethics', 'accountability', 'civil', 'rights', 'perimeter', 'network', 'requirements', 'requirement', 'statutory', 'categories', 'category', 'regulations', 'regulation', 'penalties', 'penalty', 'disclosure', 'disclosures', 'clinician', 'terminal', 'transport', 'inpatient', 'discharge', 'summaries', 'operative', 'reports', 'outpatient', 'psychiatric', 'telehealth', 'consultation', 'pathology', 'sequencing', 'laboratory', 'sensitivity', 'specificity', 'proxy', 'gateways', 'gateway', 'packet', 'packets', 'inspection', 'declarations', 'availability', 'competing', 'interests', 'interventions', 'multicenter', 'multi-center', 'institution', 'institutional', 'protocol', 'deviations', 'adverse', 'events', 'abbreviations', 'agents', 'agent', 'committee', 'committees', 'board', 'boards', 'nih', 'pubmed', 'central', 'open', 'access', 'policy', 'trial', 'trials', 'crf', 'crfs', 'ehr', 'ehrs', 'baa', 'baas', 'hitech', 'safe', 'harbor', 'section',
591
- 'retirement', 'roth', 'ira', 'acct', 'custodial', 'brokerage', 'portfolio', 'annuity', 'dividend', 'distribution', 'reconciliation', 'withholding', 'remuneration', 'pin', 'lobby', 'passcode', 'code', 'gate', 'door', 'access', 'tenant', 'landlord', 'escrow', 'cadastral', 'claim', 'claims', 'adjuster', 'insured', 'carrier', 'indemnity', 'underwriter', 'underwriting', 'deductible', 'premium', 'settlement', 'loss', 'cargo', 'writers', 'guild', 'ghostwriter', 'actor', 'screenplay', 'audiovisual', 'cinematic', 'co-production', 'alumnus', 'alumni', 'registrar', 'transcript', 'diploma', 'orchestration', 'runtime', 'helpdesk', 'freshdesk', 'intercom', 'zendesk', 'investigational', 'pharmacovigilance', 'reaction', 'medication', 'manufacturing', 'lot', 'industrial', 'electronics', 'driver', 'drivers', 'license', 'licence', 'clearing', 'house', 'automated', 'central', 'securities', 'depository', 'arbitration', 'tribunal', 'instrument', 'infrastructure', 'talent', 'dossier',
620
+ 'retirement', 'roth', 'ira', 'acct', 'custodial', 'brokerage', 'portfolio', 'annuity', 'dividend', 'distribution', 'reconciliation', 'withholding', 'remuneration', 'pin', 'lobby', 'passcode', 'code', 'gate', 'door', 'access', 'tenant', 'landlord', 'escrow', 'cadastral', 'claim', 'claims', 'adjuster', 'insured', 'carrier', 'indemnity', 'underwriter', 'underwriting', 'deductible', 'premium', 'settlement', 'loss', 'cargo', 'writers', 'guild', 'ghostwriter', 'actor', 'screenplay', 'audiovisual', 'cinematic', 'co-production', 'alumnus', 'alumni', 'registrar', 'transcript', 'diploma', 'orchestration', 'runtime', 'helpdesk', 'freshdesk', 'intercom', 'zendesk', 'investigational', 'pharmacovigilance', 'reaction', 'medication', 'manufacturing', 'lot', 'industrial', 'electronics', 'driver', 'drivers', 'license', 'licence', 'clearing', 'house', 'automated', 'central', 'securities', 'depository', 'arbitration', 'tribunal', 'instrument', 'infrastructure', 'talent', 'dossier', 'tel', 'phone', 'fax', 'mobile', 'cell', 'email', 'dpoa', 'poa',
592
621
  // Grammatical & Sentence Starters
593
622
  'the', 'a', 'an', 'this', 'that', 'these', 'those', 'my', 'your', 'his', 'her', 'their', 'our', 'its', 'it', 'he', 'she', 'they', 'we', 'i', 'you', 'who', 'whom', 'which', 'what', 'whose', 'why', 'how', 'when', 'where', 'with', 'for', 'from', 'by', 'to', 'at', 'in', 'on', 'of', 'about', 'as', 'into', 'through', 'during', 'before', 'after', 'above', 'below', 'and', 'but', 'or', 'so', 'yet', 'im', "i'm", "you're", "they're", "we're", "it's", "he's", "she's", "that's", "there's", "what's", "who's", "i've", "you've", "we've", "they've", "i'll", "you'll", "we'll", "they'll", "i'd", "you'd", "we'd", "they'd",
594
623
  // Verbs, Auxiliaries, Commands & Imperatives
@@ -727,7 +756,7 @@ const PROFILE_JARGON = {
727
756
  'insurance', 'bcbs', 'bronchitis', 'amoxicillin', 'penicillin', 'antibiotic', 'antibiotics', 'vital',
728
757
  'vitals', 'bp', 'hr', 'bpm', 'mmhg', 'allergies', 'dosage', 'parenchyma', 'ventricles', 'fazekas',
729
758
  'ischemia', 'lesion', 'cardiology', 'atherosclerotic', 'aortocoronary', 'bypass', 'graft', 'hyperlipidemia',
730
- 'diverticulitis', 'resection', 'colon', 'superbill', 'encounter', 'cpt', 'hcpcs', 'icd-10'
759
+ 'diverticulitis', 'resection', 'colon', 'superbill', 'encounter', 'cpt', 'hcpcs', 'icd-10', 'pt'
731
760
  ],
732
761
  realestate: [
733
762
  'escrow', 'tenant', 'landlord', 'lease', 'mortgage', 'appraisal', 'broker', 'property', 'zoning',
@@ -744,7 +773,13 @@ const PROFILE_JARGON = {
744
773
  'recitals', 'whereas', 'confidential', 'information', 'care', 'term', 'exhibit', 'party', 'parties',
745
774
  'disclosing', 'receiving', 'witnesseth', 'standard', 'indemnification', 'liability', 'governing',
746
775
  'law', 'docket', 'deponent', 'reporter', 'appearances', 'examination', 'services', 'scope', 'work',
747
- 'master', 'msa'
776
+ 'master', 'msa',
777
+ 'finra', 'sec', 'disciplinary', 'arbitration', 'proceeding', 'proceedings', 'dispute', 'resolution',
778
+ 'escrow', 'claimant', 'respondent', 'petitioner', 'appellant', 'appellee', 'trustee', 'trustor',
779
+ 'grantor', 'grantee', 'fiduciary', 'principal', 'agent', 'officer', 'counsel', 'lead', 'attorney',
780
+ 'partner', 'associate', 'representative', 'broker', 'primary', 'privilege', 'privileged', 'doctrine',
781
+ 'work-product', 'power', 'durable', 'evidence', 'rules', 'rule', 'statutory', 'disqualification',
782
+ 'acquisition', 'file', 'securities', 'exchange'
748
783
  ],
749
784
  academic: [
750
785
  'institutional', 'review', 'board', 'irb', 'protocol', 'national', 'institutes', 'health', 'nih',
@@ -829,7 +864,7 @@ const PROFILE_JARGON = {
829
864
  'underwriting': 'underwriting', 'lending': 'underwriting', 'mortgage': 'underwriting', 'loan': 'underwriting', 'income': 'underwriting', 'income_verification': 'underwriting', 'payroll': 'underwriting', 'w2': 'underwriting', 'paystub': 'underwriting',
830
865
  'medical': 'medical', 'healthcare': 'medical', 'health': 'medical', 'pharma': 'pharma', 'hipaa': 'medical',
831
866
  'engineering': 'engineering', 'dev': 'dev', 'api': 'dev', 'devops': 'engineering', 'tech': 'tech',
832
- 'finance': 'finance', 'bizops': 'bizops', 'sales': 'sales', 'wealthmgmt': 'wealthmgmt', 'wealth': 'wealthmgmt', 'insurance': 'insurance', 'accounting': 'accounting', 'pci': 'finance',
867
+ 'finance': 'finance', 'financial': 'finance', 'bizops': 'bizops', 'sales': 'sales', 'wealthmgmt': 'wealthmgmt', 'wealth': 'wealthmgmt', 'insurance': 'insurance', 'accounting': 'accounting', 'pci': 'finance',
833
868
  'legal': 'legal', 'compliance': 'compliance', 'ccpa': 'ccpa', 'gdpr': 'compliance', 'soc2': 'compliance', 'iso27001': 'compliance', 'nist': 'compliance', 'dpo': 'compliance', 'grc': 'compliance',
834
869
  'hr': 'hr', 'security': 'security', 'marketing': 'marketing', 'support': 'support',
835
870
  'realestate': 'realestate', 'academic': 'academic', 'agents': 'agents', 'ai_agents': 'agents', 'creative': 'creative', 'personal': 'personal'
@@ -847,16 +882,18 @@ const PROFILE_JARGON = {
847
882
  }
848
883
 
849
884
  function stitchOrphanedNameLines(text, profile) {
850
- if (profile === 'medical') {
851
- text = text.replace(/Patient Name:\s*\n+([A-Z][a-zA-Z]+\s[A-Z][a-zA-Z]+)/g, 'Patient Name: $1');
852
- } else if (profile === 'legal') {
853
- text = text.replace(/Defendant:\s*\n+([A-Z][a-zA-Z]+\s[A-Z][a-zA-Z]+)/g, 'Defendant: $1');
885
+ if (!text || !text.includes('\n')) return text;
886
+ const prof = (profile || 'general').toLowerCase();
887
+ if (prof === 'medical' || prof === 'general') {
888
+ text = text.replace(/(Patient\s+Name|Emergency\s+Contact|In\s+Case\s+of\s+Emergency|ICE(?:\s+Contact)?):\s*\r?\n+([A-Z\p{Lu}][A-Za-z0-9&.,'’ \t-]*?[A-Za-z0-9\p{Lu}\p{Ll}])(?=\r?\n|$)/gui, '$1: $2');
889
+ } else if (prof === 'legal') {
890
+ text = text.replace(/Defendant:\s*\r?\n+([A-Z][a-zA-Z]+\s[A-Z][a-zA-Z]+)/g, 'Defendant: $1');
854
891
  }
855
892
  return text;
856
893
  }
857
894
 
858
895
  function detectMatches(text, activeProfile = 'general', customRules = [], nlpNames = [], enabledEntities = null, mlEntities = []) {
859
- let textToProcess = text.replace(/[\u200b\u200c\u200d\ufeff]/g, '');
896
+ let textToProcess = stitchOrphanedNameLines(text, activeProfile).replace(/[\u200b\u200c\u200d\ufeff\u00ad]/gi, '');
860
897
  textToProcess = textToProcess.replace(/([a-z])(Email|Phone|Mobile|Tel|Address|IP|ID|URL|SSN|Date):/g, '$1 $2:');
861
898
 
862
899
  let matches = [];
@@ -934,9 +971,9 @@ const PROFILE_JARGON = {
934
971
  if (!val || NAME_STOP_LIST.has(val) || currentJargon.has(val) || NOT_NAME_WORDS.has(val)) continue;
935
972
 
936
973
  if (rule.type === 'NAME') {
937
- let words = val.split(/[ \t\xA0]+/);
974
+ let words = val.split(/[ \t\xA0\^]+/);
938
975
  if (!rule.isContextName) {
939
- let origWords = matchedText.split(/[ \t\xA0]+/);
976
+ let origWords = matchedText.split(/[ \t\xA0\^]+/);
940
977
  while (words.length > 2 && (currentJargon.has(words[0]) || (words[0].length > 1 && NOT_NAME_WORDS.has(words[0])) || (words[0].replace(/[^\p{L}]/gu, '').length > 1 && NOT_NAME_WORDS.has(words[0].replace(/[^\p{L}]/gu, ''))))) {
941
978
  origWords.shift();
942
979
  words.shift();
@@ -956,7 +993,7 @@ const PROFILE_JARGON = {
956
993
  if (cleanW.length <= 1) return false;
957
994
  return currentJargon.has(w) || NOT_NAME_WORDS.has(w) || NOT_NAME_WORDS.has(cleanW) || NAME_STOP_LIST.has(w) || NAME_STOP_LIST.has(cleanW);
958
995
  }) ||
959
- val.split(/[ \t\xA0-]+/).some(sw => {
996
+ val.split(/[ \t\xA0\^\-]+/).some(sw => {
960
997
  const cleanSW = sw.replace(/[^\p{L}]/gu, '');
961
998
  if (cleanSW.length <= 1) return false;
962
999
  return currentJargon.has(sw) || NOT_NAME_WORDS.has(sw) || NOT_NAME_WORDS.has(cleanSW) || NAME_STOP_LIST.has(sw) || NAME_STOP_LIST.has(cleanSW);
@@ -974,6 +1011,7 @@ const PROFILE_JARGON = {
974
1011
 
975
1012
  if (rule.isContextName && words.length > 0 && words.some(w => {
976
1013
  const cleanW = w.replace(/[^\p{L}]/gu, '');
1014
+ if (cleanW.length <= 1) return false;
977
1015
  if (rule.isCorporateName && (CORPORATE_SUFFIXES.has(cleanW) || US_STATES_SET.has(cleanW))) {
978
1016
  return false;
979
1017
  }
@@ -1168,7 +1206,12 @@ const PROFILE_JARGON = {
1168
1206
  cleaned = cleaned.replace(/^(?:Here (?:is|are) (?:the )?(?:redacted|scrubbed|sanitized|processed|clean|updated|modified) (?:text|output|version|data).*?[:\n]+|\*\*Scrubbed Text\*\*[:\n]+|### Scrubbed Text[:\n]+)/i, '');
1169
1207
  cleaned = cleaned.replace(/^\s*Edit\s*\n+/i, "");
1170
1208
  cleaned = cleaned.replace(/\s*\bEdit\s+in\s+a\s+page\b\s*$/i, "");
1171
- // 3. Strip stray leading colons, semicolons, or separators left by stripped icons/artifact headers
1209
+ // 3. Strip echoed system instructions and PrivacyScrubber pseudonymization disclaimers
1210
+ cleaned = cleaned.replace(/^\s*(?:Understood|Acknowledged|Got it|Sure|OK|Okay|Note taken|Understood[.,!])[^\n]*\n+/i, "");
1211
+ cleaned = cleaned.replace(/(?:^|\n*)\s*\[(?:Privacy Note|Context: identifiers|Privacy Scrubber Mode|SYSTEM INSTRUCTION: DATA PRIVACY MODE)[\s\S]*?exactly as formatted in your response\.?\]\s*(?:\n*|$)/gi, "\n\n");
1212
+ cleaned = cleaned.replace(/(?:^|\n*)\s*\[(?:Privacy Note|Context: identifiers|Privacy Scrubber Mode|SYSTEM INSTRUCTION: DATA PRIVACY MODE)[^\]]*\]\s*(?:\n*|$)/gi, "\n\n");
1213
+ cleaned = cleaned.replace(/(?:^|\n*)\s*(?:Direct identifiers in this prompt were pseudonymized|I will retain all bracketed tokens)[^\n]*(?:\n*|$)/gi, "\n\n");
1214
+ // 4. Strip stray leading colons, semicolons, or separators left by stripped icons/artifact headers
1172
1215
  cleaned = cleaned.replace(/^[:;|\-\—\–]+(?=\n|$)/, "");
1173
1216
  cleaned = cleaned.replace(/^[:;]+\s*/, "");
1174
1217
  return cleaned.trim();
package/scrubber-core.cjs CHANGED
@@ -38,10 +38,10 @@
38
38
  function stitchOrphanedNameLines(text, profile) {
39
39
  if (!text || !text.includes('\n')) return text;
40
40
  const prof = (profile || 'general').toLowerCase();
41
- if (prof === 'medical') {
42
- text = text.replace(/Patient Name:\s*\n+([A-Z][a-zA-Z]+\s[A-Z][a-zA-Z]+)/g, 'Patient Name: $1');
41
+ if (prof === 'medical' || prof === 'general') {
42
+ text = text.replace(/(Patient\s+Name|Emergency\s+Contact|In\s+Case\s+of\s+Emergency|ICE(?:\s+Contact)?):\s*\r?\n+([A-Z\p{Lu}][A-Za-z0-9&.,'’ \t-]*?[A-Za-z0-9\p{Lu}\p{Ll}])(?=\r?\n|$)/gui, '$1: $2');
43
43
  } else if (prof === 'legal') {
44
- text = text.replace(/Defendant:\s*\n+([A-Z][a-zA-Z]+\s[A-Z][a-zA-Z]+)/g, 'Defendant: $1');
44
+ text = text.replace(/Defendant:\s*\r?\n+([A-Z][a-zA-Z]+\s[A-Z][a-zA-Z]+)/g, 'Defendant: $1');
45
45
  }
46
46
  return text;
47
47
  }
@@ -66,7 +66,7 @@ function scrubText(text, customRules = [], tokenLabelMap = {}, profile = 'Genera
66
66
 
67
67
  // Protect system prompt from being scrubbed or counted
68
68
  let extractedSystemPrompt = "";
69
- let textToProcess = text.replace(/[\u200b\u200c\u200d\ufeff]/g, '');
69
+ let textToProcess = text.replace(/[\u200b\u200c\u200d\ufeff\u00ad]/gi, '');
70
70
 
71
71
  const isSpecialized = profile && profile.toLowerCase() !== 'general';
72
72
 
@@ -1116,7 +1116,7 @@ function hydrateRegex(rule) {
1116
1116
  }
1117
1117
  });
1118
1118
  if (remaskedCount > 0) {
1119
- if (typeof showInPageToast === 'function') showInPageToast(`🔒 Re-masked in DOM. Original tokens restored.`, "info");
1119
+ if (typeof showInPageToast === 'function') showInPageToast('Re-masked in DOM. Original tokens restored.', "info");
1120
1120
  document.querySelectorAll('.ps-toolbar-reveal, [id$="-reveal"]').forEach(b => {
1121
1121
  b.setAttribute('title', 'Reveal Original Data (Decrypted Locally)');
1122
1122
  b.classList.remove('ps-revealed-active');
@@ -1184,7 +1184,7 @@ function hydrateRegex(rule) {
1184
1184
  });
1185
1185
 
1186
1186
  if (restoredCount > 0) {
1187
- if (typeof showInPageToast === 'function') showInPageToast(`✅ Decrypted Locally! Your data is now safe to copy.`, "success");
1187
+ if (typeof showInPageToast === 'function') showInPageToast('✓ Decrypted Locally in RAM. Safe to copy.', "success");
1188
1188
  document.querySelectorAll('.ps-toolbar-reveal, [id$="-reveal"]').forEach(b => {
1189
1189
  b.setAttribute('title', 'Hide / Re-mask Protected Data in DOM');
1190
1190
  b.classList.add('ps-revealed-active');
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/moxno/privacyscrubber-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "2.1.0",
9
+ "version": "2.2.1",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@privacyscrubber/mcp-server",
14
- "version": "2.1.0",
14
+ "version": "2.2.1",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -26,4 +26,4 @@
26
26
  ]
27
27
  }
28
28
  ]
29
- }
29
+ }