#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
#![allow(clippy::multiple_crate_versions)]
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::path::{Component, Path, PathBuf};
pub const CODE_REVIEW_SERVICE_INTERFACE_ID: &str = "bcode.code_review/v1";
pub const REVIEW_PUBLISHER_INTERFACE_ID: &str = "bcode.review_publisher/v1";
pub const REVIEW_IMPORTER_INTERFACE_ID: &str = "bcode.review_importer/v1";
pub const OP_REVIEW_BUNDLE_GET: &str = "review.bundle.get";
pub const OP_REVIEW_PUBLISHERS_LIST: &str = "review.publishers.list";
pub const OP_REVIEW_PUBLISH_PREVIEW: &str = "review.publish.preview";
pub const OP_REVIEW_PUBLISH_SUBMIT: &str = "review.publish.submit";
pub const OP_REVIEW_PUBLISH_RECORD_SAVE: &str = "review.publish.record.save";
pub const OP_REVIEW_PUBLISH_RECORDS_LIST: &str = "review.publish.records.list";
pub const OP_REVIEW_WORKSPACE_LIST: &str = "review.workspace.list";
pub const OP_REVIEW_WORKSPACE_CREATE: &str = "review.workspace.create";
pub const OP_REVIEW_WORKSPACE_GET: &str = "review.workspace.get";
pub const OP_REVIEW_WORKSPACE_UPDATE: &str = "review.workspace.update";
pub const OP_REVIEW_WORKSPACE_ARCHIVE: &str = "review.workspace.archive";
pub const OP_REVIEW_WORKSPACE_MATERIALIZE: &str = "review.workspace.materialize";
pub const OP_REVIEW_REPO_FILE_GET: &str = "review.repo.file.get";
pub const OP_REVIEW_SUGGESTIONS_LIST: &str = "review.suggestions.list";
pub const OP_REVIEW_SUGGESTION_SAVE: &str = "review.suggestion.save";
pub const OP_REVIEW_AI_EXCHANGES_LIST: &str = "review.ai_exchanges.list";
pub const OP_REVIEW_AI_EXCHANGE_SAVE: &str = "review.ai_exchange.save";
pub const OP_REVIEW_AI_EXCHANGE_DELETE: &str = "review.ai_exchange.delete";
pub const REVIEW_AI_EXCHANGE_SCHEMA_VERSION: u16 = 1;
pub const OP_REVIEW_EXTERNAL_IMPORT_SAVE: &str = "review.external_import.save";
pub const OP_REVIEW_EXTERNAL_IMPORTS_LIST: &str = "review.external_imports.list";
pub const OP_REVIEW_PUBLISHER_MANIFEST: &str = "review.publisher.manifest";
pub const OP_REVIEW_PUBLISHER_PREVIEW: &str = "review.publisher.preview";
pub const OP_REVIEW_PUBLISHER_SUBMIT: &str = "review.publisher.submit";
pub const OP_REVIEW_IMPORTER_MANIFEST: &str = "review.importer.manifest";
pub const OP_REVIEW_IMPORTER_IMPORT: &str = "review.importer.import";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ReviewTarget {
WorkingTreeUnstaged,
IndexStaged,
WorkingTreeAndIndex,
LastCommit,
CommitRange {
base: String,
head: String,
#[serde(default)]
merge_base: bool,
},
BranchCompare {
base_branch: String,
head_branch: String,
#[serde(default = "default_true")]
merge_base: bool,
},
Repository,
}
pub const REVIEW_WORKSPACE_PRESENTATION_SCHEMA_VERSION: u16 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReviewWorkspacePresentationState {
pub schema_version: u16,
#[serde(default)]
pub selected_path: Option<String>,
#[serde(default)]
pub selected_thread_key: Option<String>,
#[serde(default)]
pub selected_line: Option<u32>,
#[serde(default)]
pub sidebar_mode: String,
#[serde(default = "default_true")]
pub sidebar_visible: bool,
#[serde(default)]
pub thread_filter: String,
#[serde(default = "default_true")]
pub show_resolved_threads: bool,
#[serde(default)]
pub hide_viewed_files: bool,
#[serde(default)]
pub collapsed_thread_keys: BTreeSet<String>,
#[serde(default)]
pub expanded_agent_answer_keys: BTreeSet<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReviewWorkspace {
pub id: String,
pub title: String,
pub repo_root: PathBuf,
pub sources: Vec<ReviewSource>,
#[serde(default)]
pub created_at_ms: Option<u64>,
#[serde(default)]
pub updated_at_ms: Option<u64>,
#[serde(default)]
pub viewed_files: BTreeSet<String>,
#[serde(default)]
pub archived_at_ms: Option<u64>,
#[serde(default)]
pub presentation_state: Option<ReviewWorkspacePresentationState>,
}
impl ReviewWorkspace {
#[must_use]
pub fn from_target(repo_root: PathBuf, target: ReviewTarget) -> Self {
let source_kind = ReviewSourceKind::from(target);
let title = source_kind.label();
Self {
id: "transient-review-workspace".to_string(),
title: title.clone(),
repo_root,
sources: vec![ReviewSource {
id: "source-1".to_string(),
kind: source_kind,
label: title,
included: true,
}],
created_at_ms: None,
updated_at_ms: None,
viewed_files: BTreeSet::new(),
archived_at_ms: None,
presentation_state: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReviewSource {
pub id: String,
pub kind: ReviewSourceKind,
pub label: String,
pub included: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ReviewSourceKind {
WorkingTreeUnstaged,
IndexStaged,
WorkingTreeAndIndex,
LastCommit,
Commit {
rev: String,
},
CommitRange {
base: String,
head: String,
#[serde(default)]
merge_base: bool,
},
BranchCompare {
base_branch: String,
head_branch: String,
#[serde(default = "default_true")]
merge_base: bool,
},
File {
path: String,
},
FileRange {
path: String,
start: u32,
end: u32,
},
Repository,
}
impl ReviewSourceKind {
#[must_use]
pub fn label(&self) -> String {
match self {
Self::WorkingTreeUnstaged => "Unstaged changes".to_string(),
Self::IndexStaged => "Staged changes".to_string(),
Self::WorkingTreeAndIndex => "Working tree and index".to_string(),
Self::LastCommit => "Last commit".to_string(),
Self::Commit { rev } => format!("Commit {rev}"),
Self::CommitRange {
base,
head,
merge_base,
} => {
let separator = if *merge_base { "..." } else { ".." };
format!("Range {base}{separator}{head}")
}
Self::BranchCompare {
base_branch,
head_branch,
merge_base,
} => {
let separator = if *merge_base { "..." } else { ".." };
format!("Compare {base_branch}{separator}{head_branch}")
}
Self::File { path } => format!("File {path}"),
Self::FileRange { path, start, end } => format!("File {path}:{start}-{end}"),
Self::Repository => "Repository browser".to_string(),
}
}
}
impl From<ReviewTarget> for ReviewSourceKind {
fn from(target: ReviewTarget) -> Self {
match target {
ReviewTarget::WorkingTreeUnstaged => Self::WorkingTreeUnstaged,
ReviewTarget::IndexStaged => Self::IndexStaged,
ReviewTarget::WorkingTreeAndIndex => Self::WorkingTreeAndIndex,
ReviewTarget::LastCommit => Self::LastCommit,
ReviewTarget::CommitRange {
base,
head,
merge_base,
} => Self::CommitRange {
base,
head,
merge_base,
},
ReviewTarget::BranchCompare {
base_branch,
head_branch,
merge_base,
} => Self::BranchCompare {
base_branch,
head_branch,
merge_base,
},
ReviewTarget::Repository => Self::Repository,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReviewSurfaceKind {
Diff,
File,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReviewSurface {
pub id: String,
pub source_id: String,
pub path: String,
pub kind: ReviewSurfaceKind,
#[serde(default)]
pub file: Option<ReviewFile>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReviewSourceDiagnosticSeverity {
Info,
Warning,
Error,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReviewSourceDiagnostic {
pub source_id: String,
pub severity: ReviewSourceDiagnosticSeverity,
pub code: String,
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReviewRepositoryCommit {
pub rev: String,
pub short_rev: String,
pub subject: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReviewWorkspaceMaterialization {
pub workspace: ReviewWorkspace,
pub surfaces: Vec<ReviewSurface>,
#[serde(default)]
pub diagnostics: Vec<ReviewSourceDiagnostic>,
#[serde(default)]
pub repository_files: Vec<String>,
#[serde(default)]
pub repository_branches: Vec<String>,
#[serde(default)]
pub repository_commits: Vec<ReviewRepositoryCommit>,
pub additions: u32,
pub deletions: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MaterializeReviewWorkspaceRequest {
pub repo_path: PathBuf,
pub workspace: ReviewWorkspace,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MaterializeReviewWorkspaceResponse {
pub materialization: ReviewWorkspaceMaterialization,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ListPublishRecordsRequest {
pub repo_path: PathBuf,
pub workspace_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ListPublishRecordsResponse {
pub records: Vec<ReviewPublishRecord>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ListReviewWorkspacesRequest {
pub repo_path: PathBuf,
#[serde(default)]
pub include_archived: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReviewPublishRecord {
pub id: String,
#[serde(default)]
pub workspace_id: Option<String>,
pub review_id: String,
pub publisher_id: String,
pub submitted: bool,
#[serde(default)]
pub published_thread_anchors: Vec<DraftAnchor>,
#[serde(default)]
pub output: Option<String>,
pub message: String,
pub created_at_ms: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReviewWorkspaceListItem {
pub workspace: ReviewWorkspace,
#[serde(default)]
pub thread_count: usize,
#[serde(default)]
pub draft_count: usize,
#[serde(default)]
pub last_publish: Option<ReviewPublishRecord>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ListReviewWorkspacesResponse {
pub workspaces: Vec<ReviewWorkspace>,
#[serde(default)]
pub items: Vec<ReviewWorkspaceListItem>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreateReviewWorkspaceRequest {
pub repo_path: PathBuf,
#[serde(default)]
pub title: Option<String>,
#[serde(default)]
pub sources: Vec<ReviewSource>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreateReviewWorkspaceResponse {
pub workspace: ReviewWorkspace,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GetReviewWorkspaceRequest {
pub repo_path: PathBuf,
pub workspace_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GetReviewWorkspaceResponse {
pub workspace: Option<ReviewWorkspace>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UpdateReviewWorkspaceRequest {
pub repo_path: PathBuf,
pub workspace: ReviewWorkspace,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UpdateReviewWorkspaceResponse {
pub workspace: ReviewWorkspace,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArchiveReviewWorkspaceRequest {
pub repo_path: PathBuf,
pub workspace_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArchiveReviewWorkspaceResponse {
pub archived: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ReviewScope {
Target {
target: ReviewTarget,
},
Workspace {
workspace_id: String,
target: ReviewTarget,
},
}
impl ReviewScope {
#[must_use]
pub const fn target(&self) -> &ReviewTarget {
match self {
Self::Target { target } | Self::Workspace { target, .. } => target,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ListDraftsRequest {
pub repo_path: PathBuf,
pub target: ReviewTarget,
#[serde(default)]
pub scope: Option<ReviewScope>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SaveDraftRequest {
pub repo_path: PathBuf,
pub target: ReviewTarget,
#[serde(default)]
pub scope: Option<ReviewScope>,
pub anchor: DraftAnchor,
pub body: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeleteDraftRequest {
pub repo_path: PathBuf,
pub target: ReviewTarget,
#[serde(default)]
pub scope: Option<ReviewScope>,
pub comment_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UpdateDraftRequest {
pub repo_path: PathBuf,
pub target: ReviewTarget,
#[serde(default)]
pub scope: Option<ReviewScope>,
pub comment_id: String,
pub body: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LinkThreadSessionRequest {
pub repo_path: PathBuf,
pub target: ReviewTarget,
#[serde(default)]
pub scope: Option<ReviewScope>,
pub anchor: DraftAnchor,
pub session_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GetReviewThreadRequest {
pub repo_path: PathBuf,
pub target: ReviewTarget,
#[serde(default)]
pub scope: Option<ReviewScope>,
pub thread_id: Option<String>,
pub anchor: Option<DraftAnchor>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ListReviewAiExchangesRequest {
pub repo_path: PathBuf,
pub target: ReviewTarget,
#[serde(default)]
pub scope: Option<ReviewScope>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeleteReviewAiExchangeRequest {
pub repo_path: PathBuf,
pub target: ReviewTarget,
#[serde(default)]
pub scope: Option<ReviewScope>,
pub exchange_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeleteReviewAiExchangeResponse {
pub deleted: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SaveReviewAiExchangeRequest {
pub repo_path: PathBuf,
pub target: ReviewTarget,
#[serde(default)]
pub scope: Option<ReviewScope>,
pub exchange: ReviewAiExchange,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ListReviewAiExchangesResponse {
pub exchanges: Vec<ReviewAiExchange>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SaveReviewAiExchangeResponse {
pub exchange: ReviewAiExchange,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReviewAiExchangeStatus {
Pending,
Linked,
Failed,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReviewAiExchange {
pub schema_version: u16,
pub exchange_id: String,
pub anchor: DraftAnchor,
pub question: String,
#[serde(default)]
pub session_id: Option<String>,
pub status: ReviewAiExchangeStatus,
#[serde(default)]
pub error: Option<String>,
pub created_at_ms: u64,
pub updated_at_ms: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ListReviewSuggestionsRequest {
pub repo_path: PathBuf,
pub target: ReviewTarget,
#[serde(default)]
pub scope: Option<ReviewScope>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SaveReviewSuggestionRequest {
pub repo_path: PathBuf,
pub target: ReviewTarget,
#[serde(default)]
pub scope: Option<ReviewScope>,
pub suggestion: ReviewSuggestion,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ListReviewSuggestionsResponse {
pub suggestions: Vec<ReviewSuggestion>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SaveReviewSuggestionResponse {
pub suggestion: ReviewSuggestion,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReviewSuggestionStatus {
Suggested,
Refining,
Accepted,
Rejected,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReviewSuggestion {
pub suggestion_id: String,
pub anchor: DraftAnchor,
pub body: String,
#[serde(default)]
pub session_id: Option<String>,
pub status: ReviewSuggestionStatus,
#[serde(default)]
pub rationale: Option<String>,
pub created_at_ms: u64,
pub updated_at_ms: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GetReviewDiffRequest {
pub repo_path: PathBuf,
pub target: ReviewTarget,
pub file_path: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ListDraftsResponse {
pub drafts: Vec<DraftComment>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SaveDraftResponse {
pub draft: DraftComment,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeleteDraftResponse {
pub deleted: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UpdateDraftResponse {
pub updated: bool,
pub updated_at_ms: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LinkThreadSessionResponse {
pub thread_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResolveThreadRequest {
pub repo_path: PathBuf,
pub target: ReviewTarget,
#[serde(default)]
pub scope: Option<ReviewScope>,
pub anchor: DraftAnchor,
pub resolved: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResolveThreadResponse {
pub thread_id: String,
pub resolved_at_ms: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReviewContextRequest {
pub repo_path: PathBuf,
pub target: ReviewTarget,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ReviewAnchorKind {
Review,
File,
#[default]
Range,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DraftAnchor {
#[serde(default)]
pub kind: ReviewAnchorKind,
pub file_path: String,
pub diff_row: u64,
#[serde(default)]
pub start_diff_row: Option<u64>,
#[serde(default)]
pub end_diff_row: Option<u64>,
#[serde(default)]
pub old_start: Option<u32>,
#[serde(default)]
pub old_end: Option<u32>,
#[serde(default)]
pub new_start: Option<u32>,
#[serde(default)]
pub new_end: Option<u32>,
#[serde(default)]
pub old_line: Option<u32>,
#[serde(default)]
pub new_line: Option<u32>,
pub line_kind: ReviewLineKind,
#[serde(default)]
pub is_file_anchor: bool,
#[serde(default)]
pub surface_id: Option<String>,
#[serde(default)]
pub source_id: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ReviewThreadKind {
#[default]
Note,
Question,
Todo,
Finding,
Summary,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ReviewThreadSeverity {
#[default]
Info,
Nit,
Warning,
Blocker,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DraftComment {
pub comment_id: String,
pub thread_id: String,
pub anchor: DraftAnchor,
pub body: String,
pub created_at_ms: u64,
pub updated_at_ms: u64,
#[serde(default)]
pub session_id: Option<String>,
#[serde(default)]
pub resolved_at_ms: Option<u64>,
#[serde(default)]
pub thread_kind: ReviewThreadKind,
#[serde(default)]
pub severity: ReviewThreadSeverity,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReviewFileStatus {
Modified,
Added,
Deleted,
Renamed,
Copied,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReviewFileSummary {
pub path: String,
pub status: ReviewFileStatus,
pub additions: u32,
pub deletions: u32,
pub hunks: usize,
pub is_binary: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReviewLineKind {
Context,
Added,
Removed,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReviewHunk {
pub old_start: u32,
pub old_count: u32,
pub new_start: u32,
pub new_count: u32,
pub heading: Option<String>,
pub lines: Vec<ReviewLine>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReviewLine {
pub kind: ReviewLineKind,
pub old_line: Option<u32>,
pub new_line: Option<u32>,
pub content: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RepositoryFileRequest {
pub repo_path: PathBuf,
pub file_path: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RepositoryFileResponse {
pub file_path: String,
pub content: Option<String>,
pub size_bytes: u64,
#[serde(default)]
pub mtime_ms: Option<u64>,
pub is_binary: bool,
#[serde(default)]
pub unavailable_reason: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReviewFile {
pub old_path: Option<String>,
pub new_path: Option<String>,
pub status: ReviewFileStatus,
pub additions: u32,
pub deletions: u32,
pub hunks: Vec<ReviewHunk>,
pub is_binary: bool,
}
impl ReviewFile {
#[must_use]
pub fn display_path(&self) -> &str {
self.new_path
.as_deref()
.or(self.old_path.as_deref())
.unwrap_or("<unknown>")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReviewBundleLine {
pub file_path: String,
pub kind: ReviewLineKind,
pub old_line: Option<u32>,
pub new_line: Option<u32>,
pub diff_row: u64,
pub content: String,
#[serde(default)]
pub surface_id: Option<String>,
#[serde(default)]
pub source_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReviewBundle {
pub review_id: String,
pub title: String,
pub repo_root: PathBuf,
pub target: ReviewTarget,
pub files: Vec<ReviewFileSummary>,
#[serde(default)]
pub surfaces: Vec<ReviewSurface>,
pub threads: Vec<ReviewBundleThread>,
pub generated_at_ms: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReviewBundleThread {
pub thread_id: String,
pub anchor: DraftAnchor,
pub comments: Vec<DraftComment>,
pub session_id: Option<String>,
#[serde(default)]
pub resolved_at_ms: Option<u64>,
#[serde(default)]
pub selected_lines: Vec<ReviewBundleLine>,
pub selected_diff_lines: Vec<String>,
pub hunk_context: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[allow(clippy::struct_excessive_bools)]
pub struct ReviewPublisherCapabilities {
pub preview: bool,
pub submit: bool,
pub update_existing: bool,
pub supports_threads: bool,
pub supports_ranges: bool,
pub supports_inline_comments: bool,
pub supports_summary_comment: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReviewPublisherRoute {
pub plugin_id: String,
pub interface_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReviewPublisherManifest {
pub id: String,
pub label: String,
pub description: String,
pub capabilities: ReviewPublisherCapabilities,
pub options_schema: serde_json::Value,
#[serde(default)]
pub route: Option<ReviewPublisherRoute>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ListReviewPublishersResponse {
pub publishers: Vec<ReviewPublisherManifest>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PublishReviewRequest {
pub repo_path: PathBuf,
pub target: ReviewTarget,
#[serde(default)]
pub workspace: Option<ReviewWorkspace>,
#[serde(default)]
pub excluded_thread_anchors: Vec<DraftAnchor>,
pub publisher_id: String,
#[serde(default)]
pub options: serde_json::Value,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SavePublishRecordRequest {
pub workspace: ReviewWorkspace,
pub review_id: String,
pub publisher_id: String,
pub submitted: bool,
#[serde(default)]
pub published_thread_anchors: Vec<DraftAnchor>,
#[serde(default)]
pub output: Option<String>,
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SavePublishRecordResponse {
pub record: ReviewPublishRecord,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExternalPublishReviewRequest {
pub bundle: ReviewBundle,
#[serde(default)]
pub options: serde_json::Value,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PublishReviewPreviewResponse {
pub publisher_id: String,
pub preview: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExternalReviewThreadState {
Open,
Resolved,
Outdated,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExternalAnchorMappingStatus {
Exact,
Approximate,
Unmappable,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExternalReviewIdentity {
pub provider_id: String,
pub external_id: String,
#[serde(default)]
pub url: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExternalReviewAnchorMapping {
pub status: ExternalAnchorMappingStatus,
#[serde(default)]
pub anchor: Option<DraftAnchor>,
#[serde(default)]
pub message: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExternalReviewComment {
pub identity: ExternalReviewIdentity,
#[serde(default)]
pub author: Option<ExternalReviewIdentity>,
#[serde(default)]
pub author_label: Option<String>,
pub body: String,
#[serde(default)]
pub created_at_ms: Option<u64>,
#[serde(default)]
pub updated_at_ms: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExternalReviewThread {
pub identity: ExternalReviewIdentity,
pub state: ExternalReviewThreadState,
pub mapping: ExternalReviewAnchorMapping,
pub comments: Vec<ExternalReviewComment>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReviewImporterManifest {
pub id: String,
pub label: String,
pub description: String,
pub options_schema: serde_json::Value,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExternalReviewImportRequest {
pub repo_root: PathBuf,
#[serde(default)]
pub files: Vec<ReviewFile>,
#[serde(default)]
pub options: serde_json::Value,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExternalReviewImportResponse {
pub importer_id: String,
pub threads: Vec<ExternalReviewThread>,
#[serde(default)]
pub warnings: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExternalReviewImportRecord {
pub workspace_id: String,
pub importer_id: String,
pub threads: Vec<ExternalReviewThread>,
#[serde(default)]
pub warnings: Vec<String>,
pub updated_at_ms: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SaveExternalReviewImportRequest {
pub repo_path: PathBuf,
pub workspace_id: String,
pub import: ExternalReviewImportResponse,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SaveExternalReviewImportResponse {
pub record: ExternalReviewImportRecord,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ListExternalReviewImportsRequest {
pub repo_path: PathBuf,
pub workspace_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ListExternalReviewImportsResponse {
pub records: Vec<ExternalReviewImportRecord>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PublishReviewResponse {
pub publisher_id: String,
pub submitted: bool,
pub output: Option<String>,
pub message: String,
}
const fn default_true() -> bool {
true
}
pub const ADVERSARIAL_REVIEW_CONTRACT_VERSION: u32 = 1;
pub const MAX_ADVERSARIAL_REVIEWERS: usize = 32;
pub const MAX_ADVERSARIAL_REVIEW_FINDINGS: usize = 512;
pub const MAX_ADVERSARIAL_REVIEW_TEXT_BYTES: usize = 16_384;
pub const MAX_ADVERSARIAL_REVIEW_FACTS: usize = 64;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AdversarialReviewValidationError {
pub path: String,
pub message: String,
}
impl fmt::Display for AdversarialReviewValidationError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"invalid adversarial review at '{}': {}",
self.path, self.message
)
}
}
impl std::error::Error for AdversarialReviewValidationError {}
fn adversarial_review_error(
path: impl Into<String>,
message: impl Into<String>,
) -> AdversarialReviewValidationError {
AdversarialReviewValidationError {
path: path.into(),
message: message.into(),
}
}
fn validate_review_id(path: &str, value: &str) -> Result<(), AdversarialReviewValidationError> {
if value.is_empty()
|| value.len() > 256
|| !value.bytes().all(|byte| {
byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'/' | b'_' | b'-' | b':')
})
{
return Err(adversarial_review_error(
path,
"identity must contain 1..=256 safe ASCII bytes",
));
}
Ok(())
}
fn validate_sha256(path: &str, value: &str) -> Result<(), AdversarialReviewValidationError> {
if value.len() != 64
|| !value
.bytes()
.all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
{
return Err(adversarial_review_error(
path,
"digest must be 64 lowercase hexadecimal bytes",
));
}
Ok(())
}
fn validate_review_path(path: &str, value: &str) -> Result<(), AdversarialReviewValidationError> {
let candidate = Path::new(value);
if value.is_empty()
|| value.len() > 4_096
|| candidate.is_absolute()
|| candidate
.components()
.any(|component| !matches!(component, Component::Normal(_)))
{
return Err(adversarial_review_error(
path,
"path must be a bounded confined repository-relative path",
));
}
Ok(())
}
fn validate_review_text(path: &str, value: &str) -> Result<(), AdversarialReviewValidationError> {
if value.trim().is_empty() || value.len() > MAX_ADVERSARIAL_REVIEW_TEXT_BYTES {
return Err(adversarial_review_error(
path,
format!("text must contain 1..={MAX_ADVERSARIAL_REVIEW_TEXT_BYTES} UTF-8 bytes"),
));
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReviewRepositorySnapshot {
pub version: u32,
pub repository_identity_sha256: String,
pub head_object_id: String,
pub aggregate_sha256: String,
pub project_instruction_fingerprint_sha256: String,
}
impl ReviewRepositorySnapshot {
pub fn validate(&self) -> Result<(), AdversarialReviewValidationError> {
if self.version != ADVERSARIAL_REVIEW_CONTRACT_VERSION {
return Err(adversarial_review_error(
"snapshot.version",
"unsupported version",
));
}
validate_sha256(
"snapshot.repository_identity_sha256",
&self.repository_identity_sha256,
)?;
validate_review_id("snapshot.head_object_id", &self.head_object_id)?;
validate_sha256("snapshot.aggregate_sha256", &self.aggregate_sha256)?;
validate_sha256(
"snapshot.project_instruction_fingerprint_sha256",
&self.project_instruction_fingerprint_sha256,
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReviewAssignment {
pub version: u32,
pub assignment_id: String,
pub reviewer_id: String,
pub snapshot: ReviewRepositorySnapshot,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub include_paths: Vec<String>,
pub policy_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReviewPanelAssignmentRequest {
pub version: u32,
pub round_id: String,
pub snapshot: ReviewRepositorySnapshot,
pub reviewer_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub include_paths: Vec<String>,
pub policy_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReviewPanelAssignments {
pub version: u32,
pub round_id: String,
pub snapshot_sha256: String,
pub assignments: Vec<ReviewAssignment>,
}
impl ReviewPanelAssignmentRequest {
pub fn materialize(&self) -> Result<ReviewPanelAssignments, AdversarialReviewValidationError> {
if self.version != ADVERSARIAL_REVIEW_CONTRACT_VERSION {
return Err(adversarial_review_error(
"panel_assignment.version",
"unsupported version",
));
}
validate_review_id("panel_assignment.round_id", &self.round_id)?;
validate_review_id("panel_assignment.policy_id", &self.policy_id)?;
self.snapshot.validate()?;
if self.reviewer_ids.len() < 2 || self.reviewer_ids.len() > MAX_ADVERSARIAL_REVIEWERS {
return Err(adversarial_review_error(
"panel_assignment.reviewer_ids",
format!("panel requires 2..={MAX_ADVERSARIAL_REVIEWERS} reviewers"),
));
}
if self.include_paths.len() > MAX_ADVERSARIAL_REVIEW_FINDINGS {
return Err(adversarial_review_error(
"panel_assignment.include_paths",
"selected paths exceed the review bound",
));
}
let mut previous = None;
for reviewer_id in &self.reviewer_ids {
validate_review_id("panel_assignment.reviewer_ids", reviewer_id)?;
if previous.is_some_and(|identity: &str| identity >= reviewer_id.as_str()) {
return Err(adversarial_review_error(
"panel_assignment.reviewer_ids",
"reviewer identities must be strictly ordered and unique",
));
}
previous = Some(reviewer_id.as_str());
}
let mut paths = BTreeSet::new();
for path in &self.include_paths {
validate_review_path("panel_assignment.include_paths", path)?;
if !paths.insert(path) {
return Err(adversarial_review_error(
"panel_assignment.include_paths",
"selected paths must be unique",
));
}
}
let assignments = self
.reviewer_ids
.iter()
.map(|reviewer_id| ReviewAssignment {
version: ADVERSARIAL_REVIEW_CONTRACT_VERSION,
assignment_id: format!("{}:{reviewer_id}", self.round_id),
reviewer_id: reviewer_id.clone(),
snapshot: self.snapshot.clone(),
include_paths: self.include_paths.clone(),
policy_id: self.policy_id.clone(),
})
.collect::<Vec<_>>();
for assignment in &assignments {
assignment.validate()?;
}
Ok(ReviewPanelAssignments {
version: ADVERSARIAL_REVIEW_CONTRACT_VERSION,
round_id: self.round_id.clone(),
snapshot_sha256: self.snapshot.aggregate_sha256.clone(),
assignments,
})
}
}
impl ReviewAssignment {
pub fn validate(&self) -> Result<(), AdversarialReviewValidationError> {
if self.version != ADVERSARIAL_REVIEW_CONTRACT_VERSION {
return Err(adversarial_review_error(
"assignment.version",
"unsupported version",
));
}
validate_review_id("assignment.assignment_id", &self.assignment_id)?;
validate_review_id("assignment.reviewer_id", &self.reviewer_id)?;
validate_review_id("assignment.policy_id", &self.policy_id)?;
self.snapshot.validate()?;
if self.include_paths.len() > MAX_ADVERSARIAL_REVIEW_FINDINGS {
return Err(adversarial_review_error(
"assignment.include_paths",
"selected paths exceed the review bound",
));
}
let mut paths = BTreeSet::new();
for path in &self.include_paths {
validate_review_path("assignment.include_paths", path)?;
if !paths.insert(path) {
return Err(adversarial_review_error(
"assignment.include_paths",
"selected paths must be unique",
));
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReviewFindingSeverity {
Advisory,
Minor,
Major,
Critical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReviewFindingDisposition {
Required,
Advisory,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReviewSourceRange {
pub path: String,
pub start_line: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub start_column: Option<u32>,
pub end_line: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub end_column: Option<u32>,
}
impl ReviewSourceRange {
fn validate(&self) -> Result<(), AdversarialReviewValidationError> {
validate_review_path("finding.location.path", &self.path)?;
if self.start_line == 0
|| self.end_line < self.start_line
|| self.start_column == Some(0)
|| self.end_column == Some(0)
|| (self.start_line == self.end_line
&& self
.start_column
.zip(self.end_column)
.is_some_and(|(start, end)| end < start))
{
return Err(adversarial_review_error(
"finding.location",
"source range is malformed or reversed",
));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReviewFinding {
pub version: u32,
pub finding_id: String,
pub snapshot_sha256: String,
pub severity: ReviewFindingSeverity,
pub disposition: ReviewFindingDisposition,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub location: Option<ReviewSourceRange>,
pub title: String,
pub evidence: String,
pub remediation: String,
}
impl ReviewFinding {
pub fn validate(&self) -> Result<(), AdversarialReviewValidationError> {
if self.version != ADVERSARIAL_REVIEW_CONTRACT_VERSION {
return Err(adversarial_review_error(
"finding.version",
"unsupported version",
));
}
validate_review_id("finding.finding_id", &self.finding_id)?;
validate_sha256("finding.snapshot_sha256", &self.snapshot_sha256)?;
if let Some(location) = &self.location {
location.validate()?;
}
validate_review_text("finding.title", &self.title)?;
validate_review_text("finding.evidence", &self.evidence)?;
validate_review_text("finding.remediation", &self.remediation)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReviewExecutionFacts {
pub provider_id: String,
pub model_id: String,
pub agent_profile: String,
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
pub tools: BTreeSet<String>,
}
impl ReviewExecutionFacts {
fn validate(&self) -> Result<(), AdversarialReviewValidationError> {
validate_review_id("report.execution.provider_id", &self.provider_id)?;
validate_review_id("report.execution.model_id", &self.model_id)?;
validate_review_id("report.execution.agent_profile", &self.agent_profile)?;
if self.tools.len() > MAX_ADVERSARIAL_REVIEW_FACTS {
return Err(adversarial_review_error(
"report.execution.tools",
"tool facts exceed the review bound",
));
}
for tool in &self.tools {
validate_review_id("report.execution.tools", tool)?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReviewReport {
pub version: u32,
pub assignment_id: String,
pub reviewer_id: String,
pub snapshot_sha256: String,
pub execution: ReviewExecutionFacts,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub findings: Vec<ReviewFinding>,
}
impl ReviewReport {
pub fn validate(&self) -> Result<(), AdversarialReviewValidationError> {
if self.version != ADVERSARIAL_REVIEW_CONTRACT_VERSION {
return Err(adversarial_review_error(
"report.version",
"unsupported version",
));
}
validate_review_id("report.assignment_id", &self.assignment_id)?;
validate_review_id("report.reviewer_id", &self.reviewer_id)?;
validate_sha256("report.snapshot_sha256", &self.snapshot_sha256)?;
self.execution.validate()?;
if self.findings.len() > MAX_ADVERSARIAL_REVIEW_FINDINGS {
return Err(adversarial_review_error(
"report.findings",
"findings exceed the review bound",
));
}
let mut findings = BTreeMap::new();
for finding in &self.findings {
finding.validate()?;
if finding.snapshot_sha256 != self.snapshot_sha256 {
return Err(adversarial_review_error(
"report.findings.snapshot_sha256",
"every finding must bind to the report snapshot",
));
}
if findings.insert(&finding.finding_id, finding).is_some() {
return Err(adversarial_review_error(
"report.findings.finding_id",
"finding identities must be unique",
));
}
}
Ok(())
}
pub fn validate_against_assignment(
&self,
assignment: &ReviewAssignment,
) -> Result<(), AdversarialReviewValidationError> {
self.validate()?;
assignment.validate()?;
if self.assignment_id != assignment.assignment_id
|| self.reviewer_id != assignment.reviewer_id
|| self.snapshot_sha256 != assignment.snapshot.aggregate_sha256
{
return Err(adversarial_review_error(
"report.assignment",
"report must exactly match its assignment, reviewer, and repository snapshot",
));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReviewPanelResult {
pub version: u32,
pub snapshot_sha256: String,
pub reports: Vec<ReviewReport>,
}
impl ReviewPanelResult {
pub fn validate(&self) -> Result<(), AdversarialReviewValidationError> {
if self.version != ADVERSARIAL_REVIEW_CONTRACT_VERSION {
return Err(adversarial_review_error(
"panel.version",
"unsupported version",
));
}
validate_sha256("panel.snapshot_sha256", &self.snapshot_sha256)?;
if self.reports.len() < 2 || self.reports.len() > MAX_ADVERSARIAL_REVIEWERS {
return Err(adversarial_review_error(
"panel.reports",
format!("panel requires 2..={MAX_ADVERSARIAL_REVIEWERS} reports"),
));
}
let mut previous = None;
let mut findings = BTreeMap::<&str, &ReviewFinding>::new();
for report in &self.reports {
report.validate()?;
if report.snapshot_sha256 != self.snapshot_sha256 {
return Err(adversarial_review_error(
"panel.reports.snapshot_sha256",
"every report must bind to the panel snapshot",
));
}
if previous.is_some_and(|identity: &str| identity >= report.reviewer_id.as_str()) {
return Err(adversarial_review_error(
"panel.reports.reviewer_id",
"reports must be strictly ordered by unique reviewer identity",
));
}
for finding in &report.findings {
if findings
.insert(&finding.finding_id, finding)
.is_some_and(|existing| existing != finding)
{
return Err(adversarial_review_error(
"panel.reports.findings.finding_id",
"duplicate finding identities must have identical normalized payloads",
));
}
}
previous = Some(report.reviewer_id.as_str());
}
Ok(())
}
fn findings_by_id(&self) -> BTreeMap<&str, &ReviewFinding> {
self.reports
.iter()
.flat_map(|report| &report.findings)
.map(|finding| (finding.finding_id.as_str(), finding))
.collect()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReviewConsensusDisposition {
Required,
Advisory,
Invalid,
Duplicate,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReviewConsensusDecision {
pub finding_id: String,
pub disposition: ReviewConsensusDisposition,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub finding: Option<ReviewFinding>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub duplicate_of: Option<String>,
pub rationale: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReviewConsensus {
pub version: u32,
pub snapshot_sha256: String,
pub decisions: Vec<ReviewConsensusDecision>,
}
impl ReviewConsensus {
pub fn validate(&self) -> Result<(), AdversarialReviewValidationError> {
if self.version != ADVERSARIAL_REVIEW_CONTRACT_VERSION {
return Err(adversarial_review_error(
"consensus.version",
"unsupported version",
));
}
validate_sha256("consensus.snapshot_sha256", &self.snapshot_sha256)?;
if self.decisions.len() > MAX_ADVERSARIAL_REVIEW_FINDINGS {
return Err(adversarial_review_error(
"consensus.decisions",
"decisions exceed the review bound",
));
}
let mut previous = None;
for decision in &self.decisions {
validate_review_id("consensus.decisions.finding_id", &decision.finding_id)?;
validate_review_text("consensus.decisions.rationale", &decision.rationale)?;
if previous.is_some_and(|identity: &str| identity >= decision.finding_id.as_str()) {
return Err(adversarial_review_error(
"consensus.decisions.finding_id",
"decisions must be strictly ordered by unique finding identity",
));
}
match decision.disposition {
ReviewConsensusDisposition::Required | ReviewConsensusDisposition::Advisory => {
let finding = decision.finding.as_ref().ok_or_else(|| {
adversarial_review_error(
"consensus.decisions.finding",
"retained decision requires the canonical finding",
)
})?;
finding.validate()?;
if finding.finding_id != decision.finding_id
|| finding.snapshot_sha256 != self.snapshot_sha256
|| decision.duplicate_of.is_some()
{
return Err(adversarial_review_error(
"consensus.decisions",
"retained decision payload is inconsistent",
));
}
}
ReviewConsensusDisposition::Duplicate => {
if decision.finding.is_some()
|| decision.duplicate_of.as_deref().is_none_or(|identity| {
identity == decision.finding_id
|| validate_review_id("duplicate_of", identity).is_err()
})
{
return Err(adversarial_review_error(
"consensus.decisions.duplicate_of",
"duplicate decision requires one different canonical finding identity",
));
}
}
ReviewConsensusDisposition::Invalid => {
if decision.finding.is_some() || decision.duplicate_of.is_some() {
return Err(adversarial_review_error(
"consensus.decisions",
"invalid decision cannot retain a finding or duplicate target",
));
}
}
}
previous = Some(decision.finding_id.as_str());
}
Ok(())
}
pub fn validate_against_panel(
&self,
panel: &ReviewPanelResult,
) -> Result<(), AdversarialReviewValidationError> {
self.validate()?;
panel.validate()?;
if self.snapshot_sha256 != panel.snapshot_sha256 {
return Err(adversarial_review_error(
"consensus.snapshot_sha256",
"consensus must bind to the panel snapshot",
));
}
let findings = panel.findings_by_id();
if self.decisions.len() != findings.len() {
return Err(adversarial_review_error(
"consensus.decisions",
"consensus must disposition every distinct panel finding exactly once",
));
}
let decisions = self
.decisions
.iter()
.map(|decision| (decision.finding_id.as_str(), decision))
.collect::<BTreeMap<_, _>>();
for (finding_id, finding) in findings {
let decision = decisions.get(finding_id).ok_or_else(|| {
adversarial_review_error(
"consensus.decisions",
"consensus omitted a submitted finding",
)
})?;
if finding.disposition == ReviewFindingDisposition::Required
&& !matches!(decision.disposition, ReviewConsensusDisposition::Required)
{
return Err(adversarial_review_error(
"consensus.decisions.disposition",
"required reviewer findings must remain required unless separately adjudicated with an explicit supported proof contract",
));
}
if matches!(
decision.disposition,
ReviewConsensusDisposition::Required | ReviewConsensusDisposition::Advisory
) && decision.finding.as_ref() != Some(finding)
{
return Err(adversarial_review_error(
"consensus.decisions.finding",
"retained consensus finding must equal the normalized panel finding",
));
}
if let Some(duplicate_of) = &decision.duplicate_of {
let Some(target) = decisions.get(duplicate_of.as_str()) else {
return Err(adversarial_review_error(
"consensus.decisions.duplicate_of",
"duplicate target must be another submitted finding",
));
};
if matches!(target.disposition, ReviewConsensusDisposition::Duplicate) {
return Err(adversarial_review_error(
"consensus.decisions.duplicate_of",
"duplicate chains are prohibited",
));
}
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "outcome", rename_all = "snake_case", deny_unknown_fields)]
pub enum ReviewRoundOutcome {
Clean {
version: u32,
snapshot_sha256: String,
},
RemediationRequired {
version: u32,
snapshot_sha256: String,
consensus: ReviewConsensus,
},
Exhausted {
version: u32,
snapshot_sha256: String,
rounds: u32,
consensus: ReviewConsensus,
},
Failed {
version: u32,
snapshot_sha256: String,
message: String,
},
Cancelled {
version: u32,
snapshot_sha256: String,
},
}
impl ReviewRoundOutcome {
pub fn validate(&self) -> Result<(), AdversarialReviewValidationError> {
let (version, snapshot) = match self {
Self::Clean {
version,
snapshot_sha256,
}
| Self::Cancelled {
version,
snapshot_sha256,
} => (*version, snapshot_sha256),
Self::RemediationRequired {
version,
snapshot_sha256,
consensus,
} => {
consensus.validate()?;
if consensus.snapshot_sha256 != *snapshot_sha256 {
return Err(adversarial_review_error(
"outcome.consensus.snapshot_sha256",
"consensus must bind to the outcome snapshot",
));
}
(*version, snapshot_sha256)
}
Self::Exhausted {
version,
snapshot_sha256,
rounds,
consensus,
} => {
if *rounds == 0 {
return Err(adversarial_review_error(
"outcome.rounds",
"exhausted outcome requires at least one round",
));
}
consensus.validate()?;
if consensus.snapshot_sha256 != *snapshot_sha256 {
return Err(adversarial_review_error(
"outcome.consensus.snapshot_sha256",
"consensus must bind to the outcome snapshot",
));
}
(*version, snapshot_sha256)
}
Self::Failed {
version,
snapshot_sha256,
message,
} => {
validate_review_text("outcome.message", message)?;
(*version, snapshot_sha256)
}
};
if version != ADVERSARIAL_REVIEW_CONTRACT_VERSION {
return Err(adversarial_review_error(
"outcome.version",
"unsupported version",
));
}
validate_sha256("outcome.snapshot_sha256", snapshot)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn review_snapshot() -> ReviewRepositorySnapshot {
ReviewRepositorySnapshot {
version: ADVERSARIAL_REVIEW_CONTRACT_VERSION,
repository_identity_sha256: "a".repeat(64),
head_object_id: "0123456789abcdef".to_string(),
aggregate_sha256: "b".repeat(64),
project_instruction_fingerprint_sha256: "c".repeat(64),
}
}
fn review_finding(id: &str) -> ReviewFinding {
ReviewFinding {
version: ADVERSARIAL_REVIEW_CONTRACT_VERSION,
finding_id: id.to_string(),
snapshot_sha256: "b".repeat(64),
severity: ReviewFindingSeverity::Major,
disposition: ReviewFindingDisposition::Required,
location: Some(ReviewSourceRange {
path: "src/lib.rs".to_string(),
start_line: 10,
start_column: Some(2),
end_line: 12,
end_column: Some(8),
}),
title: "Terminal state can reopen".to_string(),
evidence: "A stale update overwrites the persisted terminal state.".to_string(),
remediation: "Reject updates after the authoritative terminal transition.".to_string(),
}
}
fn review_report(reviewer: &str, finding_id: &str) -> ReviewReport {
ReviewReport {
version: ADVERSARIAL_REVIEW_CONTRACT_VERSION,
assignment_id: format!("assignment-{reviewer}"),
reviewer_id: reviewer.to_string(),
snapshot_sha256: "b".repeat(64),
execution: ReviewExecutionFacts {
provider_id: "provider".to_string(),
model_id: "model".to_string(),
agent_profile: "reviewer".to_string(),
tools: BTreeSet::from(["filesystem.read".to_string()]),
},
findings: vec![review_finding(finding_id)],
}
}
#[test]
fn adversarial_review_contracts_validate_and_round_trip() {
let assignment_request = ReviewPanelAssignmentRequest {
version: ADVERSARIAL_REVIEW_CONTRACT_VERSION,
round_id: "round-1".to_string(),
snapshot: review_snapshot(),
reviewer_ids: vec!["reviewer-a".to_string(), "reviewer-b".to_string()],
include_paths: vec!["src/lib.rs".to_string()],
policy_id: "strict".to_string(),
};
let assignments = assignment_request.materialize().expect("assignments");
assert_eq!(assignments.assignments.len(), 2);
assert_eq!(
assignments.assignments[0].assignment_id,
"round-1:reviewer-a"
);
assert_eq!(
assignments.assignments[1].assignment_id,
"round-1:reviewer-b"
);
let assignment = ReviewAssignment {
version: ADVERSARIAL_REVIEW_CONTRACT_VERSION,
assignment_id: "assignment-a".to_string(),
reviewer_id: "reviewer-a".to_string(),
snapshot: review_snapshot(),
include_paths: vec!["src/lib.rs".to_string()],
policy_id: "strict".to_string(),
};
assignment.validate().expect("assignment");
let panel = ReviewPanelResult {
version: ADVERSARIAL_REVIEW_CONTRACT_VERSION,
snapshot_sha256: "b".repeat(64),
reports: vec![
review_report("reviewer-a", "finding-a"),
review_report("reviewer-b", "finding-b"),
],
};
panel.validate().expect("panel");
let encoded = serde_json::to_string(&panel).expect("serialize panel");
assert_eq!(
serde_json::from_str::<ReviewPanelResult>(&encoded).expect("deserialize panel"),
panel
);
let findings = [review_finding("finding-a"), review_finding("finding-b")];
let consensus = ReviewConsensus {
version: ADVERSARIAL_REVIEW_CONTRACT_VERSION,
snapshot_sha256: "b".repeat(64),
decisions: findings
.into_iter()
.map(|finding| ReviewConsensusDecision {
finding_id: finding.finding_id.clone(),
disposition: ReviewConsensusDisposition::Required,
finding: Some(finding),
duplicate_of: None,
rationale: "The evidence demonstrates a correctness defect.".to_string(),
})
.collect(),
};
consensus.validate().expect("consensus");
consensus
.validate_against_panel(&panel)
.expect("panel consensus");
ReviewRoundOutcome::RemediationRequired {
version: ADVERSARIAL_REVIEW_CONTRACT_VERSION,
snapshot_sha256: "b".repeat(64),
consensus,
}
.validate()
.expect("outcome");
}
#[test]
fn adversarial_review_contracts_reject_mixed_snapshot_and_conflicting_identity() {
let mut report = review_report("reviewer-a", "finding-a");
report.findings[0].snapshot_sha256 = "d".repeat(64);
assert!(report.validate().is_err());
let unordered = ReviewPanelAssignmentRequest {
version: ADVERSARIAL_REVIEW_CONTRACT_VERSION,
round_id: "round-1".to_string(),
snapshot: review_snapshot(),
reviewer_ids: vec!["reviewer-b".to_string(), "reviewer-a".to_string()],
include_paths: Vec::new(),
policy_id: "strict".to_string(),
};
assert!(unordered.materialize().is_err());
let duplicate_report = review_report("reviewer-a", "finding-b");
let panel = ReviewPanelResult {
version: ADVERSARIAL_REVIEW_CONTRACT_VERSION,
snapshot_sha256: "b".repeat(64),
reports: vec![review_report("reviewer-a", "finding-a"), duplicate_report],
};
assert!(panel.validate().is_err());
let decision = ReviewConsensusDecision {
finding_id: "finding-a".to_string(),
disposition: ReviewConsensusDisposition::Duplicate,
finding: None,
duplicate_of: Some("finding-a".to_string()),
rationale: "duplicate".to_string(),
};
assert!(
ReviewConsensus {
version: ADVERSARIAL_REVIEW_CONTRACT_VERSION,
snapshot_sha256: "b".repeat(64),
decisions: vec![decision],
}
.validate()
.is_err()
);
let panel = ReviewPanelResult {
version: ADVERSARIAL_REVIEW_CONTRACT_VERSION,
snapshot_sha256: "b".repeat(64),
reports: vec![
review_report("reviewer-a", "finding-a"),
review_report("reviewer-b", "finding-b"),
],
};
let finding = review_finding("finding-a");
let incomplete = ReviewConsensus {
version: ADVERSARIAL_REVIEW_CONTRACT_VERSION,
snapshot_sha256: "b".repeat(64),
decisions: vec![ReviewConsensusDecision {
finding_id: finding.finding_id.clone(),
disposition: ReviewConsensusDisposition::Advisory,
finding: Some(finding),
duplicate_of: None,
rationale: "downgraded".to_string(),
}],
};
assert!(incomplete.validate_against_panel(&panel).is_err());
}
#[test]
fn external_review_import_models_round_trip_json() {
let response = ExternalReviewImportResponse {
importer_id: "github_pr_review".to_string(),
threads: vec![ExternalReviewThread {
identity: ExternalReviewIdentity {
provider_id: "github".to_string(),
external_id: "thread-1".to_string(),
url: Some("https://github.com/owner/repo/pull/1#discussion_r1".to_string()),
},
state: ExternalReviewThreadState::Outdated,
mapping: ExternalReviewAnchorMapping {
status: ExternalAnchorMappingStatus::Unmappable,
anchor: None,
message: Some("diff context is outdated".to_string()),
},
comments: vec![ExternalReviewComment {
identity: ExternalReviewIdentity {
provider_id: "github".to_string(),
external_id: "comment-1".to_string(),
url: None,
},
author: None,
author_label: Some("reviewer".to_string()),
body: "Please handle this case.".to_string(),
created_at_ms: Some(10),
updated_at_ms: Some(20),
}],
}],
warnings: vec!["one thread was unmappable".to_string()],
};
let json = serde_json::to_string(&response).expect("serialize import response");
let decoded: ExternalReviewImportResponse =
serde_json::from_str(&json).expect("deserialize import response");
assert_eq!(decoded, response);
}
#[test]
fn review_ai_exchange_models_round_trip_json() {
let exchange = ReviewAiExchange {
schema_version: REVIEW_AI_EXCHANGE_SCHEMA_VERSION,
exchange_id: "exchange-1".to_string(),
anchor: DraftAnchor {
kind: ReviewAnchorKind::Range,
file_path: "src/lib.rs".to_string(),
diff_row: 3,
start_diff_row: Some(3),
end_diff_row: Some(4),
old_start: Some(2),
old_end: Some(2),
new_start: Some(3),
new_end: Some(4),
old_line: None,
new_line: None,
line_kind: ReviewLineKind::Added,
is_file_anchor: false,
surface_id: Some("surface-1".to_string()),
source_id: Some("source-1".to_string()),
},
question: "Why is this safe?".to_string(),
session_id: Some("session-1".to_string()),
status: ReviewAiExchangeStatus::Linked,
error: None,
created_at_ms: 10,
updated_at_ms: 20,
};
let json = serde_json::to_string(&exchange).expect("serialize exchange");
let round_trip: ReviewAiExchange =
serde_json::from_str(&json).expect("deserialize exchange");
assert_eq!(round_trip, exchange);
assert_eq!(
serde_json::to_value(ReviewAiExchangeStatus::Failed).expect("status json"),
serde_json::json!("failed")
);
}
#[test]
fn review_suggestion_models_round_trip_json() {
let suggestion = ReviewSuggestion {
suggestion_id: "suggestion-1".to_string(),
anchor: DraftAnchor {
kind: ReviewAnchorKind::Range,
file_path: "src/lib.rs".to_string(),
diff_row: 3,
start_diff_row: Some(3),
end_diff_row: Some(4),
old_start: Some(2),
old_end: Some(2),
new_start: Some(3),
new_end: Some(4),
old_line: None,
new_line: None,
line_kind: ReviewLineKind::Added,
is_file_anchor: false,
surface_id: Some("surface-1".to_string()),
source_id: Some("source-1".to_string()),
},
body: "Handle the error explicitly.".to_string(),
session_id: Some("session-1".to_string()),
status: ReviewSuggestionStatus::Rejected,
rationale: Some("AI review".to_string()),
created_at_ms: 10,
updated_at_ms: 20,
};
let json = serde_json::to_string(&suggestion).expect("serialize suggestion");
let decoded: ReviewSuggestion =
serde_json::from_str(&json).expect("deserialize suggestion");
assert_eq!(decoded, suggestion);
}
#[test]
fn review_suggestion_status_uses_provider_neutral_wire_values() {
assert_eq!(
serde_json::to_string(&ReviewSuggestionStatus::Refining)
.expect("serialize suggestion status"),
"\"refining\""
);
assert_eq!(
serde_json::from_str::<ReviewSuggestionStatus>("\"accepted\"")
.expect("deserialize suggestion status"),
ReviewSuggestionStatus::Accepted
);
}
}