/* ────────────────────────────────────────────────────────────
   review.jsx — 검수 보드(feature b §5)
   4단계 파이프라인 SME1 → 설계 → SME2 → PM. 라운드(검수차수)·attempt(반려시 +1).
   · 단계별 승인/반려 + 사유. 반려 → SME1 으로 리셋, 전 단계 재승인(attempt-scoped, H5).
   · 승인 버튼은 호출자의 persona/role 이 단계와 맞을 때만 활성(owner override).
   · ReviewBoard(스테퍼+결정), RoundHistory(차수·attempt 타임라인).
   REST: POST/GET /api/projects/:id/reviews · GET /reviews/current · POST /reviews/:roundId/decision · /cancel.
   window 노출 패턴(빌드툴 없음). tokens·해요체·이모지 없음. ErrorBoundary 폴백 의존.
   ──────────────────────────────────────────────────────────── */

/* 단계 정의 — 서버 STAGE_ORDER 와 동일(SME1·design·SME2·pm). 라벨/요구 persona. */
const REVIEW_STAGES = [
  { key: "SME1",   label: "SME 1차",  short: "SME1", persona: "SME",    color: "var(--primary)" },
  { key: "design", label: "설계 검수", short: "설계", persona: "design", color: "var(--violet)" },
  { key: "SME2",   label: "SME 2차",  short: "SME2", persona: "SME",    color: "var(--cyan)" },
  { key: "pm",     label: "PM 검수",   short: "PM",   persona: "pm",     color: "var(--caution)" },
];
const REVIEW_STAGE_ORDER = REVIEW_STAGES.map((s) => s.key);
function reviewStageMeta(key) {
  return REVIEW_STAGES.find((s) => s.key === key) || { key, label: key, short: key, persona: null, color: "var(--label-alt)" };
}
function reviewStageIndex(key) { return REVIEW_STAGE_ORDER.indexOf(key); }

const REVIEW_PERSONA_LABEL = { SME: "SME", design: "설계", pm: "PM" };

/* 현재 사용자가 특정 단계를 승인/반려할 수 있는지(STRICT persona, owner override). */
function reviewCanActOnStage(stageKey, role, myPersona) {
  if (role === "owner") return true;               // owner 는 모든 단계 override
  if (role !== "reviewer") return false;           // editor/viewer 는 승인 불가
  const need = reviewStageMeta(stageKey).persona;
  return !!myPersona && myPersona === need;
}

/* 상대시간(해요체) — collabAgo 가 있으면 재사용, 없으면 간이판. */
function reviewAgo(iso) {
  if (typeof collabAgo === "function") return collabAgo(iso);
  if (!iso) return "";
  const d = Math.max(0, (Date.now() - new Date(iso).getTime()) / 1000);
  if (d < 60) return "방금";
  if (d < 3600) return `${Math.floor(d / 60)}분 전`;
  if (d < 86400) return `${Math.floor(d / 3600)}시간 전`;
  return `${Math.floor(d / 86400)}일 전`;
}

/* ── 단계 스테퍼 — 각 단계 상태(승인됨/현재/대기) ──
   approvedStages: Set(stageKey) (이번 attempt 의 currentApprovals 에서 파생).
   currentStage: 라운드의 current_stage. status='approved' 이면 전 단계 완료. */
function ReviewStepper({ approvedStages, currentStage, status }) {
  const done = status === "approved";
  return React.createElement("div", { style: { display: "flex", alignItems: "stretch", gap: 0, flexWrap: "wrap" } },
    REVIEW_STAGES.map((st, i) => {
      const isApproved = done || approvedStages.has(st.key);
      const isCurrent = !done && status === "open" && currentStage === st.key;
      const bg = isApproved ? "var(--positive-bg)" : isCurrent ? "var(--primary-light)" : "var(--bg-alt)";
      const fg = isApproved ? "var(--positive)" : isCurrent ? "var(--primary)" : "var(--label-assist)";
      const bd = isApproved ? "var(--positive)" : isCurrent ? "var(--primary)" : "var(--border)";
      return React.createElement("div", { key: st.key, style: { display: "flex", alignItems: "center" } },
        React.createElement("div", {
          style: {
            display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 4,
            minWidth: 92, padding: "12px 10px", borderRadius: "var(--r-md)",
            background: bg, border: `1.5px solid ${bd}`,
          },
        },
          React.createElement("div", {
            style: {
              width: 26, height: 26, borderRadius: "50%", display: "flex", alignItems: "center", justifyContent: "center",
              background: isApproved ? "var(--positive)" : isCurrent ? "var(--primary)" : "var(--bg-normal)",
              border: isApproved || isCurrent ? "none" : "1.5px solid var(--border)",
              color: isApproved || isCurrent ? "#fff" : "var(--label-assist)", fontWeight: 800, fontSize: 12.5,
            },
          }, isApproved
            ? React.createElement("svg", { width: 14, height: 14, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 3, strokeLinecap: "round", strokeLinejoin: "round" }, React.createElement("path", { d: "M20 6 9 17l-5-5" }))
            : String(i + 1)),
          React.createElement("div", { className: "t-caption-1", style: { color: fg, fontWeight: 800, lineHeight: 1.2, textAlign: "center" } }, st.label),
          React.createElement("div", { className: "t-caption-1", style: { color: "var(--label-assist)", fontSize: 10.5 } },
            isApproved ? "승인됨" : isCurrent ? "검수 중" : "대기")),
        i < REVIEW_STAGES.length - 1
          ? React.createElement("div", { style: { width: 18, height: 2, background: isApproved ? "var(--positive)" : "var(--border)", margin: "0 2px" } })
          : null);
    })
  );
}

/* ── 반려 사유 입력 모달 — 반려는 사유 필수(서버 400 방어). 리셋 결과 명시(해요체). ── */
function ReviewRejectModal({ stageLabel, onCancel, onConfirm, busy }) {
  const [reason, setReason] = useState("");
  const trimmed = reason.trim();
  return React.createElement("div", {
    style: { position: "fixed", inset: 0, background: "rgba(0,0,0,0.42)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 70, padding: 20 },
    onMouseDown: (e) => { if (e.target === e.currentTarget && !busy) onCancel(); },
  },
    React.createElement("div", { style: { width: "100%", maxWidth: 460, background: "var(--bg-normal)", border: "1px solid var(--border)", borderRadius: "var(--r-lg)", boxShadow: "var(--sh-2)", padding: "22px 22px 20px" } },
      React.createElement("div", { className: "t-headline-1", style: { color: "var(--label-normal)", marginBottom: 6 } }, `${stageLabel} 반려`),
      React.createElement("div", {
        style: { display: "flex", gap: 8, alignItems: "flex-start", background: "var(--caution-bg)", border: "1px solid var(--caution)", borderRadius: "var(--r-sm)", padding: "10px 12px", margin: "0 0 14px" },
      },
        React.createElement("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "var(--caution)", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", style: { flexShrink: 0, marginTop: 1 } }, React.createElement("path", { d: "M12 9v4M12 17h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z" })),
        React.createElement("div", { className: "t-caption-1", style: { color: "var(--label-neutral)", lineHeight: 1.5 } },
          "반려하면 검수가 ", React.createElement("b", null, "SME 1차"), "로 되돌아가고, 모든 단계를 다시 승인받아야 해요. 검수차수는 유지되고 시도(attempt)가 1 올라가요.")),
      React.createElement("label", { className: "t-label-2", style: { display: "block", color: "var(--label-neutral)", marginBottom: 6 } }, "반려 사유 (필수)"),
      React.createElement("textarea", {
        value: reason, onChange: (e) => setReason(e.target.value), rows: 4, autoFocus: true,
        placeholder: "어떤 점을 수정해야 하는지 적어 주세요.",
        style: { width: "100%", resize: "vertical", boxSizing: "border-box", padding: "10px 12px", borderRadius: "var(--r-sm)", border: "1px solid var(--border)", background: "var(--bg-canvas)", color: "var(--label-normal)", fontSize: 13.5, lineHeight: 1.5, fontFamily: "inherit" },
      }),
      React.createElement("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 16 } },
        React.createElement(Button, { kind: "ghost", size: "md", onClick: onCancel, disabled: busy }, "취소"),
        React.createElement(Button, {
          kind: "danger", size: "md", disabled: busy || !trimmed,
          onClick: () => onConfirm(trimmed),
        }, busy ? React.createElement(Spinner, { s: 15, color: "#fff" }) : "반려하기")))
  );
}

/* ── 라운드 히스토리 — 검수차수 + 시도(attempt) 타임라인(액션 시간순) ── */
function RoundHistory({ rounds }) {
  if (!Array.isArray(rounds) || rounds.length === 0) {
    return React.createElement("div", { className: "t-caption-1", style: { color: "var(--label-assist)", padding: "10px 2px" } }, "이전 검수 기록이 없어요.");
  }
  return React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 14 } },
    rounds.map((r) => {
      const actions = Array.isArray(r.actions) ? r.actions : [];
      const statusMeta = {
        open:     { label: "진행 중", color: "var(--primary)", bg: "var(--primary-light)" },
        approved: { label: "승인 완료", color: "var(--positive)", bg: "var(--positive-bg)" },
        rejected: { label: "반려", color: "var(--negative)", bg: "var(--negative-bg)" },
        canceled: { label: "취소됨", color: "var(--label-alt)", bg: "var(--bg-alt)" },
      }[r.status] || { label: r.status, color: "var(--label-alt)", bg: "var(--bg-alt)" };
      return React.createElement("div", { key: r.roundId, style: { border: "1px solid var(--border)", borderRadius: "var(--r-md)", padding: "12px 14px", background: "var(--bg-normal)" } },
        React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: actions.length ? 10 : 0 } },
          React.createElement("span", { className: "t-label-1 tnum", style: { color: "var(--label-normal)", fontWeight: 800 } }, `검수 ${r.roundNo}차`),
          React.createElement(Pill, { color: "var(--label-alt)", bg: "var(--bg-alt)" }, `시도 ${r.attemptNo}`),
          React.createElement(Pill, { color: statusMeta.color, bg: statusMeta.bg }, statusMeta.label),
          React.createElement("span", { className: "t-caption-1", style: { marginLeft: "auto", color: "var(--label-assist)" } }, reviewAgo(r.createdAt))),
        actions.length
          ? React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 7 } },
              actions.map((a) => {
                const ok = a.decision === "approve";
                return React.createElement("div", { key: a.actionId || (a.stage + a.createdAt), style: { display: "flex", alignItems: "flex-start", gap: 8 } },
                  React.createElement("span", {
                    style: { flexShrink: 0, marginTop: 2, width: 16, height: 16, borderRadius: "50%", display: "inline-flex", alignItems: "center", justifyContent: "center", background: ok ? "var(--positive)" : "var(--negative)", color: "#fff" },
                  }, ok
                    ? React.createElement("svg", { width: 9, height: 9, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 4, strokeLinecap: "round", strokeLinejoin: "round" }, React.createElement("path", { d: "M20 6 9 17l-5-5" }))
                    : React.createElement("svg", { width: 9, height: 9, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 4, strokeLinecap: "round", strokeLinejoin: "round" }, React.createElement("path", { d: "M18 6 6 18M6 6l12 12" }))),
                  React.createElement("div", { style: { minWidth: 0, flex: 1 } },
                    React.createElement("div", { className: "t-caption-1", style: { color: "var(--label-neutral)" } },
                      React.createElement("b", { style: { color: "var(--label-normal)" } }, reviewStageMeta(a.stage).label),
                      ok ? " 승인" : " 반려",
                      React.createElement("span", { style: { color: "var(--label-assist)" } }, ` · ${(a.actor && a.actor.name) || "알 수 없음"} · 시도 ${a.attemptNo} · ${reviewAgo(a.createdAt)}`)),
                    !ok && a.reason
                      ? React.createElement("div", { className: "t-caption-1", style: { color: "var(--negative)", background: "var(--negative-bg)", borderRadius: "var(--r-xs)", padding: "4px 8px", marginTop: 3, lineHeight: 1.5, wordBreak: "break-word" } }, a.reason)
                      : null));
              }))
          : null);
    })
  );
}

/* ── 검수 보드 본체 ──
   props: project(id·role), me, onClose.
   라운드 시작/결정/취소 + 스테퍼 + 현재 단계 결정 카드 + 히스토리. */
function ReviewBoard({ project, me, onClose }) {
  const role = project.role || project.projectRole || null;
  const isOwner = role === "owner";
  const canStart = role === "owner" || role === "editor";   // editor+ 가 라운드 시작

  const [rounds, setRounds] = useState(null);   // 전체 라운드(히스토리). null=로딩
  const [current, setCurrent] = useState(null); // 열린 라운드(상세) | null
  const [myPersona, setMyPersona] = useState(null);
  const [err, setErr] = useState("");
  const [busy, setBusy] = useState(false);
  const [rejecting, setRejecting] = useState(null);   // { stageKey } | null

  const load = useCallback(async () => {
    setErr("");
    try {
      const [list, cur, mem] = await Promise.all([
        api(`/projects/${project.id}/reviews`),
        api(`/projects/${project.id}/reviews/current`),
        api(`/projects/${project.id}/members`).catch(() => ({ items: [] })),
      ]);
      setRounds(Array.isArray(list?.items) ? list.items : []);
      setCurrent(cur && cur.round ? cur.round : null);
      const mine = (mem?.items || []).find((m) => m.userId === (me && me.id));
      setMyPersona(mine ? (mine.persona || null) : null);
    } catch (ex) {
      setErr(ex.message || "검수 정보를 불러오지 못했어요.");
      setRounds([]);
    }
  }, [project.id, me]);
  useEffect(() => { load(); }, [load]);

  const startRound = useCallback(async () => {
    setBusy(true); setErr("");
    try {
      await api(`/projects/${project.id}/reviews`, { method: "POST", body: {} });
      await load();
    } catch (ex) {
      setErr(ex.message || "검수 라운드를 시작하지 못했어요.");
    } finally { setBusy(false); }
  }, [project.id, load]);

  const decide = useCallback(async (decision, reason) => {
    if (!current) return;
    setBusy(true); setErr("");
    try {
      await api(`/projects/${project.id}/reviews/${current.roundId}/decision`, {
        method: "POST", body: { decision, reason: reason || undefined },
      });
      setRejecting(null);
      await load();
    } catch (ex) {
      setErr(ex.message || "검수 결정을 처리하지 못했어요.");
    } finally { setBusy(false); }
  }, [project.id, current, load]);

  const cancelRound = useCallback(async () => {
    if (!current) return;
    setBusy(true); setErr("");
    try {
      await api(`/projects/${project.id}/reviews/${current.roundId}/cancel`, { method: "POST", body: {} });
      await load();
    } catch (ex) {
      setErr(ex.message || "검수 라운드를 취소하지 못했어요.");
    } finally { setBusy(false); }
  }, [project.id, current, load]);

  /* 모달 셸 — 오른쪽 시트(코멘트 패널과 동일 톤). */
  const shell = (children) => React.createElement("div", {
    style: { position: "fixed", inset: 0, background: "rgba(0,0,0,0.42)", display: "flex", justifyContent: "flex-end", zIndex: 60 },
    onMouseDown: (e) => { if (e.target === e.currentTarget && !busy) onClose(); },
  },
    React.createElement("div", { style: { width: "min(560px, 100%)", height: "100%", background: "var(--bg-canvas)", borderLeft: "1px solid var(--border)", boxShadow: "var(--sh-2)", display: "flex", flexDirection: "column" } },
      React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10, padding: "16px 20px", borderBottom: "1px solid var(--border)", background: "var(--bg-normal)" } },
        React.createElement("svg", { width: 19, height: 19, viewBox: "0 0 24 24", fill: "none", stroke: "var(--primary)", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" }, React.createElement("path", { d: "M9 11l3 3L22 4" }), React.createElement("path", { d: "M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" })),
        React.createElement("div", { className: "t-headline-1", style: { color: "var(--label-normal)" } }, "검수"),
        React.createElement("button", { onClick: onClose, "aria-label": "닫기", style: { ...btnStyle("ghost", "sm"), marginLeft: "auto", padding: "0 8px" } },
          React.createElement("svg", { width: 17, height: 17, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.2, strokeLinecap: "round" }, React.createElement("path", { d: "M18 6 6 18M6 6l12 12" })))),
      React.createElement("div", { style: { flex: 1, overflowY: "auto", padding: "18px 20px 28px" } }, children)));

  if (rounds === null) {
    return shell(React.createElement(StateMsg, { icon: React.createElement(Spinner, { s: 22 }), title: "불러오는 중이에요" }));
  }

  const errEl = err
    ? React.createElement("div", { className: "t-label-2", style: { color: "var(--negative)", background: "var(--negative-bg)", borderRadius: "var(--r-sm)", padding: "9px 13px", marginBottom: 14 } }, err)
    : null;

  /* 현재 라운드 상세 카드. */
  let currentEl;
  if (current) {
    const approvedStages = new Set((current.currentApprovals || []).map((a) => a.stage));
    const stage = current.currentStage;       // SME1|design|SME2|pm|done
    const stageMeta = reviewStageMeta(stage);
    const canAct = stage !== "done" && current.status === "open" && reviewCanActOnStage(stage, role, myPersona);
    const blockedReason = (() => {
      if (stage === "done" || current.status !== "open") return null;
      if (role === "owner") return null;
      if (role !== "reviewer") return "검수 결정은 검수자(reviewer)만 할 수 있어요.";
      const need = stageMeta.persona;
      if (!myPersona) return "검수 권한(페르소나)이 지정되지 않았어요. 소유자에게 요청해 주세요.";
      if (myPersona !== need) return `이 단계는 ${REVIEW_PERSONA_LABEL[need] || need} 페르소나만 검수할 수 있어요.`;
      return null;
    })();
    /* 마지막 반려 사유(있으면) — 가장 최근 reject action. */
    const actions = Array.isArray(current.actions) ? current.actions : [];
    const lastReject = [...actions].reverse().find((a) => a.decision === "reject");

    currentEl = React.createElement("div", { style: { border: "1px solid var(--border)", borderRadius: "var(--r-lg)", background: "var(--bg-normal)", padding: "18px 18px 16px", marginBottom: 22 } },
      React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 16 } },
        React.createElement("span", { className: "t-title-3", style: { color: "var(--label-normal)" } }, `검수 ${current.roundNo}차`),
        React.createElement(Pill, { color: "var(--label-alt)", bg: "var(--bg-alt)" }, `시도 ${current.attemptNo}`),
        current.status === "open"
          ? React.createElement(Pill, { color: "var(--primary)", bg: "var(--primary-light)" }, "진행 중")
          : React.createElement(Pill, { color: "var(--positive)", bg: "var(--positive-bg)" }, "승인 완료")),

      React.createElement("div", { style: { marginBottom: 16 } }, React.createElement(ReviewStepper, { approvedStages, currentStage: stage, status: current.status })),

      /* 직전 반려 사유 안내(이번 시도가 반려 후 재검수일 때 맥락 제공) */
      lastReject && current.attemptNo > 1
        ? React.createElement("div", { style: { display: "flex", gap: 7, alignItems: "flex-start", background: "var(--negative-bg)", borderRadius: "var(--r-sm)", padding: "9px 12px", marginBottom: 14 } },
            React.createElement("svg", { width: 15, height: 15, viewBox: "0 0 24 24", fill: "none", stroke: "var(--negative)", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", style: { flexShrink: 0, marginTop: 1 } }, React.createElement("path", { d: "M18 6 6 18M6 6l12 12" })),
            React.createElement("div", { className: "t-caption-1", style: { color: "var(--label-neutral)", lineHeight: 1.5 } },
              React.createElement("b", { style: { color: "var(--negative)" } }, "직전 반려: "),
              `${reviewStageMeta(lastReject.stage).label} · ${(lastReject.actor && lastReject.actor.name) || "알 수 없음"}`,
              lastReject.reason ? React.createElement("div", { style: { marginTop: 2 } }, lastReject.reason) : null))
        : null,

      /* 결정 영역 — 현재 단계 승인/반려. 권한 없으면 안내. */
      stage === "done" || current.status === "approved"
        ? React.createElement("div", { className: "t-body-2", style: { color: "var(--positive)", textAlign: "center", padding: "8px 0" } }, "모든 단계 검수를 통과했어요.")
        : React.createElement("div", null,
            React.createElement("div", { className: "t-label-2", style: { color: "var(--label-alt)", marginBottom: 10 } },
              `현재 단계: `, React.createElement("b", { style: { color: stageMeta.color } }, stageMeta.label)),
            canAct
              ? React.createElement("div", { style: { display: "flex", gap: 8 } },
                  React.createElement(Button, { kind: "primary", size: "md", disabled: busy, onClick: () => decide("approve"), style: { flex: 1 } },
                    busy ? React.createElement(Spinner, { s: 15, color: "#fff" }) : "승인"),
                  React.createElement(Button, { kind: "danger", size: "md", disabled: busy, onClick: () => setRejecting({ stageKey: stage }), style: { flex: 1 } }, "반려"))
              : React.createElement("div", { className: "t-caption-1", style: { color: "var(--label-alt)", background: "var(--bg-alt)", borderRadius: "var(--r-sm)", padding: "10px 12px", lineHeight: 1.5 } },
                  blockedReason || "이 단계를 검수할 권한이 없어요."),

            /* owner 취소(진행 중 라운드 종료) */
            isOwner && current.status === "open"
              ? React.createElement("div", { style: { marginTop: 12, textAlign: "right" } },
                  React.createElement("button", { onClick: cancelRound, disabled: busy, style: { ...btnStyle("ghost", "sm"), color: "var(--label-alt)" } }, "이 라운드 취소"))
              : null));
  } else {
    /* 열린 라운드 없음 — 시작 안내(editor+). */
    currentEl = React.createElement("div", { style: { border: "1px dashed var(--border)", borderRadius: "var(--r-lg)", padding: "26px 18px", textAlign: "center", marginBottom: 22 } },
      React.createElement("div", { className: "t-headline-1", style: { color: "var(--label-normal)", marginBottom: 6 } }, "진행 중인 검수가 없어요"),
      React.createElement("p", { className: "t-body-2", style: { color: "var(--label-alt)", margin: "0 0 16px", lineHeight: 1.6 } },
        "검수를 시작하면 SME 1차 → 설계 → SME 2차 → PM 순으로 승인을 받아요. 단계 중 반려되면 SME 1차로 돌아가 다시 검수해요."),
      canStart
        ? React.createElement(Button, { kind: "primary", size: "md", disabled: busy, onClick: startRound },
            busy ? React.createElement(Spinner, { s: 15, color: "#fff" }) : "검수 시작")
        : React.createElement("div", { className: "t-caption-1", style: { color: "var(--label-assist)" } }, "검수는 편집자 이상이 시작할 수 있어요."));
  }

  return shell(React.createElement("div", null,
    errEl,
    currentEl,
    React.createElement("div", { className: "t-label-1", style: { color: "var(--label-neutral)", margin: "4px 0 12px", fontWeight: 800 } }, "검수 차수 기록"),
    React.createElement(RoundHistory, { rounds }),
    rejecting
      ? React.createElement(ReviewRejectModal, {
          stageLabel: reviewStageMeta(rejecting.stageKey).label,
          busy,
          onCancel: () => setRejecting(null),
          onConfirm: (reason) => decide("reject", reason),
        })
      : null));
}

Object.assign(window, {
  ReviewBoard, RoundHistory, ReviewStepper, ReviewRejectModal,
  REVIEW_STAGES, REVIEW_STAGE_ORDER, reviewStageMeta, reviewCanActOnStage, reviewAgo,
});
