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
381 changes: 355 additions & 26 deletions Cargo.lock

Large diffs are not rendered by default.

21 changes: 19 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ schema: aether.architecture-document/v1
id: renderflow-roadmap
title: Renderflow Roadmap
kind: architecture-document
version: 0.1.3
version: 0.1.4
status: draft
owners:
- egohygiene
created: 2026-08-19
updated: 2026-09-27
updated: 2026-09-28
governed_by:
- architecture-roadmap
depends_on:
Expand All @@ -26,6 +26,23 @@ supersedes: []

# Renderflow Roadmap

## 2026-09-28 print-interior PDF review handoff

Checkpoint #416 adds the exact `publication.generate.pdf.interior` capability
through canonical ordered-collection planning and execution. The initial route
uses homogeneous local PNG or JPEG pages, explicit uniform trim geometry and
bleed, the observed img2pdf 0.6.3 provider, bounded direct argv, and independent
PDF page/box/image-stream inspection before materialization. Synthetic tests
cover the two-page route, a generated 44-page recipe, refusal, interruption,
immutable source bytes, and reproducible clean builds. The tool and adapter
catalogs advertise only this bounded route; generic image aggregation remains
experimental. No real publication pages, printer, retailer, or upload were used.

After review and merge, #417 may proceed independently for fixed-layout EPUB.
#418 independently validates that EPUB capability; #419 releases the exact
integration candidate consumed by Flow #52. PDF physical proof and retailer
acceptance remain outside #416.

## 2026-09-27 ordered collection review handoff

Checkpoint #415 adds canonical planning and execution for one explicitly
Expand Down
4 changes: 4 additions & 0 deletions crates/renderflow-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ serde_json = "1"
serde_yaml_ng = "0.9"

sha2 = "0.10"
lopdf = { version = "=0.45.0", default-features = false }
flate2 = "1"
crc32fast = "1"
jpeg-decoder = "0.3"

anyhow = "1"
thiserror = "1"
Expand Down
28 changes: 27 additions & 1 deletion crates/renderflow-core/data/adapter-packs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,33 @@ providers:
validation: [validator.core.pdf]
provenance: [provider_id, provider_version, argv_digest, ordered_input_digests, output_digest]
upstream: https://gitlab.mister-muffin.de/josch/img2pdf
rationale: Strong lossless image-to-PDF candidate; collection execution remains experimental until Transform v2 aggregation coverage lands.
rationale: Generic image aggregation remains experimental; the separately registered print-interior route has narrower inputs, a pinned provider, and independent validation.

- id: adapter.publication.print-interior
runtime_tool: tool.img2pdf
name: Ordered print-interior PDF adapter
families: [publication, images, pdf]
maturity: integrated
selection_priority: 10
capabilities: [publication.generate.pdf.interior]
input_media_types: [image/png, image/jpeg]
output_media_types: [application/pdf]
determinism: deterministic
locality: local
fidelity: lossless
execution: *bounded_local
configuration_schema:
source: homogeneous_ordered_immutable_collection
geometry: uniform_explicit_trim_mm_and_bleed_mm
boxes: media_bleed_trim_inset
rotation: none
scaling: fit_with_exact_aspect
color: preserve_rgb_gray
provider: exact_observed_img2pdf_version
validation: [validator.print_pdf.interior]
provenance: [provider_id, provider_version, configuration_digest, ordered_input_digests, inspected_page_stream_digests, output_digest]
upstream: https://gitlab.mister-muffin.de/josch/img2pdf
rationale: Canonical v2 print-interior path verifies every ordered page stream, geometry, boxes, and source lineage before materialization.

- id: adapter.pdf.ghostscript
runtime_tool: tool.ghostscript
Expand Down
3 changes: 2 additions & 1 deletion crates/renderflow-core/data/tool-registry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -176,14 +176,15 @@ tools:
operating_systems: [linux, macos, windows]
capabilities:
- image.aggregate.pdf
- publication.generate.pdf.interior
input_media_types: [image/*]
output_media_types: [application/pdf]
determinism: deterministic
locality: local
fidelity: lossless
support_tier: experimental
license_notes: "Consult the img2pdf upstream license for redistribution terms."
distribution_notes: "Used by lossless image-to-PDF aggregation when configured."
distribution_notes: "Generic aggregation is experimental; the exact print-interior route requires a pinned compatible version and independent page inspection."

- id: tool.ghostscript
name: Ghostscript
Expand Down
53 changes: 39 additions & 14 deletions crates/renderflow-core/src/graph/dag_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,13 @@ fn failed_step(
) -> StepEvidence {
let message = redact_sensitive_text(&format!("{error:#}"));
let step_id = format!("step:{}-to-{}", edge.from, edge.to);
let provider_failure = error
.chain()
.find_map(|cause| cause.downcast_ref::<crate::print_pdf::PrintPdfProviderError>());
let inspection_failure = error.chain().find_map(|cause| {
cause.downcast_ref::<crate::print_pdf_inspect::PrintPdfInspectionError>()
});
let cancelled = provider_failure.is_some_and(|failure| failure.cancelled);
StepEvidence {
step_id: step_id.clone(),
transform: edge_identity(edge),
Expand All @@ -175,18 +182,32 @@ fn failed_step(
started_at_unix_ms,
completed_at_unix_ms: unix_time_ms(),
duration_ms,
state: StepState::Failed,
state: if cancelled {
StepState::Cancelled
} else {
StepState::Failed
},
cache: CacheDisposition::Miss,
validation: if message.contains("returned artifact format") {
validation: if cancelled {
ValidationState::Skipped
} else if message.contains("returned artifact format") {
ValidationState::Invalid
} else {
ValidationState::Unavailable
},
fidelity: edge_fidelity(edge),
skip_reason: None,
diagnostics: vec![ExecutionDiagnostic {
severity: DiagnosticSeverity::FatalFailure,
code: "execution.transform_failed".to_string(),
severity: if cancelled {
DiagnosticSeverity::Cancelled
} else {
DiagnosticSeverity::FatalFailure
},
code: provider_failure
.map(|failure| failure.code)
.or_else(|| inspection_failure.map(|failure| failure.code))
.unwrap_or("execution.transform_failed")
.to_string(),
message,
step_id: Some(step_id),
}],
Expand Down Expand Up @@ -400,6 +421,17 @@ impl DagExecutor {
Ok(self)
}

/// Register a collection-input transform for the `from → to` edge.
pub fn register_collection_artifact(
&mut self,
from: Format,
to: Format,
transform: Arc<dyn ArtifactCollectionTransform>,
) -> &mut Self {
self.collection_transforms.insert((from, to), transform);
self
}

/// Register a collection-input transform for the `from → to` edge.
pub fn register_aggregation(
&mut self,
Expand Down Expand Up @@ -672,16 +704,9 @@ impl DagExecutor {
started_at_unix_ms,
duration_ms,
);
diagnostics.push(ExecutionDiagnostic {
severity: DiagnosticSeverity::FatalFailure,
code: "execution.transform_failed".to_string(),
message: step
.diagnostics
.first()
.map(|diagnostic| diagnostic.message.clone())
.unwrap_or_else(|| "Transform failed".to_string()),
step_id: Some(step.step_id.clone()),
});
if let Some(diagnostic) = step.diagnostics.first() {
diagnostics.push(diagnostic.clone());
}
steps.push(step);
}
}
Expand Down
3 changes: 3 additions & 0 deletions crates/renderflow-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ pub mod intake;
pub mod optimization;
mod pipeline;
pub mod planning;
pub mod print_pdf;
pub mod print_pdf_image;
pub mod print_pdf_inspect;
pub mod process;
pub mod publication;
mod sdk;
Expand Down
Loading
Loading