# PII Polish — Customer-PII Gate Playbook

**Goal:** clear the PII ledger so promote and publish gates pass. Every pending finding is either real PII (replace with a placeholder in source) or a justified non-PII match (accept in the ledger with pre-decision fields).

**Scope:** Phase 1 detectors are shape-only — card-last-4, email, US phone, ZIP+4, postal-address. Order/transaction IDs, ASINs, and standalone names are a future detector class pending spec-aware detection work. A passing gate is the floor, not "PII-clean."

```bash
cli-printing-press pii-audit <cli-dir>
```

The audit writes `<cli-dir>/.printing-press-pii-polish.json`. Promote and publish re-run the audit themselves, so the ledger header is the authority — they refuse if pending findings or gate failures remain.

## The accept contract

Every `status: "accepted"` entry must populate:

- **`category`** — one of `attribution`, `place_name`, `corporate_name`, `documentation_example`, `api_provider_data`, `synthetic_placeholder`, `other`. The closed enum forces a specific claim about *what shape of non-PII this is*.
- **`evidence_context`** — quote the surrounding 1-2 lines from the source. Lets a reviewer judge the category claim by reading only the ledger.
- **`note`** — free-form; required only when `category` is `other`.

```json
{
  "kind": "email",
  "file": "data.json",
  "line": 17,
  "matched_span": "alice@example.com",
  "status": "accepted",
  "category": "documentation_example",
  "evidence_context": "Inside README's 'Sample request' block, under a code fence demonstrating customer-email; reserved @example.com TLD."
}
```

If `category` and `evidence_context` can't be filled honestly, the finding isn't yet understood. Re-read the source and either fix it or pick a sharper category.

## Per-finding decision

For each pending finding, read 1-2 lines on either side of the match and pick one outcome:

| Outcome | When | What to do |
|---|---|---|
| **Fix in source** | Real customer value (captured order, sniff response, etc.) | Replace with the non-matching placeholder from the table below |
| `category: api_provider_data` | Vendor's published example (Stripe docs use `4242`, support@vendor.com, etc.) | Cite vendor docs URL or captured field name in `evidence_context` |
| `category: documentation_example` | Example value in README/SKILL prose teaching how a field works | Quote the documentation context |
| `category: synthetic_placeholder` | Already a standards-reserved synthetic (`user@example.com`, `+1-555-01xx`, `90210-1234`, `1 EXAMPLE WAY`) | Name the convention in `evidence_context` |
| `category: other` | Non-PII shape that matched (request ID, batch ID, version range hitting ZIP regex) | Required `note` names what the value actually is |

### Replacement placeholders

Use these to avoid re-flagging on the next audit:

| Detector | Placeholder (does not re-match) |
|---|---|
| `card-last-4` | `PII_CARD_LAST4_EXAMPLE` |
| `email` | `PII_EMAIL_EXAMPLE` |
| `phone-us` | `PII_PHONE_EXAMPLE` |
| `zip-plus-4` | `PII_ZIP_EXAMPLE` |
| `postal-address` | `PII_ADDRESS_EXAMPLE` |

When a downstream consumer (parser, schema validator) needs a valid-shape value, use the standards-reserved synthetics — `user@example.com`, `+1-555-0100`, `90210-1234`, `1 EXAMPLE WAY` — and accept the first audit finding as `synthetic_placeholder`. Subsequent audits preserve the accept.

### Detector-specific notes

- **`zip-plus-4`** has high false-positive rate. Request IDs, batch IDs, correlation IDs, and version ranges all match the shape. Most pending findings here resolve to `category: other` with a `note` naming the actual field, not real PII.
- **`postal-address`** matches Title-Case and ALL-CAPS street names with the common US suffixes. Lowercase real addresses are a known gap.
- **`card-last-4`** requires a context token (`card`/`visa`/`ending in`/`last 4`/4+ asterisks/x's). Bare 4-digit numbers don't match.

## Special cases

**Generated files (DO-NOT-EDIT).** Files carrying the `// Generated by CLI Printing Press ... DO NOT EDIT.` header are wiped on every regen — edits there don't survive. Trace upstream: the captured value came from a manuscript or spec file. Fix it at the source, regenerate, re-run the audit. The finding moves to the upstream file.

**Manuscripts (`.manuscripts/<runID>/`).** Highest-risk source — captured browser-sniff content is by construction where customer values entered. Acceptance for manuscript findings requires the explicit `evidence_context`-cited justification; "captured data" alone is not sufficient. When in doubt, replace the value in the manuscript file directly (it's local working state) and use a hand-authored fixture with synthetic values for any downstream tests.

**Vendor spec files at the CLI root (`spec.yaml`, `spec.yml`, `spec.json`).** Exempt from the audit by design — these are the OpenAPI/internal source the operator passed to `--spec`, and vendor `example:` blocks (Stripe `jenny@example.com`, GitHub user-schema example phones) are documentation, not customer PII. The exemption is depth-1 only; the same basename nested under `testdata/` is captured content and stays in scope.

**Vendor spec source archived under `.manuscripts/`.** Files anywhere inside a `.manuscripts/` subtree whose head bytes match an OpenAPI 2.x/3.x or Swagger 2.0 root-document marker are exempt — the operator archived the vendor's published spec source alongside research, and its `example:` blocks are documentation. The exemption is content-based, not glob-based, so per-resource subdir basenames like `apps/calendars.json` or vendor-named files like `pushpress-v3.yaml` are covered without an allowlist. HARs, `session-state.json`, hand-edited proofs, and any other non-spec content under `.manuscripts/` stays in scope because the marker check fails on them.

## Forbidden accept patterns

The binary enforces these via gate failures, not just convention:

- **Identical rationale across 6+ findings** — clustered by normalized `note` + `category`. If you're stamping the same `note` repeatedly, fix the source content or file a follow-up to tighten the detector.
- **Wholesale-accept punt** — when finding count is ≥ 10 and every finding is accepted with no source-level fixes, the all-accepted-no-fixes gate fires.
- **Empty / placeholder `evidence_context`** — `"ctx"`, `"n/a"`, single chars all pass the `TrimSpace` check mechanically but PR review will catch them. Quote the file.

## Resumability

`progress.last_processed_finding_id` is an optional checkpoint to update after walking each finding. The audit's `next:` line uses it to resume mid-walk after a context flush. When absent, the audit resumes from the head.

Finding identity is `(file, line, kind, normalized matched-span)`. Editing a file shifts line numbers — findings get fresh IDs and prior accepts no longer carry forward. This is by design: substantive churn deserves a fresh agent walk. Cosmetic edits (whitespace, case) don't change the ID.

Don't manually mark findings as `fixed`. A real source fix makes the finding disappear from the next audit; the delta line shows `1 resolved`.

## End-state checklist

- [ ] `cli-printing-press pii-audit <cli-dir>` summary reads `no pending findings`, no `incomplete:` block.
- [ ] Every accepted finding has `category` + `evidence_context`. `category: other` also has `note`.
- [ ] No 6+ accepts share a normalized `note` + `category`.
- [ ] Real PII fixes landed in source (`git diff`).
- [ ] If publishing into a public-library clone, add `.printing-press-pii-polish.json` to that repo's `.gitignore`.
