Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ Cargo.lock.bak
/v1.otio
/v2.otio
.claude/
.worktrees/
1 change: 1 addition & 0 deletions crates/vedit-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ pub mod model;
pub mod object;
pub mod otio;
pub mod repo;
pub mod review_artifact;
97 changes: 97 additions & 0 deletions crates/vedit-core/src/review_artifact.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//! Review artifact metadata.
//!
//! Review artifacts describe generated review outputs and their provenance.
//! The schema is intentionally separate from commit objects: commits identify
//! version-control history, while review artifacts identify downstream review
//! packages, renders, and reasoning trails derived from that history.

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReviewArtifact {
/// Always `"vedit.review_artifact.1"` for this schema version.
pub schema: String,
/// Path or URI for the generated review render.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub render_path: Option<String>,
/// UTC generation timestamp, formatted as RFC 3339 / ISO 8601 text.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub generated_at: Option<String>,
/// vedit commit hash that the review output was generated from.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_commit: Option<String>,
/// Timeline object hash that the review output was generated from.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timeline: Option<String>,
/// Freeform labels for downstream review package routing.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
/// Human-facing summary/header associated with the reviewed commit.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub commit_header: Option<String>,
/// Reasoning or explanation body attached to the review package.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_body: Option<String>,
}

impl ReviewArtifact {
pub const SCHEMA: &'static str = "vedit.review_artifact.1";

pub fn new() -> Self {
Self {
schema: Self::SCHEMA.to_string(),
render_path: None,
generated_at: None,
source_commit: None,
timeline: None,
tags: Vec::new(),
commit_header: None,
reasoning_body: None,
}
}

pub fn with_render_path(mut self, render_path: impl Into<String>) -> Self {
self.render_path = Some(render_path.into());
self
}

pub fn with_generated_at(mut self, generated_at: impl Into<String>) -> Self {
self.generated_at = Some(generated_at.into());
self
}

pub fn with_source_commit(mut self, source_commit: impl Into<String>) -> Self {
self.source_commit = Some(source_commit.into());
self
}

pub fn with_timeline(mut self, timeline: impl Into<String>) -> Self {
self.timeline = Some(timeline.into());
self
}

pub fn with_tags<I, S>(mut self, tags: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.tags = tags.into_iter().map(Into::into).collect();
self
}

pub fn with_commit_header(mut self, commit_header: impl Into<String>) -> Self {
self.commit_header = Some(commit_header.into());
self
}

pub fn with_reasoning_body(mut self, reasoning_body: impl Into<String>) -> Self {
self.reasoning_body = Some(reasoning_body.into());
self
}
}

impl Default for ReviewArtifact {
fn default() -> Self {
Self::new()
}
}
65 changes: 65 additions & 0 deletions crates/vedit-core/tests/review_artifact.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use serde_json::json;
use vedit_core::review_artifact::ReviewArtifact;

#[test]
fn review_artifact_serializes_versioned_provenance_fields() {
let artifact = ReviewArtifact::new()
.with_render_path("renders/review.mp4")
.with_generated_at("2026-05-21T07:29:30Z")
.with_source_commit(
"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
)
.with_timeline("sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
.with_tags(["review", "agent"])
.with_commit_header("Trim intro, add crossfade")
.with_reasoning_body("The edit removes dead air before the first title card.");

let value = serde_json::to_value(&artifact).unwrap();

assert_eq!(
value,
json!({
"schema": "vedit.review_artifact.1",
"render_path": "renders/review.mp4",
"generated_at": "2026-05-21T07:29:30Z",
"source_commit": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"timeline": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"tags": ["review", "agent"],
"commit_header": "Trim intro, add crossfade",
"reasoning_body": "The edit removes dead air before the first title card."
})
);
}

#[test]
fn review_artifact_deserializes_legacy_artifacts_with_missing_optional_fields() {
let artifact: ReviewArtifact = serde_json::from_value(json!({
"schema": "vedit.review_artifact.1",
"source_commit": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}))
.unwrap();

assert_eq!(artifact.schema, ReviewArtifact::SCHEMA);
assert_eq!(
artifact.source_commit.as_deref(),
Some("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
);
assert_eq!(artifact.render_path, None);
assert_eq!(artifact.generated_at, None);
assert_eq!(artifact.timeline, None);
assert!(artifact.tags.is_empty());
assert_eq!(artifact.commit_header, None);
assert_eq!(artifact.reasoning_body, None);
}

#[test]
fn empty_review_artifact_omits_optional_fields() {
let value = serde_json::to_value(ReviewArtifact::new()).unwrap();

assert_eq!(
value,
json!({
"schema": "vedit.review_artifact.1"
})
);
}
21 changes: 21 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,27 @@ Both projects can coexist. They serve different layers.

Not a video editor. Not media storage. Not a rendering service. Not a collaboration platform. Not an asset manager. vedit is the version-control layer for one OTIO file. Everything above is somebody else's product.

## Review artifact metadata

`vedit-core` exposes a versioned review artifact metadata primitive for downstream systems that generate review packages, rendered previews, or signed reasoning trails from vedit history. This is a shared serialization shape, not a CLI-generated object yet.

The serialized format is canonical JSON-compatible and can be written to the content-addressed object store like any other JSON value. The current schema is `vedit.review_artifact.1`:

```json
{
"schema": "vedit.review_artifact.1",
"render_path": "renders/review.mp4",
"generated_at": "2026-05-21T07:29:30Z",
"source_commit": "sha256:abc...",
"timeline": "sha256:def...",
"tags": ["review", "agent"],
"commit_header": "Trim intro, add crossfade",
"reasoning_body": "The edit removes dead air before the first title card."
}
```

All fields except `schema` are optional so older or partial artifacts remain readable. `render_path` may be a local path or URI supplied by the package generator. `generated_at` is UTC RFC 3339 / ISO 8601 text. `source_commit` stores the vedit commit hash, while `timeline` stores the timeline object hash, preserving both version-control identity and content identity.

## License

Apache 2.0.
Loading