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
35 changes: 35 additions & 0 deletions crates/renderflow-core/data/adapter-packs.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,41 @@
schema: renderflow.adapter-catalog/v1

providers:
- id: adapter.publication.lulu
runtime_tool: tool.lulu-rules
name: Lulu publication candidate adapter
families: [publication, pdf, ebooks]
maturity: integrated
selection_priority: 10
capabilities:
- publication.lulu.print-direct.preflight
- publication.lulu.bookstore.preflight
- publication.lulu.global-distribution.preflight
- publication.lulu.pdf-ebook.preflight
- publication.lulu.epub-distribution.preflight
input_media_types: [application/pdf, application/epub+zip, application/vnd.renderflow.publication+json]
output_media_types: [application/vnd.renderflow.lulu-conformance+json]
determinism: deterministic
locality: local
fidelity: lossless
execution:
service: renderflow.process/v1
direct_argv: true
bounded_output: true
timeout: true
cancellation: true
network: false
side_effects: [reads_declared_candidates, writes_declared_report]
configuration_schema:
request: renderflow.lulu-request/v1
rules: pinned_bundled_rule_pack
cover_templates: required_user_supplied_paths
attestations: explicit_reviewed_boolean_map
validation: [validator.core.pdf, validator.core.epub, validator.epubcheck, provider.lulu.rules]
provenance: [rule_pack_id, observation_date, official_source_urls, input_digests, cover_template_digests, publication_contract_digest]
upstream: https://help.lulu.com/
rationale: Provider-specific eligibility remains in a dated, offline rule pack and reports candidates without upload side effects.

- id: adapter.documents.pandoc
runtime_tool: tool.pandoc
name: Pandoc document adapter
Expand Down
72 changes: 72 additions & 0 deletions crates/renderflow-core/data/publication/lulu-rules-2026-09-11.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
schema: renderflow.lulu-rules/v1
id: lulu-2026-09-11
observed_on: "2026-09-11"
sources:
- url: https://help.lulu.com/en/support/solutions/articles/64000255462-mandatory-print-book-distribution-requirements
observed_modified: "2026-08-21"
- url: https://help.lulu.com/en/support/solutions/articles/64000255486-how-to-create-a-print-book
observed_modified: "2026-06-12"
- url: https://help.lulu.com/en/support/solutions/articles/64000255463-mandatory-ebook-distribution-requirements
observed_modified: "2026-08-07"
- url: https://help.lulu.com/en/support/solutions/articles/64000255592-publishing-an-ebook-for-global-distribution
observed_modified: "2026-08-07"
- url: https://help.lulu.com/en/support/solutions/articles/64000308545-learn-more-about-accessible-ebooks
observed_modified: "2025-05-30"
- url: https://help.lulu.com/en/support/solutions/articles/64000267552-global-distribution-print-exclusions
observed_modified: "2026-08-21"
print:
recommended_max_bytes: 524288000
amazon_minimum_color_pages: 72
spine_text_minimum_pages: 100
maximum_consecutive_blank_pages: 2
barcode:
width_mm: 50.8
height_mm: 30.5
edge_clearance_mm: 6.0
black_on_white: true
required_distribution_attestations:
- interior_page_count_verified
- interior_geometry_verified
- fonts_embedded
- layers_flattened
- vectors_rasterized_where_required
- no_trim_or_bleed_marks
- page_order_reviewed
- blank_pages_reviewed
- images_high_resolution
- color_matches_metadata
- metadata_matches_files
- title_page_before_copyright_page
- barcode_identity_verified
- barcode_geometry_verified
- barcode_safe_placement_verified
amazon_excluded_formats: [comic_book]
ingram_excluded_formats: [small_square]
amazon_excluded_bindings: [linen_wrap, casewrap]
amazon_excluded_papers: [eighty_white, eighty_coated]
global_distribution_review_minimum_weeks: 12
ebook:
epubcheck_major: 5
epub_version: "3.3"
recommended_max_bytes: 52428800
rejection_risk_bytes: 524288000
global_distribution_languages: [en, en-US, en-GB]
cover_width_px: 612
cover_height_px: 792
cover_minimum_ppi: 72
cover_maximum_ppi: 300
apple_max_image_pixels: 5600000
required_distribution_attestations:
- xhtml_content
- complete_ordered_navigation
- text_not_truncated
- images_rgb
- no_interactive_content
- no_fillable_content
- metadata_matches_files
- copyright_page_present
- retailer_cover_is_flat
- retailer_cover_dimensions_verified
- retailer_cover_resolution_verified
- apple_image_limit_verified
- accessibility_metadata_reviewed
24 changes: 24 additions & 0 deletions crates/renderflow-core/data/tool-registry.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,30 @@
schema: renderflow.tool-registry/v1

tools:
- id: tool.lulu-rules
name: Pinned Lulu publication rule pack
discovery:
kind: virtual
operating_systems: [linux, macos, windows]
capabilities:
- publication.lulu.print-direct.preflight
- publication.lulu.bookstore.preflight
- publication.lulu.global-distribution.preflight
- publication.lulu.pdf-ebook.preflight
- publication.lulu.epub-distribution.preflight
input_media_types:
- application/pdf
- application/epub+zip
- application/vnd.renderflow.publication+json
output_media_types:
- application/vnd.renderflow.lulu-conformance+json
determinism: deterministic
locality: local
fidelity: lossless
support_tier: optional
license_notes: "Provider rules summarize dated public Lulu documentation; official terms remain authoritative."
distribution_notes: "Offline candidate validation only; never uploads, allocates ISBNs, orders proofs, or publishes."

- id: tool.pandoc
name: Pandoc
discovery:
Expand Down
1 change: 1 addition & 0 deletions crates/renderflow-core/src/adapters/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub enum AdapterFamily {
Data,
Ocr,
Subtitles,
Publication,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
Expand Down
22 changes: 20 additions & 2 deletions crates/renderflow-core/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use clap::Parser;
use tracing::info;

use crate::cli::{
AiCommands, Cli, Commands, EbookCommands, GraphCommands, PluginCommands, SpecCommands,
ToolCommands,
AiCommands, Cli, Commands, EbookCommands, GraphCommands, LuluCommands, PluginCommands,
PublicationCommands, SpecCommands, ToolCommands,
};
use crate::{commands, transforms};

Expand Down Expand Up @@ -199,6 +199,24 @@ pub fn run_cli(cli: Cli) -> Result<()> {
} => commands::ebook::run_inspect(&input, &format, epubcheck)?,
EbookCommands::Capabilities { format } => commands::ebook::run_capabilities(&format)?,
},
Some(Commands::Publication { subcommand }) => match subcommand {
PublicationCommands::Lulu { subcommand } => match subcommand {
LuluCommands::Rules { format, output } => {
commands::publication::run_lulu_rules(&format, output.as_deref())?
}
LuluCommands::Preflight {
request,
format,
output,
epubcheck,
} => commands::publication::run_lulu_preflight(
&request,
&format,
output.as_deref(),
epubcheck,
)?,
},
},
Some(Commands::Capabilities {
format,
transforms,
Expand Down
39 changes: 39 additions & 0 deletions crates/renderflow-core/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,13 @@ pub enum Commands {
subcommand: EbookCommands,
},

/// Inspect pinned commercial-publication provider rule packs and candidates.
#[command(subcommand_required = true, arg_required_else_help = true)]
Publication {
#[command(subcommand)]
subcommand: PublicationCommands,
},

/// List stable provider capability IDs and their implementations.
Capabilities {
/// Output format: text (default), json, or yaml.
Expand Down Expand Up @@ -307,6 +314,38 @@ pub enum Commands {
},
}

#[derive(Subcommand)]
pub enum PublicationCommands {
/// Evaluate candidates with the bundled, offline Lulu provider pack.
Lulu {
#[command(subcommand)]
subcommand: LuluCommands,
},
}

#[derive(Subcommand)]
pub enum LuluCommands {
/// Emit the exact pinned Lulu rule pack and official source observations.
Rules {
#[arg(long, default_value = "text", value_name = "FORMAT")]
format: String,
#[arg(long, value_name = "FILE")]
output: Option<String>,
},
/// Validate local upload candidates without uploading or allocating identifiers.
Preflight {
#[arg(long, value_name = "FILE")]
request: String,
#[arg(long, default_value = "text", value_name = "FORMAT")]
format: String,
#[arg(long, value_name = "FILE")]
output: Option<String>,
/// Run the optional local EPUBCheck v5 provider.
#[arg(long)]
epubcheck: bool,
},
}

/// Subcommands for provider-neutral e-book derivatives.
#[derive(Subcommand)]
pub enum EbookCommands {
Expand Down
1 change: 1 addition & 0 deletions crates/renderflow-core/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod ebook;
pub mod graph;
pub mod inspect;
pub mod plugin;
pub mod publication;
pub mod spec;
pub mod system;
pub mod tools;
Expand Down
84 changes: 84 additions & 0 deletions crates/renderflow-core/src/commands/publication.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use std::fs;
use std::path::Path;

use anyhow::{Context, Result};
use serde::Serialize;

use crate::publication::lulu::{
evaluate_request, LuluConformanceReport, LuluEligibility, LuluRulePack,
};

pub fn run_lulu_rules(format: &str, output: Option<&str>) -> Result<()> {
emit(&LuluRulePack::builtin()?, format, output)
}

pub fn run_lulu_preflight(
request: &str,
format: &str,
output: Option<&str>,
epubcheck: bool,
) -> Result<()> {
let report = evaluate_request(Path::new(request), epubcheck)?;
emit_report(&report, format, output)?;
if report.channels.iter().any(|channel| {
matches!(
channel.eligibility,
LuluEligibility::Ineligible | LuluEligibility::Unknown
)
}) {
anyhow::bail!("one or more requested Lulu channels are not upload-ready; see report")
}
Ok(())
}

fn emit_report(report: &LuluConformanceReport, format: &str, output: Option<&str>) -> Result<()> {
if format.eq_ignore_ascii_case("text") {
let mut text = format!(
"Lulu candidate preflight\nRule pack: {} (observed {})\nUpload performed: no\n\nChannels:\n",
report.rule_pack, report.observed_on
);
for channel in &report.channels {
text.push_str(&format!(
" {:?}: {:?}\n",
channel.channel, channel.eligibility
));
for code in &channel.blocking_findings {
text.push_str(&format!(" BLOCK: {code}\n"));
}
for code in &channel.warning_findings {
text.push_str(&format!(" WARN: {code}\n"));
}
}
text.push_str("\nFindings:\n");
for finding in &report.findings {
text.push_str(&format!(
" [{:?}] {}: {}\n",
finding.severity, finding.code, finding.message
));
}
write(&text, output)
} else {
emit(report, format, output)
}
}

fn emit<T: Serialize>(value: &T, format: &str, output: Option<&str>) -> Result<()> {
let content = match format.to_ascii_lowercase().as_str() {
"json" => format!("{}\n", serde_json::to_string_pretty(value)?),
"yaml" | "yml" | "text" => serde_yaml_ng::to_string(value)?,
other => anyhow::bail!(
"unknown publication output format '{other}'; supported: text, json, yaml"
),
};
write(&content, output)
}

fn write(content: &str, output: Option<&str>) -> Result<()> {
if let Some(path) = output {
fs::write(path, content)
.with_context(|| format!("failed to write publication report '{path}'"))?;
} else {
print!("{content}");
}
Ok(())
}
2 changes: 2 additions & 0 deletions crates/renderflow-core/src/publication.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//! Provider-neutral publication contracts and deterministic release metadata.

pub mod lulu;

use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
Expand Down
Loading
Loading