Unphish v2 Docs

V1 parity matrix

The 20-epic, 102-row truth table for every v1 capability and its v2 disposition.

The v1 parity matrix is the single source of truth for "is this v1 feature covered in v2?" It lives at docs/qa/v1-parity-matrix.md and is referenced from PRs, QA signoff, and release planning.

What the matrix is

It is a long table organized into 20 epics (PAR-001 through PAR-020) covering:

  • Identity and team
  • Client and brand setup
  • Detection configuration (scans, query folders, keywords, sites)
  • Whitelist and watchlist
  • Case management and triage
  • Evidence and enrichment
  • Classification
  • Enforcement (per channel)
  • Verification and resurrection
  • Reporting and notifications
  • Threat intelligence
  • Public API
  • Quotas and entitlements
  • Audit
  • Provider integrations

Each epic breaks down into 1–15 rows, each row representing one verifiable behavior.

What each row tracks

ColumnMeaning
IDPAR-NNN.MM identifier. Stable across releases.
EpicThe epic the row belongs to.
V1 behaviorThe exact behavior in legacy Unphish, harvested from Django/React code, staff workbooks, and the schema dump.
V2 dispositionOne of retained, replaced, merged, retired. Approved by product.
Source statusOne of Implemented, Partial, Missing, Retired, Blocked. Distinct from QA status.
Real-browser flowPath to the Playwright/manual flow that exercises this row with a real cookie.
API assertionPath to the API test that confirms the data contract.
Seeded data assertionConfirmation that the qa_v1_parity fixture exercises this row.
Source-state assertionWhether the row's UI labels its data source correctly (live / imported / demo / fixture / unavailable).
Audit assertionFor sensitive flows, whether the audit log is checked.
NotesSpec links, edge cases, open product decisions.

How rows get created

Rows come from three sources:

  1. The v1 schema and code. Every Django model, every React route, every API endpoint contributes rows to the relevant epic.
  2. Staff UAT workbooks. The 1,540-case staff UAT corpus was mapped onto rows; gaps surfaced child rows like PAR-004.05, PAR-009.06.
  3. OpenSpec changes. Each openspec/changes/*.md proposal that touches user behavior must add or update rows.

When a new v1 area is investigated and produces previously-unmapped behavior, an epic is added. The matrix is intentionally exhaustive.

Disposition rules

DispositionWhenRequired action
retainedDirect v2 equivalent required.Build the equivalent; row stays open until v2 covers it.
replacedFulfilled by a new v2 capability.Document the new capability; the row points at it.
mergedCombined into another v2 object or workflow.Document the merge; the row is closed and points at the consolidated row.
retiredIntentionally removed.Requires explicit product approval logged in the row.

A row may not be silently retired. If a v1 behavior has no equivalent and no plan, the row stays Missing until product makes a call.

Source status vs. QA status vs. checklist readiness

Three different statuses live in the matrix and are deliberately distinct:

  • Source status — "Is the code in v2 doing this?" Set by engineering. Values: Implemented, Partial, Missing, Retired, Blocked.
  • QA status — "Has a real-browser, real-cookie flow signed this off?" Set by QA. Values: Pass, Fail, Skip (with reason), Pending.
  • Source-doc readiness — "Is the canonical spec in /docs/ updated?" Set by docs/spec owner. Values: Updated, Stale.

A row is green when source status is Implemented (or explicitly Retired/Replaced), QA status is Pass, and source-doc readiness is Updated. Anything less is amber or red and blocks production promotion for the affected surface.

P0 vs. P1 rows

Each row carries a priority. P0 rows are pilot-blocking; they must be green for the relevant pilot epic to ship. P1 rows are post-pilot; they must be tracked but do not block the first cutover.

Every P0 row requires:

  1. A real-browser role flow. Not a curl call, not a unit test — an actual Playwright or manual flow with a real cookie.
  2. An API assertion. The data contract is verified at the API layer (auth, source state, scope).
  3. A seeded data assertion. The qa_v1_parity fixture exercises the row.
  4. A source-state assertion. The UI labels the row's data source correctly.

Sensitive P0 rows additionally require an audit assertion.

Fixture fallbacks fail signoff

A row that "passes" because the page silently rendered fixture data does not pass. Source-state assertions are the contract. If the production-mode UI shows fixture data without a label, the row fails and the bug is source_label_missing or silent_fixture_fallback.

This is intentional. Source labelling is one of the four release-blocking review findings; we will not let it regress.

Common matrix anti-patterns

These patterns appear in PRs and should be rejected at review:

  • "Tested manually" with no flow path. The matrix needs a path to a runnable flow.
  • "Tested with DEV_AUTH_BYPASS." Bypass is for debugging; the matrix needs real-cookie flows.
  • Marking source status Implemented without QA pass. They are independent; both must be set.
  • Closing a row by marking it Retired without product approval. Retirement requires the product owner's note.
  • Dark-mode skip on a dashboard surface. Dashboard, threat feed, case detail, client review, partner queue, watchlist, whitelist, scan centre all require dark-mode QA.

Checking the current state

To see where the matrix stands today, read docs/qa/v1-parity-matrix.md. It is the running truth table; it is updated as work lands.

For the staff UAT mapping that fed the matrix, read docs/qa/staff-uat-functional-spec-gap-review-2026-05-03.md.

On this page