Data flow · states · failure modes · what’s borrowed vs invented · every user-facing string across CLI, mac app, and SPA.
piiEnabled UserDefaults key + one env line, and
two isinstance lines in the shared exception classifier. No new stage, event,
manifest field, orchestration primitive, progress signal, colour, or glyph.
(Delivering the detection stack itself — the on-demand Background Assets fetch + [pii]
extra + find_pii_stack() — is a separate workstream; see docs/design-redact-pii.md.)
language="en"
Manifest stage states for PII: absent (off) · running · complete ·
running-then-abandoned (failure — re-runs next time, never poisons cache).
Cache-bust contract: the topic stage hashes pii_enabled
(pipeline.py:1309), so toggling correctly invalidates everything downstream. Nothing to add.
| ID | Trigger | Raise site | Exception (base) | Classified TODAY | Target | Retry? |
|---|---|---|---|---|---|---|
| F1 | Presidio not installed | _init_presidio import | ImportError | ✓ MISSING_DEP isinstance(ImportError) |
MISSING_DEP | no |
| F2 | Model missing from bundle (desktop) | _ensure_spacy_model | FrozenSidecarError (RuntimeError) |
✗ UNKNOWN + engineer text (“PyInstaller datas”) |
MISSING_DEP | no |
| F3 | Model download failed (CLI) | ensure_spacy_model | PackageInstallError (RuntimeError) |
✗ UNKNOWN “failed (exit 1)” ∉ _NETWORK_RE |
MISSING_DEP | yes |
| F4 | Disk full writing cooked / summary | write_cooked_* | OSError errno 28 | ✓ DISK | DISK | yes |
| F5 | Presidio analyse/anonymise error | _redact_text | (various) | ~ UNKNOWN acceptable — genuinely rare/opaque |
UNKNOWN | — |
The whole gap is two rows. F1 and F4 already classify correctly through the existing
catch-all. Only F2 and F3 — the two most likely real-world cases — fall to
UNKNOWN and leak engineer-worded text, because categorise_exception’s
isinstance ladder doesn’t know these two custom exception types. Fix = extend that
one central function; see §3.
| Mechanism | Template / source (file:line) | PII today | Verdict |
|---|---|---|---|
Terminus RunFailedEvent | run_lifecycle.py:799 (BaseException) | already covers PII | BORROW · 0 change |
| Exception → Cause classify | categorise_exception:351 | F2/F3 → UNKNOWN | EXTEND · +2 isinstance |
| Cause → CLI failure banner | cli.py:822 MISSING_DEP branch | renders on category | BORROW · 0 change |
Cause → desktop row .failed | ProjectRow.swift:245 | renders on category | BORROW · 0 change |
| Cause → desktop popover + fixHint | ProjectDiagnosticPopover.swift · PipelineRunner.swift:1709 | .missingDep hint exists | BORROW · 0 change |
| Cause → SPA status page | server/status_page.py:152 | renders terminus | BORROW · 0 change |
| Manifest running/complete | mark_stage_* — pipeline.py:1265,1279 | already called | BORROW · 0 change |
| Cache-bust provenance | pipeline.py:1309 (pii_enabled hash) | already hashed | BORROW · 0 change |
| Doctor health check | doctor.py:616 check_pii | exists | BORROW · 0 change |
| CLI capability matrix row | pipeline_view/cli.py:48–50 | PII row + reasons exist | BORROW · reword lg→sm* |
| CLI success + download line | _print_step · preflight.pii.downloading | exist | BORROW · 0 change |
| Per-stage progress event | _emit_stage_entry / RunProgressSubtitle.swift:22 | deliberately folded into neighbours | BORROW · leave as-is |
| Settings → env → sidecar | BristlenoseShared.swift:226–274 | no PII key | NEW · 1 key + 1 line |
| Native Settings tab | SettingsView.swift:41 TabView | no Privacy tab | NEW · tab instance |
| New thing | Kind | Justification / what it borrows |
|---|---|---|
| Privacy Settings tab | UI | PII has no control anywhere. Exact TabView + in-cell-subtitle idiom of the three existing tabs — a new instance, not a new pattern. |
piiEnabled UserDefaults + env line | state | Same persist-then-inject pattern as whisperBackend, palette et al. 1 read + 1 env[…] line. |
+2 isinstance in categorise_exception | logic | Without it F2/F3 → UNKNOWN + engineer text. Extends the existing isinstance ladder in the one central classifier; benefits every caller. |
| 3 Cause messages + tab strings | copy | Shown for approval in §4. Re-id-safe constant strings (no str(exc)). |
| Orchestration / events / journalling | NONE | No new stage, event type, manifest field, or progress signal. pii_summary.txt already written; s07 already logs. |
| UX tokens (colour / glyph / spacing) | NONE | Reuses MessageKind.error glyph, system red, existing categories. Zero new tokens. |
* Phase 0 — model choice: the name is split across 5 sites —
sm: s07 constant + the “~12 MB, one-off” download copy;
lg: the spec-excludes entry, Presidio’s default engine, the matrix reason string.
In the on-demand world (Apple-Hosted Background Assets, not bundled) this is a
download-time choice, not a bundle one: sm-stack ≈150 MB vs
lg-stack ≈550 MB, both Apple-hosted. Recommend sm (faster first-enable,
~1 F1-point lower NER) and align the three lg sites. Full spec:
docs/design-redact-pii.md.
EXISTS ships today, unchanged NEW / REWORD proposed — approve or adjust the text.
$ bristlenose run interviews/ --redact-pii Downloading PII detector (~12 MB, one-off)… ✓ ← EXISTS · preflight.pii.downloading ✓ Redacted PII (47 entities) [3.4s] ← EXISTS · _print_step # failure (F2/F3) — after the fix, classified MISSING_DEP: ✗ The language model for redaction isn’t installed. ← NEW copy (F2/F3 Cause.message) Run bristlenose doctor to check your setup. ← reuses MISSING_DEP banner shape
.failed rowMessageKind.error glyph + Cause.message. Right-click → Retry. No new UI.PII LLM pass / Custom names. Recommend dropping or marking them — they warn and do nothing. Your call; SPA-only.The interactive toggle is native-only (like Whisper backend, palette, etc.) — the web SPA keeps its read-only config-reference. No new SPA control in this plan.
sm); align s07 constant · Presidio NlpEngine config · spec · matrix reason string · download copy. Prove redaction in a real unpacked layout, not just the dev venv. (Delivery of that stack — the on-demand Background Assets fetch — is Phase 2; see docs/design-redact-pii.md.)isinstance in categorise_exception (FrozenSidecarError, PackageInstallError → MISSING_DEP) with clean re-id-safe messages. Unit test: F1–F5 each map to the target category.PrivacySettingsView + piiEnabled UserDefaults key + one env["BRISTLENOSE_PII_ENABLED"] line in BristlenoseShared.swift. Register the tab in SettingsView.