stemedb/crates/stemedb-api/src/dto/responses.rs
jordan ad07a75d0a feat: add source content to source registry, signed assertions, feed endpoint, dashboard enhancements
- Add `content: Option<String>` to SourceRecord with rkyv schema evolution
  (LegacySourceRecord compat deserializer for backward compatibility)
- Add MAX_SOURCE_CONTENT_LEN (1MB) limit with API validation
- Strip content from list responses, include in single-source GET
- Update Go SDK RegisterSourceRequest with Content field
- FCM pipeline extracts PDF text via pdftotext and passes to registration
- Dashboard impact panel fetches and displays source content with expand/collapse
- Add feed endpoint, dashboard feed panel, and signed assertion support
- Update data-structures.md, API docs, and storage docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 21:54:27 -07:00

281 lines
8.9 KiB
Rust

//! Response DTOs for query results and API operations.
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use super::enums::{LifecycleDto, ObjectValueDto, SignatureDto, SourceClassDto};
// ============================================================================
// Source Status Warning (P3.2 Cascade Flagging)
// ============================================================================
/// Warning attached to assertions citing non-Active sources.
///
/// Enables "show with warning" behavior per P3.2 requirements.
/// When a source is deprecated or quarantined, assertions citing it
/// receive this warning instead of being silently filtered.
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct SourceWarningDto {
/// Warning type: "deprecated" or "quarantined"
pub warning_type: String,
/// Human-readable explanation
pub message: String,
/// Source label from registry (if available)
#[serde(skip_serializing_if = "Option::is_none")]
pub source_label: Option<String>,
/// When the source status was last updated (Unix timestamp)
pub status_updated_at: u64,
}
// ============================================================================
// Response DTOs
// ============================================================================
/// Response containing a single assertion.
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct AssertionResponse {
/// Content-addressed hash of this assertion (hex-encoded)
pub hash: String,
/// The subject entity
pub subject: String,
/// The predicate/relation
pub predicate: String,
/// The object value
pub object: ObjectValueDto,
/// Hash of parent assertion (hex-encoded, optional)
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_hash: Option<String>,
/// Hash of source evidence (hex-encoded)
pub source_hash: String,
/// Source authority tier
pub source_class: SourceClassDto,
/// Perceptual hash for visual anchoring (hex-encoded, optional)
#[serde(skip_serializing_if = "Option::is_none")]
pub visual_hash: Option<String>,
/// Epoch ID (hex-encoded, optional)
#[serde(skip_serializing_if = "Option::is_none")]
pub epoch: Option<String>,
/// Lifecycle stage
pub lifecycle: LifecycleDto,
/// Agent signatures
pub signatures: Vec<SignatureDto>,
/// Confidence score (0.0 to 1.0)
pub confidence: f32,
/// Creation timestamp (Unix epoch)
pub timestamp: u64,
/// Semantic embedding vector (optional)
#[serde(skip_serializing_if = "Option::is_none")]
pub vector: Option<Vec<f32>>,
/// Structured source metadata as a JSON string (optional).
/// Domain-specific schema (journal, DOI, sample_size, etc.).
#[serde(skip_serializing_if = "Option::is_none")]
pub source_metadata: Option<String>,
/// Free-text narrative explaining methodology, limitations, bias, and caveats.
#[serde(skip_serializing_if = "Option::is_none")]
pub narrative: Option<String>,
/// Warning if this assertion cites a quarantined or deprecated source.
///
/// Present when the assertion's source has a non-Active status in the
/// Source Registry. Enables "show with warning" behavior for enterprise
/// compliance workflows.
#[serde(skip_serializing_if = "Option::is_none")]
pub source_warning: Option<SourceWarningDto>,
}
/// Response from a query operation.
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct QueryResponse {
/// Matching assertions
pub assertions: Vec<AssertionResponse>,
/// Total number of results returned
pub total_count: usize,
/// Whether there are more results beyond the limit
pub has_more: bool,
/// Degree of disagreement among candidates (0.0 = unanimous, 1.0 = max conflict).
/// See `stemedb_lens::compute_conflict_score()` for the canonical algorithm.
/// Only present when a lens is applied.
#[serde(skip_serializing_if = "Option::is_none")]
pub conflict_score: Option<f32>,
/// Confidence in the resolution (0.0 to 1.0).
/// Only present when a lens is applied.
#[serde(skip_serializing_if = "Option::is_none")]
pub resolution_confidence: Option<f32>,
/// Changelog entries for MV changes since the `since` parameter.
/// Only present when `since` parameter was provided.
#[serde(skip_serializing_if = "Option::is_none")]
pub changes_since: Option<Vec<ChangeEntryDto>>,
}
/// A changelog entry from a "since" query.
///
/// Represents a single change to a materialized view's winner.
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct ChangeEntryDto {
/// Unix timestamp when the change occurred.
pub timestamp: u64,
/// Hash of the previous winner (hex-encoded, None if first MV).
#[serde(skip_serializing_if = "Option::is_none")]
pub previous_winner_hash: Option<String>,
/// Hash of the new winner (hex-encoded).
pub new_winner_hash: String,
/// Subject of the changed pair.
pub subject: String,
/// Predicate of the changed pair.
pub predicate: String,
/// Which lens was used for resolution.
pub lens_name: String,
}
/// Error response.
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct ErrorResponse {
/// Human-readable error message
pub error: String,
/// Machine-readable error code
pub code: String,
}
/// Health check response.
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct HealthResponse {
/// Service status (e.g., "healthy")
pub status: String,
/// API version
pub version: String,
/// Total number of assertions in the database
pub assertions_count: u64,
}
/// Response from retrieving a source document.
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct ProvenanceResponse {
/// BLAKE3 hash of the content (hex-encoded).
pub hash: String,
/// The source document content (base64-encoded).
pub content: String,
/// MIME type of the content.
pub content_type: String,
/// Size of the content in bytes.
pub size: usize,
}
/// Per-tier resolution result from LayeredConsensus lens.
///
/// Represents the consensus within a single source class tier.
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct TierResolutionDto {
/// The tier number (0-5). Lower = higher authority.
pub tier: u8,
/// The source class for this tier.
pub source_class: SourceClassDto,
/// The winning assertion from within-tier consensus, if any candidates.
#[serde(skip_serializing_if = "Option::is_none")]
pub winner: Option<AssertionResponse>,
/// Number of candidates in this tier.
pub candidates_count: usize,
/// Within-tier conflict score (0.0 = unanimous, 1.0 = max conflict).
#[schema(minimum = 0.0, maximum = 1.0)]
pub conflict_score: f32,
/// Within-tier resolution confidence (0.0 to 1.0).
#[schema(minimum = 0.0, maximum = 1.0)]
pub resolution_confidence: f32,
}
/// Response from the admin rebuild-indexes endpoint.
///
/// Reports how many assertion indexes were rebuilt, how many were
/// skipped (e.g., deserialization failures), and how long the
/// operation took.
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct RebuildIndexesResponse {
/// Number of assertions whose indexes were rebuilt.
pub rebuilt_count: u64,
/// Number of keys that were skipped (deserialization failures).
pub skipped_count: u64,
/// Wall-clock time for the operation in milliseconds.
pub elapsed_ms: u64,
/// Human-readable status message.
pub status: String,
/// First error encountered (for diagnostics). Absent when all succeed.
#[serde(skip_serializing_if = "Option::is_none")]
pub first_error: Option<String>,
}
/// Response from a LayeredConsensus query.
///
/// Provides per-tier resolution results plus an overall winner.
/// Use this to see "What does Tier 0 say? What does Tier 5 say?"
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct LayeredQueryResponse {
/// The subject that was queried.
pub subject: String,
/// The predicate that was queried.
pub predicate: String,
/// Per-tier consensus results, ordered by tier (0 = highest authority first).
/// Only tiers with at least one candidate are included.
pub tiers: Vec<TierResolutionDto>,
/// Overall winner: winner from the highest-authority tier that has candidates.
#[serde(skip_serializing_if = "Option::is_none")]
pub overall_winner: Option<AssertionResponse>,
/// Cross-tier disagreement score (0.0 = tiers agree, 1.0 = tiers disagree).
#[schema(minimum = 0.0, maximum = 1.0)]
pub overall_conflict_score: f32,
/// Total candidates considered across all tiers.
pub total_candidates: usize,
/// Unix timestamp when this view was computed.
pub computed_at: u64,
/// Which lens was used (always "LayeredConsensus").
pub lens_name: String,
}