← snapshot
8314 bytes
//! Sketches and the rendezvous board.
//!
//! What can leave an org through these endpoints is exactly what the
//! registry can hold: org ids, 64 lane minima, timestamps. The publications
//! listing deliberately omits even the lanes, mirroring the demo's "what
//! left the org, in its entirety" scene.
use core::str::FromStr as _;
use axum::Json;
use axum::extract::{Path, State};
use axum::http::StatusCode;
use jac_core::{Clock as _, OrgId, SystemClock};
use jac_rendezvous::{Publication, PublishedSketch, Registry as _};
use jac_sketch::Similarity;
use serde::Deserialize;
use serde_json::{Value, json};
use crate::dto::{self, SketchDto};
use crate::error::ApiError;
use crate::routes::with_repo;
use crate::state::SharedState;
/// `GET /api/repos/{slug}/sketches/snapshot/{hex}`.
pub(crate) async fn sketch_snapshot(
State(state): State<SharedState>,
Path((slug, hex)): Path<(String, String)>,
) -> Result<Json<SketchDto>, ApiError> {
with_repo(&state, &slug, |entry| {
let id = dto::parse_snapshot_id(&hex)?;
let sketch = entry.repo.sketch_snapshot(id)?;
Ok(Json(SketchDto::of("content", &sketch.0)))
})
}
/// `GET /api/repos/{slug}/sketches/decision/{hex}`.
pub(crate) async fn sketch_decision(
State(state): State<SharedState>,
Path((slug, hex)): Path<(String, String)>,
) -> Result<Json<SketchDto>, ApiError> {
with_repo(&state, &slug, |entry| {
let id = dto::parse_decision_id(&hex)?;
let sketch = entry.repo.sketch_decision(id)?;
Ok(Json(SketchDto::of("decision", &sketch.0)))
})
}
/// What to publish: a content sketch of a snapshot, or a decision sketch.
#[derive(Debug, Deserialize)]
pub(crate) struct PublishRequest {
pub kind: String,
#[serde(default)]
pub snapshot: Option<String>,
#[serde(default)]
pub decision: Option<String>,
}
fn probe_sketch(
entry: &crate::state::RepoEntry,
kind: &str,
snapshot: Option<&str>,
decision: Option<&str>,
) -> Result<PublishedSketch, ApiError> {
match kind {
"content" => {
let hex =
snapshot.ok_or_else(|| ApiError::invalid("kind `content` requires `snapshot`"))?;
let id = dto::parse_snapshot_id(hex)?;
Ok(PublishedSketch::Content(entry.repo.sketch_snapshot(id)?))
}
"decision" => {
let hex =
decision.ok_or_else(|| ApiError::invalid("kind `decision` requires `decision`"))?;
let id = dto::parse_decision_id(hex)?;
Ok(PublishedSketch::Decision(entry.repo.sketch_decision(id)?))
}
other => Err(ApiError::invalid(format!(
"unknown sketch kind `{other}` (content|decision)"
))),
}
}
/// `POST /api/repos/{slug}/rendezvous/publish`.
pub(crate) async fn publish(
State(state): State<SharedState>,
Path(slug): Path<String>,
Json(req): Json<PublishRequest>,
) -> Result<(StatusCode, Json<Value>), ApiError> {
let (org, sketch) = with_repo(&state, &slug, |entry| {
let sketch = probe_sketch(
entry,
&req.kind,
req.snapshot.as_deref(),
req.decision.as_deref(),
)?;
Ok((entry.org_id, sketch))
})?;
let at = SystemClock.now();
let publication = {
let mut registry = state
.registry
.lock()
.map_err(|_| ApiError::internal("registry lock poisoned"))?;
registry.publish(Publication { org, sketch, at })
};
state.telemetry.emit(
jac_telemetry::Envelope::event("jac.rendezvous.published", at.as_millis())
.attr("kind", sketch.kind()),
);
Ok((
StatusCode::CREATED,
Json(json!({
"publication": publication.to_string(),
"org": org.to_string(),
"kind": sketch.kind(),
"at": at.as_millis(),
})),
))
}
/// A similarity probe against the shared board.
#[derive(Debug, Deserialize)]
pub(crate) struct FindSimilarRequest {
pub kind: String,
#[serde(default)]
pub snapshot: Option<String>,
#[serde(default)]
pub decision: Option<String>,
/// Minimum similarity in permille (0..=1000).
#[serde(default)]
pub min_permille: Option<u16>,
}
/// `POST /api/repos/{slug}/rendezvous/find-similar`.
pub(crate) async fn find_similar(
State(state): State<SharedState>,
Path(slug): Path<String>,
Json(req): Json<FindSimilarRequest>,
) -> Result<Json<Value>, ApiError> {
let (org, probe) = with_repo(&state, &slug, |entry| {
let sketch = probe_sketch(
entry,
&req.kind,
req.snapshot.as_deref(),
req.decision.as_deref(),
)?;
Ok((entry.org_id, sketch))
})?;
let min = Similarity::from_permille(req.min_permille.unwrap_or(150));
let matches: Vec<Value> = {
let registry = state
.registry
.lock()
.map_err(|_| ApiError::internal("registry lock poisoned"))?;
registry
.find_similar(&probe, min, org)
.iter()
.map(|m| {
json!({
"publication": m.publication.to_string(),
"org": m.org.to_string(),
"similarity_permille": m.similarity.permille(),
"similarity_percent": m.similarity.to_string(),
})
})
.collect()
};
Ok(Json(json!({ "matches": matches })))
}
/// `GET /api/rendezvous/publications` — the whole board: org, kind, when.
/// Lanes are deliberately absent here; per-sketch endpoints serve them.
pub(crate) async fn publications(
State(state): State<SharedState>,
) -> Result<Json<Value>, ApiError> {
let registry = state
.registry
.lock()
.map_err(|_| ApiError::internal("registry lock poisoned"))?;
let list: Vec<Value> = registry
.publications()
.iter()
.map(|p| {
json!({
"org": p.org.to_string(),
"kind": p.sketch.kind(),
"at": p.at.as_millis(),
})
})
.collect();
Ok(Json(json!({
"publications": list,
"note": "64 u64 lane minima and a timestamp per row — the registry cannot hold content",
})))
}
/// `GET /api/rendezvous/introductions`.
pub(crate) async fn introductions(
State(state): State<SharedState>,
) -> Result<Json<Value>, ApiError> {
let registry = state
.registry
.lock()
.map_err(|_| ApiError::internal("registry lock poisoned"))?;
let list: Vec<Value> = registry
.introductions()
.iter()
.map(|i| {
json!({
"token": i.token.to_string(),
"parties": [i.parties.0.to_string(), i.parties.1.to_string()],
"at": i.at.as_millis(),
})
})
.collect();
Ok(Json(json!({ "introductions": list })))
}
/// Two orgs to introduce.
#[derive(Debug, Deserialize)]
pub(crate) struct BrokerRequest {
pub a: String,
pub b: String,
}
/// `POST /api/rendezvous/introductions` — broker an introduction: a token,
/// two names, nothing else. The conversation happens outside the system.
pub(crate) async fn broker(
State(state): State<SharedState>,
Json(req): Json<BrokerRequest>,
) -> Result<(StatusCode, Json<Value>), ApiError> {
let a = OrgId::from_str(&req.a).map_err(|e| ApiError::invalid(format!("bad org id: {e}")))?;
let b = OrgId::from_str(&req.b).map_err(|e| ApiError::invalid(format!("bad org id: {e}")))?;
let at = SystemClock.now();
let introduction = {
let mut registry = state
.registry
.lock()
.map_err(|_| ApiError::internal("registry lock poisoned"))?;
registry.broker(a, b, at)
};
state.telemetry.emit(jac_telemetry::Envelope::event(
"jac.rendezvous.introduction",
at.as_millis(),
));
Ok((
StatusCode::CREATED,
Json(json!({
"token": introduction.token.to_string(),
"parties": [
introduction.parties.0.to_string(),
introduction.parties.1.to_string(),
],
"at": introduction.at.as_millis(),
})),
))
}