---
phase: 02-scanners
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/meshulash_guard/scanners/pii.py
  - src/meshulash_guard/guard.py
autonomous: true

must_haves:
  truths:
    - "PIIScanner(labels=[PIILabel.EMAIL_ADDRESS], action=Action.REPLACE) constructs without error"
    - "PIIScanner with explicit labels produces a guardline spec with those labels in required.ner and required.regex"
    - "PIIScanner with PIILabel.PII produces a guardline spec with bundles=['PII'] and expands PII bundle labels into required.ner and required.regex"
    - "PIIScanner with PIILabel.ALL expands all 5 bundles and their labels into the spec"
    - "PIIScanner with overrides produces multiple guardline specs via to_guardline_specs(), one per distinct action group"
    - "Guard.scan_input() uses to_guardline_specs() when available, falling back to to_guardline_spec()"
  artifacts:
    - path: "src/meshulash_guard/scanners/pii.py"
      provides: "PIIScanner class and PIILabel enum"
      contains: "class PIIScanner"
    - path: "src/meshulash_guard/guard.py"
      provides: "Guard.scan_input with to_guardline_specs support"
      contains: "to_guardline_specs"
  key_links:
    - from: "src/meshulash_guard/scanners/pii.py"
      to: "src/meshulash_guard/enums.py"
      via: "imports Action, Condition, _StrValueMixin"
      pattern: "from.*enums.*import"
    - from: "src/meshulash_guard/scanners/pii.py"
      to: "src/meshulash_guard/scanners/base.py"
      via: "subclasses BaseScanner"
      pattern: "class PIIScanner\\(BaseScanner\\)"
    - from: "src/meshulash_guard/guard.py"
      to: "scanner.to_guardline_specs"
      via: "hasattr check for multi-spec scanners"
      pattern: "to_guardline_specs"
---

<objective>
Implement PIIScanner with its 53-label PIILabel enum, bundle shortcuts, ALL sentinel, per-label action overrides via multiple guardline specs, and update Guard.scan_input() to support scanners that produce multiple guardline specs.

Purpose: PIIScanner is the most complex scanner in the SDK. It must expand bundles SDK-side (server's process_sdk() does NOT expand bundles -- confirmed by reading gating.py's _build_label_guardline_index which only reads required.ner and required.regex). Per-label action overrides require multiple guardline specs since the server only supports one action per guardline.

Output: Working PIIScanner class + updated Guard.scan_input() that handles multi-spec scanners.
</objective>

<execution_context>
@/Users/shalev/.claude/get-shit-done/workflows/execute-plan.md
@/Users/shalev/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/02-scanners/02-CONTEXT.md
@.planning/phases/02-scanners/02-RESEARCH.md
@src/meshulash_guard/enums.py
@src/meshulash_guard/scanners/base.py
@src/meshulash_guard/guard.py
@src/meshulash_guard/models.py
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create PIIScanner with PIILabel enum, bundle expansion, and to_guardline_specs</name>
  <files>src/meshulash_guard/scanners/pii.py</files>
  <action>
Create `src/meshulash_guard/scanners/pii.py` with:

1. **PIILabel enum** using `_StrValueMixin` from `..enums`:
   - 47 canonical labels organized by category (see exact list below)
   - 5 bundle shortcuts: `PII = "PII"`, `PHI = "PHI"`, `PCI = "PCI"`, `SECRETS = "SECRETS"`, `TECH = "TECH"`
   - 1 wildcard: `ALL = "__ALL__"`
   - Total: 53 enum members (47 canonical + 5 bundles + 1 ALL)

   Canonical labels by category:
   - Identity (7): PERSON_NAME, NATIONAL_ID, DRIVER_LICENSE, USERNAME, TAX_NUMBER, ACCOUNT_ID, BUSINESS_ID
   - Contact (11): EMAIL_ADDRESS, PHONE_NUMBER, STREET_ADDRESS, CITY, COUNTY, STATE, COUNTRY, POSTAL_CODE, IP_ADDRESS, MAC_ADDRESS, URL
   - Financial (5): CREDIT_CARD, IBAN, BANK_ACCOUNT, ROUTING_NUMBER, CRYPTO_ADDRESS
   - Credentials (20): PASSWORD, SECRET_KEY, ACCESS_TOKEN, PRIVATE_KEY, CONNECTION_STRING, AWS_ACCESS_KEY, AWS_SECRET_KEY, GCP_API_KEY, GCP_SERVICE_ACCOUNT, OPENAI_API_KEY, AZURE_API_KEY, STRIPE_SECRET_KEY, STRIPE_PUBLISHABLE_KEY, SLACK_TOKEN, GITHUB_TOKEN, TWILIO_API_KEY, SENDGRID_API_KEY, HEROKU_API_KEY, API_KEY, TOKEN
   - Technical (2): USER_AGENT, AWS_ARN
   - Temporal (1): DATE_TIME
   - Organizational (1): ORGANIZATION

2. **Bundle-to-labels mapping** as a module-level constant `_BUNDLE_LABELS`:
   The server's `process_sdk()` does NOT expand bundles. The SDK must expand them into explicit labels in `required.ner` and `required.regex`. Define:
   ```python
   _BUNDLE_NAMES = frozenset({"PII", "PHI", "PCI", "SECRETS", "TECH"})
   _ALL_SENTINEL = "__ALL__"
   _BUNDLE_LABELS: dict[str, list[str]] = {
       "PII": [
           "PERSON_NAME", "NATIONAL_ID", "DRIVER_LICENSE", "USERNAME",
           "TAX_NUMBER", "ACCOUNT_ID", "BUSINESS_ID",
           "EMAIL_ADDRESS", "PHONE_NUMBER", "STREET_ADDRESS", "CITY",
           "COUNTY", "STATE", "COUNTRY", "POSTAL_CODE", "IP_ADDRESS",
           "MAC_ADDRESS", "URL",
           "CREDIT_CARD", "IBAN", "BANK_ACCOUNT", "ROUTING_NUMBER",
           "CRYPTO_ADDRESS",
       ],
       "PHI": ["PERSON_NAME"],
       "PCI": ["CREDIT_CARD", "IBAN", "BANK_ACCOUNT", "ROUTING_NUMBER"],
       "SECRETS": [
           "PASSWORD", "SECRET_KEY", "ACCESS_TOKEN", "PRIVATE_KEY",
           "CONNECTION_STRING", "AWS_ACCESS_KEY", "AWS_SECRET_KEY",
           "GCP_API_KEY", "GCP_SERVICE_ACCOUNT", "OPENAI_API_KEY",
           "AZURE_API_KEY", "STRIPE_SECRET_KEY", "STRIPE_PUBLISHABLE_KEY",
           "SLACK_TOKEN", "GITHUB_TOKEN", "TWILIO_API_KEY",
           "SENDGRID_API_KEY", "HEROKU_API_KEY", "API_KEY", "TOKEN",
       ],
       "TECH": ["USER_AGENT", "AWS_ARN"],
   }
   ```
   NOTE: The PHI bundle on the server maps to PERSON_NAME (plus PHI-specific labels which are handled by a separate model -- for v1, PHI in the SDK expands to the labels the server's label registry tags with the PHI bundle). Verify against the server's `get_labels_for_bundle("PHI")` results. The key point: the SDK puts expanded labels into `required.ner` and `required.regex` instead of relying on the server's `bundles` field.

3. **PIIScanner class** subclassing `BaseScanner`:
   ```python
   class PIIScanner(BaseScanner):
       def __init__(
           self,
           labels: Sequence[PIILabel],
           action: Action = Action.REPLACE,
           condition: Condition = Condition.ANY,
           overrides: dict[PIILabel, Action] | None = None,
           threshold: float | None = None,
           allowlist: list[str] | None = None,
       ) -> None:
   ```
   - Validate `labels` is non-empty: raise `ValueError("PIIScanner requires at least one label. Use PIILabel.ALL to scan everything.")`
   - Store all params as private attributes

4. **`to_guardline_spec(self) -> dict`**: Returns a single guardline spec dict.
   - If ANY label has value `_ALL_SENTINEL`: expand ALL 5 bundles' labels into required.ner/regex (deduplicated).
   - Else: iterate labels. If value is in `_BUNDLE_NAMES`, expand that bundle's labels. Otherwise, add the canonical label string to both `ner_labels` and `regex_labels`.
   - Deduplicate the final label lists.
   - The `bundles` field in the spec is ALSO set (for forward compatibility) but the expanded labels in `required.ner`/`required.regex` are what the server actually reads.
   - Set `types.regex = bool(regex_labels)`, `types.ner = bool(ner_labels)`, `types.tc = False`.
   - Include `threshold` in spec if set (as `"threshold"` key).
   - NOTE: If overrides are present, this method returns the spec for the DEFAULT action labels only (labels not in overrides). This follows the principle that `to_guardline_spec()` returns a single dict.

5. **`to_guardline_specs(self) -> dict[str, dict]`**: Returns multiple specs when `overrides` are present.
   - Groups labels by their effective action: labels not in overrides get `self._action`; labels in overrides get their override action.
   - For each action group, builds a guardline spec with just those labels.
   - Returns `{"main": spec_for_default_action, "override_0": spec_for_first_override_action, ...}`.
   - If no overrides, returns `{"main": self.to_guardline_spec()}`.
   - Each spec includes the `name` field as "PIIScanner".

Important implementation notes:
- Use `from __future__ import annotations` for PEP 604 union syntax.
- Import `_StrValueMixin` from `..enums` (it's internal, not re-exported from the package).
- The enum values are ALL_CAPS strings matching the server's canonical label registry exactly.
- Do NOT use `auto()` -- use explicit string values matching the server.
  </action>
  <verify>
Run in Python:
```bash
cd /Users/shalev/Desktop/meshulash_guard && .venv/bin/python -c "
from meshulash_guard.scanners.pii import PIIScanner, PIILabel
from meshulash_guard.enums import Action, Condition

# Basic construction
pii = PIIScanner(labels=[PIILabel.EMAIL_ADDRESS], action=Action.REPLACE)
spec = pii.to_guardline_spec()
assert spec['required']['ner'] == ['EMAIL_ADDRESS'], f'Expected EMAIL_ADDRESS, got {spec[\"required\"][\"ner\"]}'
assert spec['required']['regex'] == ['EMAIL_ADDRESS']
assert spec['types']['tc'] == False
assert spec['action'] == 'replace'

# Bundle expansion
pii_bundle = PIIScanner(labels=[PIILabel.PII])
spec2 = pii_bundle.to_guardline_spec()
assert 'EMAIL_ADDRESS' in spec2['required']['ner'], 'PII bundle should expand to include EMAIL_ADDRESS'
assert 'PHONE_NUMBER' in spec2['required']['ner'], 'PII bundle should expand to include PHONE_NUMBER'
assert spec2['types']['regex'] == True
assert spec2['types']['ner'] == True

# ALL sentinel
pii_all = PIIScanner(labels=[PIILabel.ALL])
spec3 = pii_all.to_guardline_spec()
assert len(spec3['required']['ner']) > 40, f'ALL should expand to 40+ labels, got {len(spec3[\"required\"][\"ner\"])}'

# Overrides produce multiple specs
pii_override = PIIScanner(
    labels=[PIILabel.EMAIL_ADDRESS, PIILabel.NATIONAL_ID],
    action=Action.REPLACE,
    overrides={PIILabel.NATIONAL_ID: Action.BLOCK},
)
specs = pii_override.to_guardline_specs()
assert len(specs) == 2, f'Expected 2 specs, got {len(specs)}'

# Empty labels raises
try:
    PIIScanner(labels=[])
    assert False, 'Should have raised ValueError'
except ValueError:
    pass

# Enum string values
assert str(PIILabel.EMAIL_ADDRESS) == 'EMAIL_ADDRESS'
assert str(PIILabel.PII) == 'PII'

print('All PIIScanner assertions passed')
"
```
  </verify>
  <done>PIIScanner constructs with labels, produces correct guardline specs for individual labels, bundle shortcuts, and ALL sentinel. Bundle labels are expanded SDK-side into required.ner and required.regex. Per-label overrides produce multiple guardline specs via to_guardline_specs(). PIILabel has 53 members (47 canonical + 5 bundles + 1 ALL).</done>
</task>

<task type="auto">
  <name>Task 2: Update Guard.scan_input() to support multi-spec scanners</name>
  <files>src/meshulash_guard/guard.py</files>
  <action>
Modify `Guard.scan_input()` in `src/meshulash_guard/guard.py` to handle scanners that produce multiple guardline specs (i.e., PIIScanner with overrides).

Current code builds guardline_specs as:
```python
guardline_specs = {
    f"sdk_{scanner.__class__.__name__.lower()}_{i}": scanner.to_guardline_spec()
    for i, scanner in enumerate(scanners)
}
```

Change to:
```python
guardline_specs: dict[str, dict] = {}
for i, scanner in enumerate(scanners):
    prefix = f"sdk_{scanner.__class__.__name__.lower()}_{i}"
    if hasattr(scanner, "to_guardline_specs"):
        multi_specs = scanner.to_guardline_specs()
        for key, spec in multi_specs.items():
            guardline_specs[f"{prefix}_{key}"] = spec
    else:
        guardline_specs[prefix] = scanner.to_guardline_spec()
```

This means:
- Simple scanners (TC scanners without overrides): key is `sdk_topicscanner_0` (same as before).
- PIIScanner without overrides: `to_guardline_specs()` returns `{"main": spec}`, so key is `sdk_piiscanner_0_main`.
- PIIScanner with overrides: `to_guardline_specs()` returns `{"main": spec, "override_0": spec2}`, so keys are `sdk_piiscanner_0_main` and `sdk_piiscanner_0_override_0`.

The rest of `scan_input()` stays the same -- the payload is sent with these keys, and the server returns per-scanner results keyed the same way.

Do NOT change scan_output(), deanonymize(), clear_cache(), or _validate_credentials().
Do NOT change the class docstring or __init__ signature.
  </action>
  <verify>
Run in Python:
```bash
cd /Users/shalev/Desktop/meshulash_guard && .venv/bin/python -c "
from meshulash_guard.guard import Guard
from meshulash_guard.scanners.pii import PIIScanner, PIILabel
from meshulash_guard.enums import Action, Condition
from meshulash_guard.scanners.base import BaseScanner

# Test that scan_input builds specs correctly by inspecting the code path
# (We can't test against a live server, but we can verify the spec-building logic)

# Create a mock scanner with to_guardline_specs
class MockMultiScanner(BaseScanner):
    def to_guardline_spec(self):
        return {'name': 'MockMulti', 'action': 'log'}
    def to_guardline_specs(self):
        return {'main': {'name': 'MockMulti', 'action': 'replace'}, 'override_0': {'name': 'MockMulti', 'action': 'block'}}

# Create a simple scanner without to_guardline_specs
class MockSimpleScanner(BaseScanner):
    def to_guardline_spec(self):
        return {'name': 'MockSimple', 'action': 'log'}

# Verify the spec-building logic by extracting it
scanners = [MockMultiScanner(), MockSimpleScanner()]
guardline_specs = {}
for i, scanner in enumerate(scanners):
    prefix = f'sdk_{scanner.__class__.__name__.lower()}_{i}'
    if hasattr(scanner, 'to_guardline_specs'):
        multi_specs = scanner.to_guardline_specs()
        for key, spec in multi_specs.items():
            guardline_specs[f'{prefix}_{key}'] = spec
    else:
        guardline_specs[prefix] = scanner.to_guardline_spec()

assert 'sdk_mockmultiscanner_0_main' in guardline_specs
assert 'sdk_mockmultiscanner_0_override_0' in guardline_specs
assert 'sdk_mocksimplescanner_1' in guardline_specs
assert guardline_specs['sdk_mockmultiscanner_0_main']['action'] == 'replace'
assert guardline_specs['sdk_mockmultiscanner_0_override_0']['action'] == 'block'
assert guardline_specs['sdk_mocksimplescanner_1']['action'] == 'log'

print('Guard.scan_input spec-building logic verified')
"
```
  </verify>
  <done>Guard.scan_input() checks for to_guardline_specs() on each scanner and expands multi-spec scanners into separate guardline entries. Simple scanners (using to_guardline_spec()) continue to work as before. Keys follow the pattern sdk_{class}_{index}_{speckey} for multi-spec and sdk_{class}_{index} for single-spec.</done>
</task>

</tasks>

<verification>
1. PIILabel enum has exactly 53 members (47 canonical + 5 bundles + 1 ALL)
2. `str(PIILabel.EMAIL_ADDRESS) == "EMAIL_ADDRESS"` (StrValueMixin works)
3. PIIScanner with explicit labels produces correct required.ner/regex
4. PIIScanner with bundle shortcuts expands labels SDK-side
5. PIIScanner with ALL expands all bundles' labels
6. PIIScanner with overrides produces multiple specs via to_guardline_specs()
7. Guard.scan_input() handles both single-spec and multi-spec scanners
8. Empty labels list raises ValueError
</verification>

<success_criteria>
- PIIScanner is importable from meshulash_guard.scanners.pii
- PIILabel contains all 53 canonical labels + bundles + ALL
- to_guardline_spec() produces valid server-compatible guardline specs
- to_guardline_specs() splits overrides into separate specs per action group
- Guard.scan_input() integrates with multi-spec scanners
- All verification commands pass
</success_criteria>

<output>
After completion, create `.planning/phases/02-scanners/02-01-SUMMARY.md`
</output>
