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
| Column | Meaning |
|---|---|
| ID | PAR-NNN.MM identifier. Stable across releases. |
| Epic | The epic the row belongs to. |
| V1 behavior | The exact behavior in legacy Unphish, harvested from Django/React code, staff workbooks, and the schema dump. |
| V2 disposition | One of retained, replaced, merged, retired. Approved by product. |
| Source status | One of Implemented, Partial, Missing, Retired, Blocked. Distinct from QA status. |
| Real-browser flow | Path to the Playwright/manual flow that exercises this row with a real cookie. |
| API assertion | Path to the API test that confirms the data contract. |
| Seeded data assertion | Confirmation that the qa_v1_parity fixture exercises this row. |
| Source-state assertion | Whether the row's UI labels its data source correctly (live / imported / demo / fixture / unavailable). |
| Audit assertion | For sensitive flows, whether the audit log is checked. |
| Notes | Spec links, edge cases, open product decisions. |
How rows get created
Rows come from three sources:
- The v1 schema and code. Every Django model, every React route, every API endpoint contributes rows to the relevant epic.
- 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.
- OpenSpec changes. Each
openspec/changes/*.mdproposal 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
| Disposition | When | Required action |
|---|---|---|
| retained | Direct v2 equivalent required. | Build the equivalent; row stays open until v2 covers it. |
| replaced | Fulfilled by a new v2 capability. | Document the new capability; the row points at it. |
| merged | Combined into another v2 object or workflow. | Document the merge; the row is closed and points at the consolidated row. |
| retired | Intentionally 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:
- A real-browser role flow. Not a curl call, not a unit test — an actual Playwright or manual flow with a real cookie.
- An API assertion. The data contract is verified at the API layer (auth, source state, scope).
- A seeded data assertion. The
qa_v1_parityfixture exercises the row. - 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
Implementedwithout QA pass. They are independent; both must be set. - Closing a row by marking it
Retiredwithout 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.