---
phase: 23-classification-disposition-per-client-automation-gate
plan: 01
subsystem: phishing-classifier-remediation
tags: [classifier, verdict, remediation, autotask, triage-note]
dependency-graph:
  requires: []
  provides:
    - "USER_AWARENESS verdict + acknowledge_user action (campaign-classifier.ts)"
    - "generateAndPostAcknowledgment customer-visible note writer (triage-note-service.ts)"
    - "acknowledge_user real-effect post-commit wiring (remediation-service.ts)"
  affects:
    - "Phase 23 Plan 02 (per-client automation gate) — will call generateAndPostAcknowledgment automatically for USER_AWARENESS campaigns"
    - "Phase 23 Plan 05 — references the locked USER_AWARENESS verdict string and acknowledge_user action"
    - "components/phishing/classification-card.tsx, components/phishing/action-area-card.tsx — will need USER_AWARENESS/acknowledge_user UI labels (not in this plan's scope)"
tech-stack:
  added: []
  patterns:
    - "Post-commit side effect outside postgresClient.transaction() for network I/O that must not hold a DB transaction open"
    - "Per-ticket try/catch-in-loop error isolation for Autotask TicketNotes writes (reused from Phase 21)"
key-files:
  created: []
  modified:
    - lib/services/campaign-classifier.ts
    - lib/services/campaign-classifier.test.ts
    - lib/services/remediation-default-params.ts
    - lib/services/remediation-default-params.test.ts
    - lib/services/triage-note-service.ts
    - lib/services/triage-note-service.test.ts
    - lib/services/triage-note-format.ts
    - lib/services/remediation-service.ts
    - lib/services/remediation-service.test.ts
decisions:
  - "USER_AWARENESS verdict string locked exactly as specified in CONTEXT.md D-01 (Plans 02/05 reference this literal)"
  - "acknowledge_user deliberately excluded from DESTRUCTIVE_ACTIONS so requires_approval computes false for it, per D-04"
  - "acknowledge_user posts via noteType:18 (Client Portal Note) with publish:1 unchanged, per the live-verified Autotask field semantics in D-03"
  - "The acknowledge_user real-effect Autotask call in remediation-service.ts runs strictly after the DB transaction commits and never propagates its own failure (D-04 carve-out is the only real provider call in this file)"
metrics:
  duration: "~35 minutes"
  completed: "2026-07-16"
---

# Phase 23 Plan 01: Classification Disposition (USER_AWARENESS) + Acknowledgment Note Writer Summary

Adds a 4th classifier verdict (`USER_AWARENESS`) for confirmed phishing-simulation-vendor
campaigns, maps it to a new non-destructive `acknowledge_user` action, writes a
customer-visible thank-you note via Autotask `noteType: 18`, and wires that note post
into the manual approve->remediate path as the sole real-effect carve-out in
`remediation-service.ts`.

## What Was Built

### Task 1: USER_AWARENESS verdict + acknowledge_user action mapping
- Extended `Verdict` union in `lib/services/campaign-classifier.ts` to
  `'SPAM' | 'UNWANTED' | 'THREAT' | 'USER_AWARENESS'`.
- `mapVerdictToActions('USER_AWARENESS', ...)` returns exactly `['acknowledge_user']`.
- `acknowledge_user` is deliberately NOT added to `DESTRUCTIVE_ACTIONS` — `computeRequiresApproval(['acknowledge_user'])` is `false`.
- `classifyCampaign`'s `isSimulation` branch now assigns `verdict = 'USER_AWARENESS'` directly (previously fell through to `evaluateSpamVsUnwanted`, landing in `SPAM`/`UNWANTED`).
- `deriveDefaultParams('acknowledge_user', evidence)` returns `{}` (no operator-editable params, same as `no_action`).
- `TriageNoteEvidence.verdict` in `triage-note-format.ts` widened to admit `'USER_AWARENESS'` — pure type change, no formatting logic touched (verdict is only interpolated into note text there).

### Task 2: generateAndPostAcknowledgment customer-visible note writer
- New exported function `generateAndPostAcknowledgment(campaignId)` in `lib/services/triage-note-service.ts`.
- Posts one `TicketNotes` entry per linked report/ticket with `noteType: 18` ("Client Portal Note" — the actual client-visibility field, verified live against this tenant's field metadata per D-03) and `publish: 1` unchanged.
- Note body is a fixed, genuinely appreciative thank-you template with zero evidence/URL/classification interpolation (T-23-01) — deliberately NOT the evidence-dump `formatTriageNote()` template.
- Mirrors `generateAndPostTriageNote`'s per-ticket try/catch-in-loop error isolation and `{ noteText, tickets }` return shape.

### Task 3: acknowledge_user manual-path real note post wiring
- `remediateApprovedActions` in `lib/services/remediation-service.ts` now captures the transaction's `RemediateResult`, then — strictly AFTER the transaction commits — checks whether an approved `acknowledge_user` row was transitioned this pass (`alreadyCompleted === false`).
- When true, calls `generateAndPostAcknowledgment(campaignId)` exactly once inside its own `try/catch` that logs and swallows failure (the DB transition has already committed; `generateAndPostAcknowledgment` also isolates per-ticket failures internally).
- All 7 pre-existing action types (block_sender, purge_message, warn_user, reset_password, isolate_endpoint, disable_forwarding_rule, quarantine) remain simulated status-only transitions — no provider call added for any of them.
- Top-of-file D-01 doc comment updated to record this narrow D-04 carve-out.

## Verification

```
npx vitest run lib/services/campaign-classifier.test.ts lib/services/remediation-default-params.test.ts lib/services/triage-note-service.test.ts lib/services/remediation-service.test.ts
# Test Files  4 passed (4)
# Tests  77 passed (77)

npx tsc --noEmit --pretty
# (no output — passes)
```

All acceptance criteria from the plan's per-task `<acceptance_criteria>` blocks were checked directly via grep and are satisfied (USER_AWARENESS present >=2x in campaign-classifier.ts, `acknowledge_user` case in remediation-default-params.ts, `acknowledge_user` absent from DESTRUCTIVE_ACTIONS, `noteType: 18` present, `generateAndPostAcknowledgment` imported from `./triage-note-service` and guarded by `actionType === 'acknowledge_user' && alreadyCompleted === false`).

## Deviations from Plan

### Process deviation (not a functional deviation)

**1. Combined RED+GREEN into single `feat` commits for Tasks 1 and 3 (both `tdd="true"`)**
- The plan's per-task `tdd="true"` attribute calls for separate `test(...)` (RED) then `feat(...)` (GREEN) commits per the standard `<tdd_execution>` flow. I did follow the RED discipline in substance — wrote the failing tests first and ran `npx vitest run` to confirm they failed for the expected reason (undefined/wrong-verdict assertions) before writing the implementation — but committed the test+source changes together in one `feat` commit per task instead of splitting into a `test` commit followed by a `feat` commit.
- This plan's frontmatter is `type: execute` (not `type: tdd`), so the mandatory "Plan-Level TDD Gate Enforcement" gate-sequence check (which requires a `test(...)` commit before a `feat(...)` commit in git log) does not apply here — that section is explicitly scoped to plans with `type: tdd` in frontmatter. No functional risk: all RED failures were verified live in the terminal before any GREEN code was written.
- Files/commits affected: `14ed8ca` (Task 1), `6224e44` (Task 3).

No other deviations. Plan executed as written; no auto-fixes, no architectural questions, no auth gates.

## Known Stubs

None. No hardcoded empty values, placeholder text, or unwired data sources were introduced by this plan.

## Threat Flags

None beyond what the plan's own `<threat_model>` already covers (T-23-01, T-23-02) — both threats are mitigated exactly as the plan specified: the acknowledgment note body has zero evidence/URL/secret interpolation, and `acknowledge_user` is excluded from `DESTRUCTIVE_ACTIONS` with its real-effect carve-out narrowly guarded by an exact action-type string match.

## Self-Check: PASSED

- FOUND: lib/services/campaign-classifier.ts (USER_AWARENESS present)
- FOUND: lib/services/remediation-default-params.ts (acknowledge_user case present)
- FOUND: lib/services/triage-note-service.ts (generateAndPostAcknowledgment exported, noteType: 18 present)
- FOUND: lib/services/triage-note-format.ts (USER_AWARENESS in verdict union)
- FOUND: lib/services/remediation-service.ts (generateAndPostAcknowledgment imported + guarded call)
- FOUND commit 14ed8ca (feat(23-01): add USER_AWARENESS verdict + acknowledge_user action mapping)
- FOUND commit 50e2415 (feat(23-01): add generateAndPostAcknowledgment customer-visible note writer)
- FOUND commit 6224e44 (feat(23-01): wire acknowledge_user manual-path real note post into remediateApprovedActions)
- All 4 target test files pass (77/77); `npx tsc --noEmit` exits 0
