schema_version = "0.1.0"
rulepack_id = "gaze-core-extended"
rulepack_version = "0.5.1"
default_locales = ["en-US", "de-DE", "de-AT", "de-CH", "global"]

[[recognizers]]
id = "phone.structural"
class = "custom:phone"
cooperates_with = ["phone.national.de", "phone.national.us"]
enabled = true
locales = ["global"]

[recognizers.match]
kind = "regex"
# Mirrors the national phone consuming boundary so phone.structural cannot
# re-open identifier-attached E.164 matches rejected by locale recognizers;
# cross-recognizer asymmetry caught in PR #126 review scratchpad 795.
pattern = '''(?:^|[^[:alnum:]_+])(\+\d{6,15})\b'''
capture_groups = [1]

[recognizers.validator]
kind = "e164_phone"

[recognizers.scoring]
base = 0.70
priority = 80

[recognizers.token]

[recognizers.collision]
family = "phone-or-imei"
variant = "phone"
precedence = 10

[[recognizers]]
id = "phone.national.de"
class = "custom:phone"
cooperates_with = ["phone.structural", "phone.national.us"]
enabled = true
locales = ["de-DE", "de-AT", "de-CH"]

[recognizers.match]
kind = "regex"
# Source: BNetzA Vorwahlverzeichnis (https://www.bundesnetzagentur.de/DE/Fachthemen/Telekommunikation/Nummerierung/ONRufnr/start.html), as-of 2026-04-29.
# BNetzA lists thousands of 2- to 5-digit Ortsnetzkennzahlen; this broad
# candidate shape delegates final validity to the E164PhoneNational(De)
# parser-backed validator while preserving identifier-attached rejection.
# Broad candidate scanning stays tenant-agnostic. Leading no-capture branches
# consume country-neutral 22-character IBAN grouping and short 4+5 numeric ID
# shapes before phone parsing sees phone-like substrings.
pattern = '''(?x)\b[A-Z]{2}\d{2}(?:\s?[A-Z0-9]{4}){4}\s?[A-Z0-9]{2}\b|(?:^|[^[:alnum:]_+])0[1-9]\d{2}[\s./-]*\d{5}\b|(?:^|[^[:alnum:]_+])((?:\+49[\s.-]*|0)[1-9]\d{1,4}[\s./-]*\d(?:[\s./-]*\d){4,10})\b'''
capture_groups = [1]

[recognizers.validator]
kind = "e164_phone_national_de"

[recognizers.scoring]
base = 0.82
priority = 85

[recognizers.token]

[recognizers.collision]
family = "phone-or-imei"
variant = "phone"
precedence = 10

[[recognizers]]
id = "phone.national.us"
class = "custom:phone"
cooperates_with = ["phone.structural", "phone.national.de"]
enabled = true
locales = ["en-US"]

[recognizers.match]
kind = "regex"
# Mirrors phone.national.de's consuming boundary so identifier-attached
# numbers reject consistently; see audit scratchpad 778.
pattern = '''(?x)(?:^|[^[:alnum:]_+])((?:\+?1[\s.-]+555[\s.-]*01\d{2}|\+?1[\s.-]+(?:\([2-9]\d{2}\)|[2-9]\d{2})[\s.-]*[2-9]\d{2}[\s.-]*\d{4}|\([2-9]\d{2}\)[\s.-]*[2-9]\d{2}[\s.-]*\d{4}|[2-9]\d{2}[\s.-]+[2-9]\d{2}[\s.-]+\d{4}|555[\s.-]*01\d{2}))\b'''
capture_groups = [1]

[recognizers.validator]
kind = "e164_phone_national_us"

[recognizers.scoring]
base = 0.82
priority = 85

[recognizers.token]

[recognizers.collision]
family = "phone-or-imei"
variant = "phone"
precedence = 10

[[recognizers]]
id = "iban.structural"
class = "custom:iban"
enabled = true
locales = ["global"]

[recognizers.match]
kind = "regex"
# Allows internal whitespace; iban_canonical normalizer strips before mod-97 validation.
pattern = '''\b[A-Z]{2}\d{2}(?: ?[A-Z0-9]{4}){2,7} ?[A-Z0-9]{1,4}\b'''

[recognizers.validator]
kind = "iban_mod97"

[recognizers.normalizer]
kind = "iban_canonical"

[recognizers.scoring]
base = 0.70
priority = 80

[recognizers.token]

[recognizers.collision]
family = "payment-card-or-iban"
variant = "iban"
precedence = 10
mandatory_anchor = "iban"

[[recognizers]]
id = "card.structural"
class = "custom:credit_card"
enabled = true
locales = ["global"]

[recognizers.match]
kind = "regex"
pattern = '''\b\d(?:[\s-]?\d){12,18}\b'''

[recognizers.validator]
kind = "luhn"

[recognizers.scoring]
base = 0.70
priority = 80

[recognizers.token]

[recognizers.collision]
family = "payment-card-or-iban"
variant = "pan"
precedence = 20

[[recognizers]]
id = "ip.v4"
class = "custom:ip_address"
cooperates_with = ["ip.v6"]
enabled = true
locales = ["global"]

[recognizers.match]
kind = "regex"
pattern = '''\b(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\b'''

[recognizers.validator]
kind = "ipv4_parse"

[recognizers.scoring]
base = 0.70
priority = 80

[recognizers.token]

[[recognizers]]
id = "ip.v6"
class = "custom:ip_address"
cooperates_with = ["ip.v4"]
enabled = true
locales = ["global"]

[recognizers.match]
kind = "regex"
# IPv4-embedded alternatives derive from RFC 4291 Section 2.2 form 3
# (`x:x:x:x:x:x:d.d.d.d`), with the audit trail captured in scratchpad 778.
pattern = '''(?ix)
(?:^|[^0-9a-f:.])
(
  (?:
    (?:[0-9a-f]{1,4}:){6}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}
    |::(?:[0-9a-f]{1,4}:){0,5}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}
    |(?:[0-9a-f]{1,4}:){1}:(?:[0-9a-f]{1,4}:){0,4}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}
    |(?:[0-9a-f]{1,4}:){2}:(?:[0-9a-f]{1,4}:){0,3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}
    |(?:[0-9a-f]{1,4}:){3}:(?:[0-9a-f]{1,4}:){0,2}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}
    |(?:[0-9a-f]{1,4}:){4}:(?:[0-9a-f]{1,4}:){0,1}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}
    |(?:[0-9a-f]{1,4}:){5}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}
  )
  |
  (?:
    (?:[0-9a-f]{1,4}:){7}[0-9a-f]{1,4}
    |(?:[0-9a-f]{1,4}:){1,7}:
    |(?:[0-9a-f]{1,4}:){1,6}:[0-9a-f]{1,4}
    |(?:[0-9a-f]{1,4}:){1,5}(?::[0-9a-f]{1,4}){1,2}
    |(?:[0-9a-f]{1,4}:){1,4}(?::[0-9a-f]{1,4}){1,3}
    |(?:[0-9a-f]{1,4}:){1,3}(?::[0-9a-f]{1,4}){1,4}
    |(?:[0-9a-f]{1,4}:){1,2}(?::[0-9a-f]{1,4}){1,5}
    |[0-9a-f]{1,4}:(?:(?::[0-9a-f]{1,4}){1,6})
    |::(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4}
    |::
  )
)
(?:$|[^0-9a-f:.]|\.(?:$|[^0-9]))
'''
capture_groups = [1]

[recognizers.validator]
kind = "ipv6_parse"

[recognizers.scoring]
base = 0.70
priority = 80

[recognizers.token]

[[recognizers]]
id = "eth.address"
class = "custom:eth_address"
enabled = true
locales = ["global"]

[recognizers.match]
kind = "regex"
pattern = '''\b0x[0-9a-fA-F]{40}\b'''

[recognizers.validator]
kind = "eth_eip55"

[recognizers.scoring]
base = 0.70
priority = 80

[recognizers.token]

# Postal collision warning (research-855 §Collision matrix conclusion):
# `postal.de` and `postal.us` both accept naked five-digit numeric strings.
# This is safe only when active locale chains keep DE and US projections apart.
# Avoid broad chains such as [global, en-US, de-DE] for postal-only workflows.
# Prefer a single active country locale, or split processing by locale_chain.
# Loader lint warns or fails strict mode when this collision class can co-fire.
[[recognizers]]
id = "postal.de"
class = "custom:postal_code"
cooperates_with = ["postal.us"]
enabled = true
locales = ["de-DE"]

[recognizers.match]
kind = "regex"
pattern = '''\b\d{5}\b'''

[recognizers.scoring]
base = 0.70
priority = 70

[recognizers.token]

# Postal collision warning (research-855 §Collision matrix conclusion):
# `postal.de` and `postal.us` both accept naked five-digit numeric strings.
# This is safe only when active locale chains keep DE and US projections apart.
# Avoid broad chains such as [global, en-US, de-DE] for postal-only workflows.
# Prefer a single active country locale, or split processing by locale_chain.
# Loader lint warns or fails strict mode when this collision class can co-fire.
[[recognizers]]
id = "postal.us"
class = "custom:postal_code"
cooperates_with = ["postal.de"]
enabled = true
locales = ["en-US"]

[recognizers.match]
kind = "regex"
pattern = '''\b\d{5}(-\d{4})?\b'''

[recognizers.scoring]
base = 0.70
priority = 70

[recognizers.token]
