/** * T-162/T-163 — Review workflow DTOs. * * The page's `reviewState` machine (A-003: explicit reviewer lists, not * roles): * * published ──submit──▶ in_review ──approve────────▶ published * ▲ │ ▲ * │ │ └── submit (editor resubmits after fixing) * └──changes requested─┘ * cancel (editor) withdraws an in-flight review → published. * archived is orthogonal (the page trash/archive — not a review state). * * The LEGACY default is `published` — pages created before this feature * behave exactly as before (no review banner, no gate). */ import { z } from "zod"; export const REVIEW_STATES = ["draft", "in_review", "approved", "published", "changes_requested"] as const; export type ReviewState = (typeof REVIEW_STATES)[number]; /** * The transition matrix (enforced in `ReviewService` — an illegal move is a * 400 with the allowed set). `actorRole` is the caller's position: * editor — the page's `updatedBy` (or a page-EDITOR grant) * reviewer — a member of the OPEN review's `reviewerIds` * other — anyone else in the space (may READ, not act) */ export const REVIEW_ACTIONS: Record = { /** Editor submits the page for review (opens/reattaches a Review row). */ submit: { from: ["draft", "published", "changes_requested"], by: "editor" }, /** A reviewer approves → the page is published, the review closes. */ approve: { from: ["in_review"], by: "reviewer" }, /** A reviewer sends it back with feedback. */ request_changes: { from: ["in_review"], by: "reviewer" }, /** The editor withdraws an in-flight review (no decision recorded). */ cancel: { from: ["in_review"], by: "editor" } } as const; export const REVIEW_ACTION_VALUES = ["submit", "approve", "request_changes", "cancel"] as const; export type ReviewAction = (typeof REVIEW_ACTION_VALUES)[number]; /** `POST /pages/:id/review` — perform a review action. */ export const ReviewActionDto = z.object({ action: z.enum(["submit", "approve", "request_changes", "cancel"]), /** Required for `submit`: the explicit reviewer list (A-003 — stable user ids, ≥1). */ reviewerIds: z.array(z.string().min(1)).max(50).optional(), /** Optional decision comment (stored on the Review row). */ comment: z.string().max(5000).optional(), /** Optional deadline for the decision (ISO date-time). */ deadline: z.string().datetime({ offset: true }).nullable().optional() }); export type ReviewActionInput = z.infer;