use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use std::sync::RwLock;
use mmdflux::graph::{Direction, Edge, Graph, Node};
use mmdflux::layout::{LaidOutGraph, LayoutOptions, layout_graph};
use pointbreak::documents::revision_show_document;
use pointbreak::highlight::{emphasis_file, highlight_file};
use pointbreak::model::{ObjectId, ReviewEndpoint, RevisionId, RevisionSource, ValidationStatus};
use pointbreak::session::event::ReviewAssessment;
use pointbreak::session::{
AssessmentRecordStatus, AssessmentView, BaseProjectionConfig, CurrentAssessmentStatus,
EventVerificationPolicy, HistoryPage, HistoryQuery, InputRequestStatus, LivenessEnrichment,
ObservationStatus, ObservationView, ProjectionDiagnostic, ReviewHistoryEntry,
RevisionCommitRangeView, RevisionListEntry, RevisionListOptions, RevisionOverview,
RevisionOverviewsOptions, RevisionProjectionSummary, RevisionShowOptions, RevisionShowResult,
SessionState, StoreIdentity, StoreIdentityOptions, SupersessionView, apply_history_query,
enrich_liveness, event_log_head_marker, history_base_projection, list_revisions,
read_bound_object_artifact, read_events_for_display, read_object_artifact, show_revision,
show_revision_overviews, store_identity,
};
use serde::Serialize;
use super::server::HighlightCache;
use super::wire::WireObjectArtifact;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct HistoryPayload {
schema: &'static str,
event_set_hash: String,
event_count: usize,
history_count: usize,
entries: Vec<ReviewHistoryEntry>,
facets: BTreeMap<String, usize>,
match_count: usize,
offset: usize,
#[serde(skip_serializing_if = "Option::is_none")]
match_index: Option<usize>,
diagnostics: Vec<ProjectionDiagnostic>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct RevisionsPayload {
schema: &'static str,
event_set_hash: String,
event_count: usize,
revision_count: usize,
entries: Vec<RevisionEntryDocument>,
diagnostics: Vec<ProjectionDiagnostic>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ThreadsPayload {
schema: &'static str,
event_set_hash: String,
event_count: usize,
thread_count: usize,
threads: Vec<ThreadDocument>,
supersedes: BTreeMap<String, Vec<String>>,
superseded_by: BTreeMap<String, Vec<String>>,
revision_classification: BTreeMap<String, RevisionClassification>,
diagnostics: Vec<ProjectionDiagnostic>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct RevisionClassification {
state: &'static str,
superseded_by: Vec<String>,
supersedes: Vec<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ThreadDocument {
revisions: Vec<String>,
heads: Vec<String>,
superseded: Vec<String>,
competing: bool,
laid_out: ThreadLayout,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ThreadLayout {
nodes: Vec<LaidOutNodeWire>,
edges: Vec<LaidOutEdgeWire>,
bounds: LayoutBounds,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct LaidOutNodeWire {
id: String,
x: f64,
y: f64,
w: f64,
h: f64,
is_head: bool,
is_superseded: bool,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct LaidOutEdgeWire {
from: String,
to: String,
path: Vec<[f64; 2]>,
#[serde(skip_serializing_if = "Option::is_none")]
kind: Option<&'static str>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct LayoutBounds {
w: f64,
h: f64,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct FactSupersessionDocument {
#[serde(skip_serializing_if = "Option::is_none")]
assessments: Option<FactGraphDocument>,
#[serde(skip_serializing_if = "Option::is_none")]
observations: Option<FactGraphDocument>,
}
impl FactSupersessionDocument {
fn is_empty(&self) -> bool {
self.assessments.is_none() && self.observations.is_none()
}
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct FactGraphDocument {
laid_out: ThreadLayout,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct RevisionEntryDocument {
captured_at: String,
revision_id: RevisionId,
snapshot_id: ObjectId,
source: RevisionSource,
base: ReviewEndpoint,
target: ReviewEndpoint,
snapshot_content_hash: String,
commit_range: RevisionCommitRangeView,
merge_status: String,
grouped_revision_ids: Vec<RevisionId>,
target_display: TargetDisplay,
overview: RevisionOverviewDocument,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct RevisionOverviewDocument {
current_assessment: RevisionOverviewAssessmentDocument,
attention: RevisionAttentionDocument,
counts: RevisionOverviewCounts,
latest_activity: Option<RevisionLatestActivityDocument>,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct RevisionOverviewAssessmentDocument {
status: &'static str,
assessment: Option<ReviewAssessment>,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct RevisionAttentionDocument {
unassessed: bool,
accepted_with_follow_up: bool,
open_input_request_count: usize,
failed_validation_count: usize,
errored_validation_count: usize,
stale_fact_count: usize,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct RevisionOverviewCounts {
files: usize,
rows: usize,
observations: usize,
input_requests: usize,
assessments: usize,
validation_checks: usize,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct RevisionLatestActivityDocument {
kind: &'static str,
title: String,
at: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct FreshnessPayload {
schema: &'static str,
event_count: u64,
}
const WORKING_TREE_FLOOR: &str = "working tree";
const GIT_COMMIT_FLOOR: &str = "git commit";
const SHORT_OID_LEN: usize = 7;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct TargetDisplay {
kind: &'static str,
label: String,
#[serde(skip_serializing_if = "Option::is_none")]
head: Option<HeadDisplay>,
path_private: bool,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct HeadDisplay {
commit_oid_short: String,
label: String,
#[serde(skip_serializing_if = "Option::is_none")]
live_branch: Option<String>,
}
fn derive_target_display(target: &ReviewEndpoint, base: &ReviewEndpoint) -> TargetDisplay {
let (kind, label) = match target {
ReviewEndpoint::GitWorkingTree { worktree_root } => {
("working_tree", basename_label(worktree_root))
}
ReviewEndpoint::GitCommit { commit_oid, .. } => (
"git_commit",
short_oid(commit_oid).unwrap_or_else(|| GIT_COMMIT_FLOOR.to_owned()),
),
};
TargetDisplay {
kind,
label,
head: head_display(base),
path_private: true,
}
}
fn basename_label(worktree_root: &str) -> String {
Path::new(worktree_root)
.file_name()
.and_then(|name| name.to_str())
.filter(|name| !name.is_empty())
.map(str::to_owned)
.unwrap_or_else(|| WORKING_TREE_FLOOR.to_owned())
}
fn short_oid(commit_oid: &str) -> Option<String> {
if commit_oid.is_empty() {
return None;
}
Some(commit_oid.chars().take(SHORT_OID_LEN).collect())
}
fn head_display(base: &ReviewEndpoint) -> Option<HeadDisplay> {
match base {
ReviewEndpoint::GitCommit { commit_oid, .. } => {
let short = short_oid(commit_oid)?;
Some(HeadDisplay {
label: short.clone(),
commit_oid_short: short,
live_branch: None,
})
}
ReviewEndpoint::GitWorkingTree { .. } => None,
}
}
fn splice_target_display(
document: &mut serde_json::Value,
target_display: TargetDisplay,
) -> Result<(), String> {
let value = serde_json::to_value(target_display).map_err(|error| error.to_string())?;
if let Some(revision) = document
.get_mut("revision")
.and_then(|ru| ru.as_object_mut())
{
revision.insert("targetDisplay".to_owned(), value);
}
Ok(())
}
fn to_unit_entry_documents(
entries: Vec<RevisionListEntry>,
mut overviews: BTreeMap<String, RevisionOverviewDocument>,
) -> Result<Vec<RevisionEntryDocument>, String> {
entries
.into_iter()
.map(|entry| {
let RevisionListEntry {
captured_at,
revision_id,
object_id,
source,
base,
target,
object_artifact_content_hash,
commit_range,
merge_status,
grouped_revision_ids,
} = entry;
let target_display = derive_target_display(&target, &base);
let overview = overviews.remove(revision_id.as_str()).ok_or_else(|| {
format!("missing overview for revision: {}", revision_id.as_str())
})?;
Ok(RevisionEntryDocument {
captured_at,
revision_id,
snapshot_id: object_id,
source,
base,
target,
snapshot_content_hash: object_artifact_content_hash,
commit_range,
merge_status,
grouped_revision_ids,
target_display,
overview,
})
})
.collect()
}
fn revision_overviews(
repo: &Path,
entries: &[RevisionListEntry],
) -> Result<BTreeMap<String, RevisionOverviewDocument>, String> {
let overviews = show_revision_overviews(
RevisionOverviewsOptions::new(repo)
.with_revisions(entries.iter().map(|entry| entry.revision_id.clone()))
.with_read_for_display(true)
.with_trust_set(crate::cli::common::discover_trust_set(repo)),
)
.map_err(|error| {
tracing::debug!(error = %error, "inspect_unit_overviews_failed");
format!("revision overviews not available: {error}")
})?;
let mut documents = BTreeMap::new();
for entry in entries {
let revision_id = entry.revision_id.as_str().to_owned();
let overview = overviews
.get(&entry.revision_id)
.ok_or_else(|| format!("revision overview not available: {revision_id}"))?;
documents.insert(
revision_id,
revision_overview_document(overview, &entry.captured_at),
);
}
Ok(documents)
}
fn stale_review_fact_count(
superseded_by: &BTreeSet<RevisionId>,
summary: &RevisionProjectionSummary,
) -> usize {
if superseded_by.is_empty() {
0
} else {
summary.observation_count
+ summary.input_request_count
+ summary.assessment_count
+ summary.validation_check_count
}
}
fn revision_overview_document(
result: &RevisionOverview,
captured_at: &str,
) -> RevisionOverviewDocument {
let summary = &result.summary;
RevisionOverviewDocument {
current_assessment: overview_current_assessment(&result.current_assessment.status),
attention: RevisionAttentionDocument {
unassessed: result.current_assessment.status == CurrentAssessmentStatus::Unassessed,
accepted_with_follow_up: current_assessment_includes_follow_up(
&result.current_assessment.status,
),
open_input_request_count: result
.input_requests
.iter()
.filter(|request| request.status == InputRequestStatus::Open)
.count(),
failed_validation_count: result
.validation_checks
.iter()
.filter(|check| check.status == ValidationStatus::Failed)
.count(),
errored_validation_count: result
.validation_checks
.iter()
.filter(|check| check.status == ValidationStatus::Errored)
.count(),
stale_fact_count: stale_review_fact_count(&result.superseded_by, summary),
},
counts: RevisionOverviewCounts {
files: summary.file_count,
rows: summary.row_count,
observations: summary.observation_count,
input_requests: summary.input_request_count,
assessments: summary.assessment_count,
validation_checks: summary.validation_check_count,
},
latest_activity: latest_revision_activity(result, captured_at),
}
}
fn overview_current_assessment(
status: &CurrentAssessmentStatus,
) -> RevisionOverviewAssessmentDocument {
RevisionOverviewAssessmentDocument {
status: status.as_str(),
assessment: match status {
CurrentAssessmentStatus::Resolved(assessment) => Some(*assessment),
CurrentAssessmentStatus::Unassessed | CurrentAssessmentStatus::Ambiguous(_) => None,
},
}
}
fn current_assessment_includes_follow_up(status: &CurrentAssessmentStatus) -> bool {
match status {
CurrentAssessmentStatus::Resolved(ReviewAssessment::AcceptedWithFollowUp) => true,
CurrentAssessmentStatus::Ambiguous(assessments) => {
assessments.contains(&ReviewAssessment::AcceptedWithFollowUp)
}
CurrentAssessmentStatus::Unassessed | CurrentAssessmentStatus::Resolved(_) => false,
}
}
fn latest_revision_activity(
result: &RevisionOverview,
captured_at: &str,
) -> Option<RevisionLatestActivityDocument> {
let mut latest = Some(RevisionLatestActivityDocument {
kind: "revision",
title: "Revision captured".to_owned(),
at: captured_at.to_owned(),
});
for observation in &result.observations {
set_latest_activity(
&mut latest,
"observation",
observation.title.clone(),
observation.created_at.clone(),
);
}
for request in &result.input_requests {
set_latest_activity(
&mut latest,
"input_request",
request.title.clone(),
request.created_at.clone(),
);
for response in &request.responses {
set_latest_activity(
&mut latest,
"input_request",
"Input request response".to_owned(),
response.created_at.clone(),
);
}
}
for assessment in &result.assessments {
let title = assessment
.summary
.clone()
.unwrap_or_else(|| format!("Assessment: {}", assessment_label(assessment.assessment)));
set_latest_activity(
&mut latest,
"assessment",
title,
assessment.created_at.clone(),
);
}
for check in &result.validation_checks {
let at = check
.completed_at
.clone()
.unwrap_or_else(|| check.created_at.clone());
set_latest_activity(&mut latest, "validation", check.check_name.clone(), at);
}
latest
}
fn set_latest_activity(
latest: &mut Option<RevisionLatestActivityDocument>,
kind: &'static str,
title: String,
at: String,
) {
if latest
.as_ref()
.is_none_or(|current| at.as_str() > current.at.as_str())
{
*latest = Some(RevisionLatestActivityDocument { kind, title, at });
}
}
fn assessment_label(assessment: ReviewAssessment) -> &'static str {
match assessment {
ReviewAssessment::Accepted => "accepted",
ReviewAssessment::AcceptedWithFollowUp => "accepted with follow-up",
ReviewAssessment::NeedsChanges => "needs changes",
ReviewAssessment::NeedsClarification => "needs clarification",
}
}
fn inspect_base_config(repo: &Path) -> BaseProjectionConfig {
BaseProjectionConfig {
verification_policy: Some(EventVerificationPolicy::advisory()),
trust_set: crate::cli::common::discover_trust_set(repo),
actor_attributes: crate::cli::common::discover_actor_attributes(repo),
delegation_map: crate::cli::common::discover_delegation_map(repo),
removal_policy: pointbreak::session::RemovalPolicy::default(),
}
}
pub(super) fn history_json(
repo: &Path,
cache: &super::cache::HistoryProjectionCache,
query: &HistoryQuery,
page: &HistoryPage,
) -> Result<String, String> {
let marker = event_log_head_marker(repo).map_err(|error| error.to_string())?;
let config = inspect_base_config(repo);
let base = cache.get_or_build(marker, || {
history_base_projection(repo, &config).map_err(|error| error.to_string())
})?;
let out = apply_history_query(&base, query, page);
let payload = HistoryPayload {
schema: "shore.inspect-history",
event_set_hash: out.event_set_hash,
event_count: out.event_count,
history_count: out.entries.len(),
entries: out.entries,
facets: out.facets,
match_count: out.match_count,
offset: out.offset,
match_index: out.match_index,
diagnostics: out.diagnostics,
};
serde_json::to_string(&payload).map_err(|error| error.to_string())
}
pub(super) fn revisions_json(repo: &Path) -> Result<String, String> {
let result = list_revisions(RevisionListOptions::new(repo).with_read_for_display(true))
.map_err(|error| error.to_string())?;
let overviews = revision_overviews(repo, &result.entries)?;
let payload = RevisionsPayload {
schema: "shore.inspect-revisions",
event_set_hash: result.event_set_hash,
event_count: result.event_count,
revision_count: result.revision_count,
entries: to_unit_entry_documents(result.entries, overviews)?,
diagnostics: result.diagnostics,
};
serde_json::to_string(&payload).map_err(|error| error.to_string())
}
pub(super) fn threads_json(repo: &Path) -> Result<String, String> {
let (events, display_diagnostics) =
read_events_for_display(repo).map_err(|error| error.to_string())?;
let state = SessionState::from_events(&events).map_err(|error| error.to_string())?;
let view = SupersessionView::from_events(&events).map_err(|error| error.to_string())?;
let threads = view
.components
.iter()
.map(|component| {
let heads: Vec<String> = component
.intersection(&view.heads)
.map(|revision| revision.as_str().to_owned())
.collect();
let superseded: Vec<String> = component
.intersection(&view.superseded)
.map(|revision| revision.as_str().to_owned())
.collect();
Ok(ThreadDocument {
revisions: component
.iter()
.map(|revision| revision.as_str().to_owned())
.collect(),
competing: heads.len() > 1,
heads,
superseded,
laid_out: thread_layout(component, &view)?,
})
})
.collect::<Result<Vec<_>, String>>()?;
let supersedes = revision_edge_map(&view.supersedes);
let superseded_by = revision_edge_map(&view.superseded_by);
let classification = revision_classification(&view);
let mut diagnostics = view.diagnostics;
diagnostics.extend(display_diagnostics);
let payload = ThreadsPayload {
schema: "shore.inspect-threads",
event_set_hash: state.event_set_hash.unwrap_or_default(),
event_count: state.event_count,
thread_count: threads.len(),
threads,
supersedes,
superseded_by,
revision_classification: classification,
diagnostics,
};
serde_json::to_string(&payload).map_err(|error| error.to_string())
}
fn short_node_label(id: &str) -> String {
let tail = id.rsplit(':').next().unwrap_or("");
tail.chars().take(12).collect()
}
struct SupersessionLayoutNode {
id: String,
label: String,
is_head: bool,
is_superseded: bool,
}
struct SupersessionLayoutEdge {
from: String,
to: String,
kind: Option<&'static str>,
}
fn layout_supersession_graph(
nodes: &[SupersessionLayoutNode],
edges: &[SupersessionLayoutEdge],
) -> Result<ThreadLayout, String> {
let mut graph = Graph::new(Direction::TopDown);
for node in nodes {
graph.add_node(Node::new(node.id.as_str()).with_label(node.label.clone()));
}
for edge in edges {
graph.add_edge(Edge::new(edge.from.as_str(), edge.to.as_str()));
}
let laid =
layout_graph(&graph, &LayoutOptions::default()).map_err(|error| error.to_string())?;
let kinds: std::collections::HashMap<(&str, &str), Option<&'static str>> = edges
.iter()
.map(|e| ((e.from.as_str(), e.to.as_str()), e.kind))
.collect();
let is_head = |id: &str| nodes.iter().any(|n| n.id.as_str() == id && n.is_head);
let is_superseded = |id: &str| nodes.iter().any(|n| n.id.as_str() == id && n.is_superseded);
let (min_x, min_y, max_x, max_y) = content_bounds(&laid);
let (origin_x, origin_y) = (min_x - NODE_STROKE_MARGIN, min_y - NODE_STROKE_MARGIN);
Ok(ThreadLayout {
nodes: laid
.nodes
.iter()
.map(|n| LaidOutNodeWire {
id: n.id.clone(),
x: n.center.x - origin_x,
y: n.center.y - origin_y,
w: n.width,
h: n.height,
is_head: is_head(&n.id),
is_superseded: is_superseded(&n.id),
})
.collect(),
edges: laid
.edges
.iter()
.map(|e| LaidOutEdgeWire {
from: e.from.clone(),
to: e.to.clone(),
path: e
.points
.iter()
.map(|p| [p.x - origin_x, p.y - origin_y])
.collect(),
kind: kinds
.get(&(e.from.as_str(), e.to.as_str()))
.copied()
.flatten(),
})
.collect(),
bounds: LayoutBounds {
w: (max_x - min_x) + 2.0 * NODE_STROKE_MARGIN,
h: (max_y - min_y) + 2.0 * NODE_STROKE_MARGIN,
},
})
}
fn thread_layout(
component: &BTreeSet<RevisionId>,
view: &SupersessionView,
) -> Result<ThreadLayout, String> {
let nodes: Vec<SupersessionLayoutNode> = component
.iter()
.map(|revision| SupersessionLayoutNode {
id: revision.as_str().to_owned(),
label: short_node_label(revision.as_str()),
is_head: view.heads.contains(revision),
is_superseded: view.superseded.contains(revision),
})
.collect();
let mut edges = Vec::new();
for revision in component {
if let Some(targets) = view.supersedes.get(revision) {
for target in targets {
if component.contains(target) {
edges.push(SupersessionLayoutEdge {
from: revision.as_str().to_owned(),
to: target.as_str().to_owned(),
kind: None,
});
}
}
}
}
layout_supersession_graph(&nodes, &edges)
}
const NODE_STROKE_MARGIN: f64 = 4.0;
fn content_bounds(laid: &LaidOutGraph) -> (f64, f64, f64, f64) {
let (mut min_x, mut min_y) = (f64::INFINITY, f64::INFINITY);
let (mut max_x, mut max_y) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
for n in &laid.nodes {
min_x = min_x.min(n.center.x - n.width / 2.0);
max_x = max_x.max(n.center.x + n.width / 2.0);
min_y = min_y.min(n.center.y - n.height / 2.0);
max_y = max_y.max(n.center.y + n.height / 2.0);
}
for e in &laid.edges {
for p in &e.points {
min_x = min_x.min(p.x);
max_x = max_x.max(p.x);
min_y = min_y.min(p.y);
max_y = max_y.max(p.y);
}
}
if min_x.is_finite() {
(min_x, min_y, max_x, max_y)
} else {
(0.0, 0.0, laid.width, laid.height)
}
}
fn revision_classification(view: &SupersessionView) -> BTreeMap<String, RevisionClassification> {
let mut map = BTreeMap::new();
for component in &view.components {
for revision in component {
let superseded_by = view
.superseded_by
.get(revision)
.map(|s| s.iter().map(|r| r.as_str().to_owned()).collect())
.unwrap_or_default();
let supersedes = view
.supersedes
.get(revision)
.map(|s| s.iter().map(|r| r.as_str().to_owned()).collect())
.unwrap_or_default();
let state = if view.superseded.contains(revision) {
"superseded"
} else if view.supersedes.contains_key(revision) {
"head" } else {
"isolated" };
map.insert(
revision.as_str().to_owned(),
RevisionClassification {
state,
superseded_by,
supersedes,
},
);
}
}
map
}
fn revision_edge_map(
edges: &std::collections::BTreeMap<RevisionId, std::collections::BTreeSet<RevisionId>>,
) -> BTreeMap<String, Vec<String>> {
edges
.iter()
.filter(|(_, targets)| !targets.is_empty())
.map(|(revision, targets)| {
(
revision.as_str().to_owned(),
targets.iter().map(|t| t.as_str().to_owned()).collect(),
)
})
.collect()
}
pub(super) fn snapshot_json(
repo: &Path,
snapshot_id: &str,
content_hash: Option<&str>,
cache: Option<&RwLock<HighlightCache>>,
) -> Result<String, String> {
if snapshot_id.is_empty() {
return Err("missing snapshot id".to_owned());
}
let object_id = ObjectId::new(snapshot_id.to_owned());
let artifact = match content_hash {
Some(content_hash) => read_bound_object_artifact(repo, &object_id, content_hash),
None => read_object_artifact(repo, &object_id),
}
.map_err(|error| {
tracing::debug!(
error = %error,
snapshot = snapshot_id,
content_hash,
"inspect_snapshot_read_failed"
);
format!("snapshot not found or unreadable: {snapshot_id}")
})?;
let cache_key = artifact.content_hash.clone();
if let Some(cache) = cache
&& let Some(cached) = cache.read().ok().and_then(|cache| cache.get(&cache_key))
{
return Ok(cached);
}
let wire = WireObjectArtifact::from_artifact(&artifact, highlight_file, emphasis_file);
let value = serde_json::to_value(&wire).map_err(|error| error.to_string())?;
let body = serde_json::to_string(&value).map_err(|error| error.to_string())?;
if let Some(cache) = cache
&& let Ok(mut cache) = cache.write()
{
cache.put(&cache_key, body.clone());
}
Ok(body)
}
fn fact_supersession_document(result: &RevisionShowResult) -> FactSupersessionDocument {
let assessments = matches!(
result.current_assessment.status,
CurrentAssessmentStatus::Ambiguous(_)
)
.then(|| assessment_fact_graph(&result.assessments))
.flatten();
let observations = result
.observations
.iter()
.any(|o| o.status == ObservationStatus::Superseded)
.then(|| observation_fact_graph(&result.observations))
.flatten();
FactSupersessionDocument {
assessments,
observations,
}
}
fn assessment_fact_graph(assessments: &[AssessmentView]) -> Option<FactGraphDocument> {
let ids: std::collections::BTreeSet<&str> = assessments.iter().map(|a| a.id.as_str()).collect();
let nodes: Vec<SupersessionLayoutNode> = assessments
.iter()
.map(|a| SupersessionLayoutNode {
id: a.id.as_str().to_owned(),
label: short_node_label(a.id.as_str()),
is_head: a.status == AssessmentRecordStatus::Current,
is_superseded: a.status == AssessmentRecordStatus::Replaced,
})
.collect();
let mut edges = Vec::new();
for a in assessments {
for replaced in &a.replaces {
if ids.contains(replaced.as_str()) {
edges.push(SupersessionLayoutEdge {
from: a.id.as_str().to_owned(),
to: replaced.as_str().to_owned(),
kind: Some("replaces"),
});
}
}
}
layout_or_log(&nodes, &edges, "assessment")
}
fn observation_fact_graph(observations: &[ObservationView]) -> Option<FactGraphDocument> {
let ids: std::collections::BTreeSet<&str> =
observations.iter().map(|o| o.id.as_str()).collect();
let nodes: Vec<SupersessionLayoutNode> = observations
.iter()
.map(|o| SupersessionLayoutNode {
id: o.id.as_str().to_owned(),
label: short_node_label(o.id.as_str()),
is_head: o.status == ObservationStatus::Active,
is_superseded: o.status == ObservationStatus::Superseded,
})
.collect();
let mut edges = Vec::new();
for o in observations {
for superseded in &o.supersedes {
if ids.contains(superseded.as_str()) {
edges.push(SupersessionLayoutEdge {
from: o.id.as_str().to_owned(),
to: superseded.as_str().to_owned(),
kind: Some("supersedes"),
});
}
}
}
layout_or_log(&nodes, &edges, "observation")
}
fn layout_or_log(
nodes: &[SupersessionLayoutNode],
edges: &[SupersessionLayoutEdge],
kind: &str,
) -> Option<FactGraphDocument> {
match layout_supersession_graph(nodes, edges) {
Ok(laid_out) => Some(FactGraphDocument { laid_out }),
Err(error) => {
tracing::debug!(error = %error, fact_kind = kind, "fact_supersession_layout_failed");
None
}
}
}
fn splice_fact_supersession(
value: &mut serde_json::Value,
doc: FactSupersessionDocument,
) -> Result<(), String> {
if doc.is_empty() {
return Ok(());
}
if let Some(object) = value.as_object_mut() {
object.insert(
"factSupersession".to_owned(),
serde_json::to_value(&doc).map_err(|error| error.to_string())?,
);
}
Ok(())
}
pub(super) fn revision_json(repo: &Path, revision_id: &str) -> Result<String, String> {
if revision_id.is_empty() {
return Err("missing revision id".to_owned());
}
let mut show_options = RevisionShowOptions::new(repo)
.with_revision_id(RevisionId::new(revision_id.to_owned()))
.with_exact(true)
.with_include_body(true)
.with_read_for_display(true)
.with_verification_policy(EventVerificationPolicy::advisory())
.with_trust_set(crate::cli::common::discover_trust_set(repo))
.with_actor_attributes(crate::cli::common::discover_actor_attributes(repo));
if let Some(map) = crate::cli::common::discover_delegation_map(repo) {
show_options = show_options.with_delegation_map(map);
}
let result = show_revision(show_options).map_err(|error| {
tracing::debug!(error = %error, revision = revision_id, "inspect_unit_read_failed");
format!("revision not found or unreadable: {revision_id}")
})?;
let target_display = derive_target_display(&result.revision.target, &result.revision.base);
let head_oid = match &result.revision.base {
ReviewEndpoint::GitCommit { commit_oid, .. } => Some(commit_oid.clone()),
ReviewEndpoint::GitWorkingTree { .. } => None,
};
let commit_range = result.commit_range.clone();
let fact_supersession = fact_supersession_document(&result);
let document = revision_show_document(result);
let mut value = serde_json::to_value(&document).map_err(|error| error.to_string())?;
splice_target_display(&mut value, target_display)?;
splice_fact_supersession(&mut value, fact_supersession)?;
if let Some(head_oid) = head_oid
&& let Ok(enrichment) = enrich_liveness(&commit_range, repo, None)
&& let Some(live_branch) = resolve_head_live_branch(&enrichment, &head_oid)
{
set_head_live_branch(&mut value, live_branch);
}
serde_json::to_string(&value).map_err(|error| error.to_string())
}
fn resolve_head_live_branch(enrichment: &LivenessEnrichment, head_oid: &str) -> Option<String> {
if let Some(commit) = enrichment
.per_commit
.iter()
.find(|commit| commit.commit_oid == head_oid)
{
return commit.live_branch.clone();
}
let mut labels = enrichment
.per_commit
.iter()
.filter_map(|commit| commit.live_branch.clone());
let first = labels.next()?;
labels.all(|label| label == first).then_some(first)
}
fn set_head_live_branch(document: &mut serde_json::Value, live_branch: String) {
if let Some(head) = document
.get_mut("revision")
.and_then(|revision| revision.get_mut("targetDisplay"))
.and_then(|target_display| target_display.get_mut("head"))
.and_then(|head| head.as_object_mut())
{
head.insert("liveBranch".to_owned(), live_branch.into());
}
}
pub(super) fn freshness_json(repo: &Path) -> Result<String, String> {
let event_count = event_log_head_marker(repo).map_err(|error| error.to_string())?;
let payload = FreshnessPayload {
schema: "shore.inspect-freshness",
event_count,
};
serde_json::to_string(&payload).map_err(|error| error.to_string())
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct IdentityPayload {
schema: &'static str,
#[serde(flatten)]
identity: StoreIdentity,
}
pub(super) fn identity_json(repo: &Path) -> Result<String, String> {
let identity =
store_identity(StoreIdentityOptions::new(repo)).map_err(|error| error.to_string())?;
let payload = IdentityPayload {
schema: "shore.inspect-identity",
identity,
};
serde_json::to_string(&payload).map_err(|error| error.to_string())
}
#[cfg(test)]
mod tests {
use pointbreak::model::{
EngagementId, ObjectId, ReviewEndpoint, RevisionId, RevisionSource, WorktreeCaptureMode,
};
use pointbreak::session::event::{
GitProvenance, Revision, WorkObjectProposal, WorkObjectProposedPayload,
};
use super::*;
#[test]
fn stale_review_fact_count_sums_review_facts_only_when_superseded() {
use pointbreak::session::RevisionProjectionSummary;
let summary = RevisionProjectionSummary {
observation_count: 2,
input_request_count: 1,
assessment_count: 1,
validation_check_count: 3,
..Default::default()
};
let superseded: BTreeSet<RevisionId> = [RevisionId::new("rev:sha256:successor")]
.into_iter()
.collect();
assert_eq!(stale_review_fact_count(&superseded, &summary), 7);
assert_eq!(stale_review_fact_count(&BTreeSet::new(), &summary), 0);
}
#[test]
fn revision_classification_marks_head_superseded_and_isolated() {
use pointbreak::session::SupersessionView;
let view = SupersessionView::from_edges([
(RevisionId::new("A"), vec![]),
(RevisionId::new("B"), vec![RevisionId::new("A")]),
(RevisionId::new("Z"), vec![]),
]);
let map = revision_classification(&view);
assert_eq!(map["B"].state, "head");
assert_eq!(map["A"].state, "superseded");
assert_eq!(map["A"].superseded_by, vec!["B".to_owned()]);
assert_eq!(map["B"].supersedes, vec!["A".to_owned()]);
assert_eq!(map["Z"].state, "isolated");
}
#[test]
fn thread_layout_lays_out_a_fork_as_equal_peers() {
use pointbreak::session::SupersessionView;
let view = SupersessionView::from_edges([
(RevisionId::new("A"), vec![]),
(RevisionId::new("B"), vec![RevisionId::new("A")]),
(RevisionId::new("C"), vec![RevisionId::new("A")]),
]);
let component: std::collections::BTreeSet<RevisionId> =
["A", "B", "C"].into_iter().map(RevisionId::new).collect();
let laid = thread_layout(&component, &view).expect("layout");
assert_eq!(laid.nodes.len(), 3);
let head_ids: Vec<&str> = laid
.nodes
.iter()
.filter(|n| n.is_head)
.map(|n| n.id.as_str())
.collect();
assert_eq!(head_ids.len(), 2, "two equal-peer heads: {head_ids:?}");
assert!(head_ids.contains(&"B") && head_ids.contains(&"C"));
let superseded: Vec<&str> = laid
.nodes
.iter()
.filter(|n| n.is_superseded)
.map(|n| n.id.as_str())
.collect();
assert_eq!(superseded, vec!["A"]);
assert_eq!(laid.edges.len(), 2);
for e in &laid.edges {
assert_eq!(e.to, "A");
assert_ne!(e.from, "A");
assert!(e.path.len() >= 2, "a routed polyline has >=2 points");
}
assert!(laid.bounds.w > 0.0 && laid.bounds.h > 0.0);
for n in &laid.nodes {
assert!(n.x - n.w / 2.0 >= -0.5 && n.x + n.w / 2.0 <= laid.bounds.w + 0.5);
assert!(n.y - n.h / 2.0 >= -0.5 && n.y + n.h / 2.0 <= laid.bounds.h + 0.5);
}
let head_ys: Vec<f64> = laid
.nodes
.iter()
.filter(|n| n.is_head)
.map(|n| n.y)
.collect();
assert!((head_ys[0] - head_ys[1]).abs() < 1.0, "heads share a rank");
let min_head_y = head_ys.iter().cloned().fold(f64::INFINITY, f64::min);
for n in &laid.nodes {
if !n.is_head {
assert!(n.y >= min_head_y, "no non-head node above a head");
}
}
}
#[test]
fn layout_core_lays_out_tagged_edges_over_opaque_ids() {
let nodes = vec![
SupersessionLayoutNode {
id: "a1".into(),
label: "a1".into(),
is_head: false,
is_superseded: true,
},
SupersessionLayoutNode {
id: "a2".into(),
label: "a2".into(),
is_head: true,
is_superseded: false,
},
SupersessionLayoutNode {
id: "a3".into(),
label: "a3".into(),
is_head: true,
is_superseded: false,
},
];
let edges = vec![
SupersessionLayoutEdge {
from: "a2".into(),
to: "a1".into(),
kind: Some("replaces"),
},
SupersessionLayoutEdge {
from: "a3".into(),
to: "a1".into(),
kind: Some("replaces"),
},
];
let laid = layout_supersession_graph(&nodes, &edges).expect("layout");
assert_eq!(laid.nodes.len(), 3);
assert_eq!(laid.edges.len(), 2);
for e in &laid.edges {
assert_eq!(e.to, "a1");
assert_ne!(e.from, "a1");
assert_eq!(
e.kind,
Some("replaces"),
"the edge kind rides through the layout"
);
assert!(e.path.len() >= 2);
}
let heads: Vec<&str> = laid
.nodes
.iter()
.filter(|n| n.is_head)
.map(|n| n.id.as_str())
.collect();
assert_eq!(heads.len(), 2);
assert!(laid.bounds.w > 0.0 && laid.bounds.h > 0.0);
}
#[test]
fn revision_thread_edges_omit_the_kind_field_on_the_wire() {
use pointbreak::session::SupersessionView;
let view = SupersessionView::from_edges([
(RevisionId::new("A"), vec![]),
(RevisionId::new("B"), vec![RevisionId::new("A")]),
]);
let component: std::collections::BTreeSet<RevisionId> =
["A", "B"].into_iter().map(RevisionId::new).collect();
let laid = thread_layout(&component, &view).expect("layout");
let json = serde_json::to_value(&laid.edges[0]).expect("serialize edge");
assert!(
json.get("kind").is_none(),
"revision edge must omit `kind`: {json}"
);
assert_eq!(laid.edges[0].from, "B");
assert_eq!(laid.edges[0].to, "A");
}
#[test]
fn thread_layout_degenerate_single_node_has_no_edges() {
use pointbreak::session::SupersessionView;
let view = SupersessionView::from_edges([(RevisionId::new("solo"), vec![])]);
let component: std::collections::BTreeSet<RevisionId> =
std::iter::once(RevisionId::new("solo")).collect();
let laid = thread_layout(&component, &view).expect("layout");
assert_eq!(laid.nodes.len(), 1);
assert_eq!(laid.edges.len(), 0);
assert!(laid.nodes[0].is_head);
}
fn working_tree(root: &str) -> ReviewEndpoint {
ReviewEndpoint::GitWorkingTree {
worktree_root: root.to_owned(),
}
}
fn commit(oid: &str) -> ReviewEndpoint {
ReviewEndpoint::GitCommit {
commit_oid: oid.to_owned(),
tree_oid: "tree-oid".to_owned(),
}
}
fn captured_repo() -> (tempfile::TempDir, String, String) {
let root = tempfile::tempdir().expect("create temp repo");
let path = root.path();
git(path, &["init"]);
git(path, &["config", "user.name", "Shore Tests"]);
git(path, &["config", "user.email", "shore-tests@example.com"]);
git(path, &["config", "commit.gpgsign", "false"]);
std::fs::write(path.join("src.txt"), "base\n").unwrap();
git(path, &["add", "--all"]);
git(path, &["commit", "-m", "base"]);
std::fs::write(path.join("src.txt"), "changed\n").unwrap();
let result = pointbreak::session::capture_worktree_review(
pointbreak::session::CaptureOptions::new(path),
)
.expect("capture worktree review");
(
root,
result.object_id.as_str().to_owned(),
result.object_artifact_content_hash,
)
}
fn git(cwd: &Path, args: &[&str]) {
let output = std::process::Command::new("git")
.args(args)
.current_dir(cwd)
.output()
.unwrap_or_else(|error| panic!("run git {args:?}: {error}"));
assert!(
output.status.success(),
"git {args:?} failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
fn captured_repo_with(
file: &str,
base: &str,
changed: &str,
) -> (tempfile::TempDir, String, String) {
let root = tempfile::tempdir().expect("create temp repo");
let path = root.path();
git(path, &["init"]);
git(path, &["config", "user.name", "Shore Tests"]);
git(path, &["config", "user.email", "shore-tests@example.com"]);
git(path, &["config", "commit.gpgsign", "false"]);
if let Some(parent) = Path::new(file).parent()
&& !parent.as_os_str().is_empty()
{
std::fs::create_dir_all(path.join(parent)).unwrap();
}
std::fs::write(path.join(file), base).unwrap();
git(path, &["add", "--all"]);
git(path, &["commit", "-m", "base"]);
std::fs::write(path.join(file), changed).unwrap();
let result = pointbreak::session::capture_worktree_review(
pointbreak::session::CaptureOptions::new(path),
)
.expect("capture worktree review");
(
root,
result.object_id.as_str().to_owned(),
result.object_artifact_content_hash,
)
}
fn common_dir_store(repo: &Path) -> std::path::PathBuf {
let output = std::process::Command::new("git")
.args(["rev-parse", "--path-format=absolute", "--git-common-dir"])
.current_dir(repo)
.output()
.expect("run git rev-parse --git-common-dir");
assert!(output.status.success(), "git rev-parse --git-common-dir");
let common_dir = String::from_utf8(output.stdout)
.expect("git-common-dir is utf-8")
.trim()
.to_owned();
Path::new(&common_dir).join("shore")
}
fn stored_object_artifact_path(repo: &Path) -> std::path::PathBuf {
let snapshots_dir = common_dir_store(repo).join("artifacts/objects");
let mut entries: Vec<_> = std::fs::read_dir(&snapshots_dir)
.expect("object artifacts dir exists")
.map(|entry| entry.unwrap().path())
.collect();
assert_eq!(entries.len(), 1, "exactly one stored object artifact");
entries.remove(0)
}
#[test]
fn threads_json_advertises_domain_named_schema() {
let (repo, _, _) = captured_repo();
let payload: serde_json::Value =
serde_json::from_str(&threads_json(repo.path()).unwrap()).unwrap();
assert_eq!(payload["schema"], "shore.inspect-threads");
}
#[test]
fn snapshot_json_serves_snapshot_scoped_wire() {
let (repo, snapshot_id, _) = captured_repo();
let wire: serde_json::Value =
serde_json::from_str(&snapshot_json(repo.path(), &snapshot_id, None, None).unwrap())
.unwrap();
assert!(wire["contentHash"].is_string());
assert!(wire.get("revisionId").is_none());
assert!(wire.get("source").is_none());
assert!(wire.get("base").is_none());
assert!(wire.get("target").is_none());
assert!(wire.get("worktreeRootRedacted").is_none());
assert!(wire.get("contentHashScope").is_none());
assert!(wire.get("targetDisplay").is_none());
}
#[test]
fn snapshot_json_includes_tokens_for_known_language() {
let (repo, object_id, content_hash) = captured_repo_with(
"src/lib.rs",
"pub fn value() -> u32 { 1 }\n",
"pub fn value() -> u32 { 2 }\n",
);
let json: serde_json::Value = serde_json::from_str(
&snapshot_json(repo.path(), &object_id, Some(&content_hash), None).unwrap(),
)
.unwrap();
let row = &json["snapshot"]["files"][0]["hunks"][0]["rows"][0];
let tokens = row["tokens"]
.as_array()
.expect("highlighted row has tokens");
assert!(!tokens.is_empty());
assert!(
tokens.iter().any(|t| t["kind"] == "keyword"),
"a .rs row carries a keyword token"
);
}
#[test]
fn snapshot_json_is_byte_stable_across_two_calls() {
let (repo, object_id, content_hash) = captured_repo_with(
"src/lib.rs",
"pub fn value() -> u32 { 1 }\n",
"pub fn value() -> u32 { 2 }\n",
);
let cache = std::sync::RwLock::new(super::super::server::HighlightCache::new(8));
let first =
snapshot_json(repo.path(), &object_id, Some(&content_hash), Some(&cache)).unwrap();
let second =
snapshot_json(repo.path(), &object_id, Some(&content_hash), Some(&cache)).unwrap();
assert_eq!(first, second);
}
#[test]
fn snapshot_body_includes_emphasis_and_is_cached() {
let (repo, object_id, content_hash) = captured_repo_with(
"src/lib.rs",
"pub fn value() -> u32 { 1 }\n",
"pub fn value() -> u32 { 2 }\n",
);
let cache = std::sync::RwLock::new(super::super::server::HighlightCache::new(8));
let first =
snapshot_json(repo.path(), &object_id, Some(&content_hash), Some(&cache)).unwrap();
assert!(
first.contains("\"emphasis\""),
"the rendered body carries intraline emphasis on the changed row"
);
let second =
snapshot_json(repo.path(), &object_id, Some(&content_hash), Some(&cache)).unwrap();
assert_eq!(first, second);
}
#[test]
fn snapshot_json_omits_tokens_for_unknown_language() {
let (repo, object_id, content_hash) =
captured_repo_with("notes.xyzzy", "alpha\n", "beta\n");
let json: serde_json::Value = serde_json::from_str(
&snapshot_json(repo.path(), &object_id, Some(&content_hash), None).unwrap(),
)
.unwrap();
let row = &json["snapshot"]["files"][0]["hunks"][0]["rows"][0];
assert!(row.get("tokens").is_none());
}
#[test]
fn snapshot_json_can_read_rebased_recapture_by_bound_hash() {
let root = tempfile::tempdir().expect("create temp repo");
let path = root.path();
git(path, &["init"]);
git(path, &["config", "user.name", "Shore Tests"]);
git(path, &["config", "user.email", "shore-tests@example.com"]);
git(path, &["config", "commit.gpgsign", "false"]);
std::fs::create_dir_all(path.join("src")).unwrap();
let base = (1..=12)
.map(|line| format!("preamble {line}\n"))
.chain(["pub fn value() -> u32 { 1 }\n".to_owned()])
.chain((1..=6).map(|line| format!("trailer {line}\n")))
.collect::<String>();
std::fs::write(path.join("src/lib.rs"), &base).unwrap();
git(path, &["add", "--all"]);
git(path, &["commit", "-m", "base"]);
let reviewed = base.replace("pub fn value() -> u32 { 1 }", "pub fn value() -> u32 { 2 }");
std::fs::write(path.join("src/lib.rs"), reviewed).unwrap();
let first = pointbreak::session::capture_worktree_review(
pointbreak::session::CaptureOptions::new(path),
)
.expect("capture first revision");
git(path, &["checkout", "--", "src/lib.rs"]);
let rebased_base = format!("inserted upstream line\n{base}");
std::fs::write(path.join("src/lib.rs"), &rebased_base).unwrap();
git(path, &["add", "--all"]);
git(path, &["commit", "-m", "upstream insert"]);
let rebased_reviewed =
rebased_base.replace("pub fn value() -> u32 { 1 }", "pub fn value() -> u32 { 2 }");
std::fs::write(path.join("src/lib.rs"), rebased_reviewed).unwrap();
let second = pointbreak::session::capture_worktree_review(
pointbreak::session::CaptureOptions::new(path)
.with_supersedes(vec![first.revision_id.clone()]),
)
.expect("capture rebased successor");
assert_eq!(first.object_id, second.object_id);
assert_ne!(
first.object_artifact_content_hash,
second.object_artifact_content_hash
);
let first_wire: serde_json::Value = serde_json::from_str(
&snapshot_json(
path,
first.object_id.as_str(),
Some(&first.object_artifact_content_hash),
None,
)
.unwrap(),
)
.unwrap();
let second_wire: serde_json::Value = serde_json::from_str(
&snapshot_json(
path,
second.object_id.as_str(),
Some(&second.object_artifact_content_hash),
None,
)
.unwrap(),
)
.unwrap();
assert_eq!(
first_wire["contentHash"],
first.object_artifact_content_hash.as_str()
);
assert_eq!(
second_wire["contentHash"],
second.object_artifact_content_hash.as_str()
);
}
#[test]
fn snapshot_json_rejects_tampered_artifact_before_wire_shaping() {
let (repo, snapshot_id, _) = captured_repo();
let artifact_path = stored_object_artifact_path(repo.path());
let mut json: serde_json::Value =
serde_json::from_slice(&std::fs::read(&artifact_path).unwrap()).unwrap();
json["snapshot"]["files"][0]["new_path"] = serde_json::json!("/evil");
std::fs::write(&artifact_path, serde_json::to_vec(&json).unwrap()).unwrap();
let error = snapshot_json(repo.path(), &snapshot_id, None, None)
.expect_err("tampered artifact is rejected before wire shaping");
assert!(error.contains("snapshot not found or unreadable"));
assert!(!error.contains("/evil"));
}
#[test]
fn derives_basename_label_and_short_head_from_captured_fields() {
let target = working_tree("/Users/x/worktrees/boardwalk/plan-0006");
let base = commit("545b0eb81463aaaaaaaaaaaaaaaaaaaaaaaaaaaa");
let display = derive_target_display(&target, &base);
assert_eq!(display.kind, "working_tree");
assert_eq!(display.label, "plan-0006");
let head = display
.head
.as_ref()
.expect("head derived from base commit");
assert_eq!(head.commit_oid_short, "545b0eb");
assert_eq!(head.label, "545b0eb");
assert!(head.live_branch.is_none());
assert!(display.path_private);
}
#[test]
fn floors_empty_or_root_worktree_root_to_working_tree() {
assert_eq!(
derive_target_display(&working_tree("/"), &commit("abc1234")).label,
"working tree"
);
assert_eq!(
derive_target_display(&working_tree(""), &commit("abc1234")).label,
"working tree"
);
}
#[test]
fn empty_commit_oid_yields_no_head() {
let display = derive_target_display(&working_tree("/repo/wt"), &commit(""));
assert!(display.head.is_none());
}
#[test]
fn commit_target_displays_short_target_oid_label() {
let display = derive_target_display(
&commit("9fceb02d0ae598e95dc970b74767f19372d61af8"),
&commit("abc1234def"),
);
assert_eq!(display.kind, "git_commit");
assert_eq!(display.label, "9fceb02");
assert_eq!(display.head.unwrap().commit_oid_short, "abc1234");
assert!(display.path_private);
}
#[test]
fn commit_target_with_empty_oid_floors_to_kind_label() {
let display = derive_target_display(&commit(""), &commit("abc1234def"));
assert_eq!(display.kind, "git_commit");
assert_eq!(display.label, "git commit");
assert_ne!(display.label, "working tree");
}
#[test]
fn serialized_block_is_camel_case_and_path_private() {
let display = derive_target_display(
&working_tree("/Users/x/worktrees/boardwalk/plan-0006"),
&commit("545b0eb81463aaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
);
let json = serde_json::to_string(&display).unwrap();
assert!(json.contains("\"pathPrivate\":true"));
assert!(json.contains("\"commitOidShort\":\"545b0eb\""));
assert!(json.contains("\"label\":\"plan-0006\""));
assert!(!json.contains("/Users"));
assert!(!json.contains("worktreeRoot"));
}
fn entry(worktree: &str, commit: &str) -> RevisionListEntry {
RevisionListEntry {
captured_at: "2026-05-13T10:00:00Z".to_owned(),
revision_id: RevisionId::new("rev:sha256:abc"),
object_id: ObjectId::new("snap:sha256:abc"),
source: RevisionSource::GitWorktree {
mode: WorktreeCaptureMode::CombinedHeadToWorkingTree,
include_untracked: true,
pathspecs: Vec::new(),
},
base: ReviewEndpoint::GitCommit {
commit_oid: commit.to_owned(),
tree_oid: "tree-oid".to_owned(),
},
target: ReviewEndpoint::GitWorkingTree {
worktree_root: worktree.to_owned(),
},
object_artifact_content_hash: "sha256:artifact:abc".to_owned(),
commit_range: pointbreak::session::RevisionCommitRangeView {
revision_id: RevisionId::new("review-unit:sha256:abc"),
anchored: false,
current_commits: Vec::new(),
current_refs: Vec::new(),
withdrawn_commits: Vec::new(),
withdrawn_refs: Vec::new(),
diagnostics: Vec::new(),
},
merge_status: "unknown".to_owned(),
grouped_revision_ids: vec![RevisionId::new("review-unit:sha256:abc")],
}
}
fn overview() -> RevisionOverviewDocument {
RevisionOverviewDocument {
current_assessment: RevisionOverviewAssessmentDocument {
status: "resolved",
assessment: Some(ReviewAssessment::Accepted),
},
attention: RevisionAttentionDocument {
unassessed: false,
accepted_with_follow_up: false,
open_input_request_count: 0,
failed_validation_count: 0,
errored_validation_count: 0,
stale_fact_count: 0,
},
counts: RevisionOverviewCounts {
files: 1,
rows: 1,
observations: 0,
input_requests: 0,
assessments: 1,
validation_checks: 0,
},
latest_activity: None,
}
}
#[test]
fn units_document_splices_target_display_additively() {
let entries = vec![entry(
"/Users/x/worktrees/boardwalk/plan-0006",
"545b0eb81463aaaaaaaaaaaaaaaaaaaaaaaaaaaa",
)];
let overviews = BTreeMap::from([("rev:sha256:abc".to_owned(), overview())]);
let docs = to_unit_entry_documents(entries, overviews).unwrap();
let json = serde_json::to_value(&docs[0]).unwrap();
assert_eq!(json["targetDisplay"]["label"], "plan-0006");
assert_eq!(json["targetDisplay"]["head"]["commitOidShort"], "545b0eb");
assert_eq!(json["targetDisplay"]["pathPrivate"], true);
assert_eq!(
json["target"]["worktreeRoot"],
"/Users/x/worktrees/boardwalk/plan-0006"
);
assert_eq!(json["target"]["kind"], "git_working_tree");
assert_eq!(
json["base"]["commitOid"],
"545b0eb81463aaaaaaaaaaaaaaaaaaaaaaaaaaaa"
);
assert!(json["source"].is_object());
assert_eq!(json["snapshotContentHash"], "sha256:artifact:abc");
assert!(
json.get("reviewUnitId").is_none(),
"the redundant reviewUnitId alias is dropped"
);
assert_eq!(json["capturedAt"], "2026-05-13T10:00:00Z");
assert_eq!(json["revisionId"], "rev:sha256:abc");
assert_eq!(json["snapshotId"], "snap:sha256:abc");
assert!(
json.get("objectId").is_none() && json.get("objectArtifactContentHash").is_none(),
"the inspector entry re-keys the shared fields into snapshot vocabulary"
);
}
#[test]
fn splice_target_display_adds_block_without_dropping_target_fields() {
let mut document = serde_json::json!({
"revision": {
"id": "review-unit:sha256:abc",
"target": {
"kind": "git_working_tree",
"worktreeRoot": "/Users/x/worktrees/boardwalk/plan-0006"
},
"base": { "kind": "git_commit", "commitOid": "545b0eb81463", "treeOid": "t" }
}
});
let display = derive_target_display(
&working_tree("/Users/x/worktrees/boardwalk/plan-0006"),
&commit("545b0eb81463aaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
);
splice_target_display(&mut document, display).unwrap();
assert_eq!(document["revision"]["targetDisplay"]["label"], "plan-0006");
assert_eq!(
document["revision"]["targetDisplay"]["head"]["commitOidShort"],
"545b0eb"
);
assert_eq!(
document["revision"]["target"]["worktreeRoot"],
"/Users/x/worktrees/boardwalk/plan-0006"
);
assert_eq!(document["revision"]["target"]["kind"], "git_working_tree");
}
#[test]
fn legacy_worktree_root_payload_derives_basename_without_touching_identity() {
let revision_id = RevisionId::new("rev:sha256:legacy");
let payload = WorkObjectProposedPayload {
engagement_id: EngagementId::new("engagement:sha256:legacy"),
work_object: WorkObjectProposal::Revision {
revision: Revision {
id: revision_id.clone(),
object_id: ObjectId::new("obj:sha256:legacy"),
git_provenance: Some(GitProvenance {
source: RevisionSource::GitWorktree {
mode: WorktreeCaptureMode::CombinedHeadToWorkingTree,
include_untracked: true,
pathspecs: Vec::new(),
},
base: ReviewEndpoint::GitCommit {
commit_oid: "0123456789abcdef0123456789abcdef01234567".to_owned(),
tree_oid: "tree-oid".to_owned(),
},
target: ReviewEndpoint::GitWorkingTree {
worktree_root: "/repo/legacy-wt".to_owned(),
},
}),
},
object_artifact_content_hash: "sha256:artifact:legacy".to_owned(),
supersedes: vec![],
},
};
let WorkObjectProposal::Revision { revision, .. } = payload.work_object else {
unreachable!("constructed a revision proposal");
};
let provenance = revision.git_provenance.as_ref().unwrap();
let display = derive_target_display(&provenance.target, &provenance.base);
let json = serde_json::to_string(&display).unwrap();
assert_eq!(display.label, "legacy-wt");
assert!(display.path_private);
assert_eq!(display.head.as_ref().unwrap().commit_oid_short, "0123456");
assert!(!json.contains("/repo"));
assert_eq!(revision.id, revision_id);
}
fn captured_commit_range_repo() -> (tempfile::TempDir, String, String) {
let root = tempfile::tempdir().expect("create temp repo");
let path = root.path();
git(path, &["init"]);
git(path, &["config", "user.name", "Shore Tests"]);
git(path, &["config", "user.email", "shore-tests@example.com"]);
git(path, &["config", "commit.gpgsign", "false"]);
std::fs::write(path.join("src.txt"), "base\n").unwrap();
git(path, &["add", "--all"]);
git(path, &["commit", "-m", "base"]);
std::fs::write(path.join("src.txt"), "next\n").unwrap();
git(path, &["add", "--all"]);
git(path, &["commit", "-m", "next"]);
let result = pointbreak::session::capture_review(
pointbreak::session::CaptureOptions::new(path).with_commit_range(
pointbreak::session::CommitRangeSpec::new("HEAD~1").with_target_rev("HEAD"),
),
)
.expect("capture commit range review");
let branch = current_branch(path);
(root, result.revision_id.as_str().to_owned(), branch)
}
fn current_branch(repo: &Path) -> String {
let output = std::process::Command::new("git")
.args(["symbolic-ref", "--short", "HEAD"])
.current_dir(repo)
.output()
.unwrap();
String::from_utf8(output.stdout).unwrap().trim().to_owned()
}
#[test]
fn revision_json_populates_live_branch_for_anchored_commit_on_a_branch() {
let (repo, revision_id, branch) = captured_commit_range_repo();
let value: serde_json::Value =
serde_json::from_str(&revision_json(repo.path(), &revision_id).unwrap()).unwrap();
assert_eq!(
value["revision"]["targetDisplay"]["head"]["liveBranch"],
serde_json::json!(branch),
"the anchored target commit is the branch tip → live on that branch"
);
}
#[test]
fn revision_json_omits_live_branch_for_floating_worktree_capture() {
let root = tempfile::tempdir().expect("create temp repo");
let path = root.path();
git(path, &["init"]);
git(path, &["config", "user.name", "Shore Tests"]);
git(path, &["config", "user.email", "shore-tests@example.com"]);
git(path, &["config", "commit.gpgsign", "false"]);
std::fs::write(path.join("src.txt"), "base\n").unwrap();
git(path, &["add", "--all"]);
git(path, &["commit", "-m", "base"]);
std::fs::write(path.join("src.txt"), "changed\n").unwrap();
let capture = pointbreak::session::capture_worktree_review(
pointbreak::session::CaptureOptions::new(path),
)
.expect("capture worktree review");
let value: serde_json::Value =
serde_json::from_str(&revision_json(path, capture.revision_id.as_str()).unwrap())
.unwrap();
assert!(
value["revision"]["targetDisplay"]["head"]["liveBranch"].is_null(),
"a floating worktree capture has no current commit → liveBranch omitted"
);
}
#[test]
fn revision_json_omits_live_branch_when_commit_objects_are_unavailable() {
let (repo, revision_id, _branch) = captured_commit_range_repo();
let elsewhere = tempfile::tempdir().expect("create separate repo");
git(elsewhere.path(), &["init"]);
git(elsewhere.path(), &["config", "user.name", "Shore Tests"]);
git(
elsewhere.path(),
&["config", "user.email", "shore-tests@example.com"],
);
git(elsewhere.path(), &["config", "commit.gpgsign", "false"]);
copy_dir_all(
&common_dir_store(repo.path()),
&common_dir_store(elsewhere.path()),
);
let value: serde_json::Value =
serde_json::from_str(&revision_json(elsewhere.path(), &revision_id).unwrap()).unwrap();
assert!(
value["revision"]["targetDisplay"]["head"]["liveBranch"].is_null(),
"commit objects absent → reachability unknown → liveBranch omitted, request still 200s"
);
}
fn copy_dir_all(from: &Path, to: &Path) {
std::fs::create_dir_all(to).unwrap();
for entry in std::fs::read_dir(from).unwrap() {
let entry = entry.unwrap();
let target = to.join(entry.file_name());
if entry.file_type().unwrap().is_dir() {
copy_dir_all(&entry.path(), &target);
} else {
std::fs::copy(entry.path(), target).unwrap();
}
}
}
#[test]
fn resolve_head_live_branch_prefers_head_then_falls_back_to_single_unambiguous() {
use pointbreak::session::{CommitGraphCondition, CommitLiveness, LivenessEnrichment};
let matched = LivenessEnrichment {
per_commit: vec![CommitLiveness {
commit_oid: "headoid".to_owned(),
condition: CommitGraphCondition::Live,
live_branch: Some("main".to_owned()),
}],
headline: Some(CommitGraphCondition::Live),
};
assert_eq!(
resolve_head_live_branch(&matched, "headoid").as_deref(),
Some("main")
);
assert_eq!(
resolve_head_live_branch(&matched, "baseoid").as_deref(),
Some("main")
);
let ambiguous = LivenessEnrichment {
per_commit: vec![
CommitLiveness {
commit_oid: "a".to_owned(),
condition: CommitGraphCondition::Live,
live_branch: Some("main".to_owned()),
},
CommitLiveness {
commit_oid: "b".to_owned(),
condition: CommitGraphCondition::Live,
live_branch: Some("feature".to_owned()),
},
],
headline: None,
};
assert_eq!(resolve_head_live_branch(&ambiguous, "baseoid"), None);
}
}