From 9f0cfb3373bb6c1f730333a1660456b41290b824 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Thu, 1 Oct 2026 20:49:29 -0400 Subject: [PATCH] M0.1: cargo xtask (layers, assets, wasm, ci); attribute ArtCraft brand marks Layer table matches the real dependency graph; wasm check passes for all L0-L4 crates and the egui UI. Co-Authored-By: Claude Opus 5.5 --- .cargo/config.toml | 2 + AGENTS.md | 5 + ATTRIBUTION.md | 16 ++ Cargo.toml | 2 +- .../brand/artcraft-logo-white.png.attribution | 8 + .../brand/artcraft-logo-white.svg.attribution | 8 + docs/brand/artcraft-logo.png.attribution | 8 + docs/brand/artcraft-logo.svg.attribution | 8 + .../brand/artcraft-mark-black.png.attribution | 8 + .../brand/artcraft-mark-black.svg.attribution | 8 + docs/brand/artcraft-mark.png.attribution | 8 + docs/brand/artcraft-mark.svg.attribution | 8 + xtask/Cargo.toml | 9 + xtask/src/main.rs | 251 ++++++++++++++++++ 14 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 .cargo/config.toml create mode 100644 docs/brand/artcraft-logo-white.png.attribution create mode 100644 docs/brand/artcraft-logo-white.svg.attribution create mode 100644 docs/brand/artcraft-logo.png.attribution create mode 100644 docs/brand/artcraft-logo.svg.attribution create mode 100644 docs/brand/artcraft-mark-black.png.attribution create mode 100644 docs/brand/artcraft-mark-black.svg.attribution create mode 100644 docs/brand/artcraft-mark.png.attribution create mode 100644 docs/brand/artcraft-mark.svg.attribution create mode 100644 xtask/Cargo.toml create mode 100644 xtask/src/main.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..9741f113 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +xtask = "run --quiet --package xtask --" diff --git a/AGENTS.md b/AGENTS.md index 11de2781..ff6e109b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,6 +43,11 @@ reproduce an image (for example, point lists traced from someone else's icon). `crates/engine/src/demo.rs`, generators in `crates/effects`) are original work under the project licence and are listed in `ATTRIBUTION.md`. 7. **When in doubt, leave it out** and draw or generate it yourself. +8. **The one exception: first-party ArtCraft brand marks.** The ArtCraft name and logos in `docs/brand/` + (supplied by the ArtCraft team) are trademarks, all rights reserved, used with permission. They still + need a sidecar and an `ATTRIBUTION.md` row (licence `LicenseRef-ArtCraft-Trademark`). No other + non-open asset is allowed, and this exception never covers third-party marks (Adobe, Discord, GitHub + and other logos stay out; draw a generic icon instead). ## 2. Clean-room code diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 9a61c820..8e6e3533 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -21,3 +21,19 @@ iconography, images or other Adobe assets; open licences only; every asset attri - UI icons drawn in code: `crates/ui-egui/src/icons.rs`. - Procedural demo content (solids, shapes, gradients, text animations): `crates/engine/src/demo.rs`. + +## First-party brand marks + +The ArtCraft name and logo are trademarks of the ArtCraft team, used in EffectCraft with permission. They are not +openly licensed and are not covered by the project licence; forks must remove or replace them (AGENTS.md §1.8). + +| File | Author | Source | Licence | +|---|---|---|---| +| `docs/brand/artcraft-logo-white.png` | ArtCraft team | Original work: first-party ArtCraft brand mark (ArtCraft wordmark, light ink for dark backgrounds) | ArtCraft trademark, all rights reserved; used with permission (not MIT/Apache) | +| `docs/brand/artcraft-logo-white.svg` | ArtCraft team | Original work: first-party ArtCraft brand mark (ArtCraft wordmark, light ink for dark backgrounds) | ArtCraft trademark, all rights reserved; used with permission (not MIT/Apache) | +| `docs/brand/artcraft-logo.png` | ArtCraft team | Original work: first-party ArtCraft brand mark (ArtCraft wordmark (symbol + "ARTCRAFT"), dark ink for light backgrounds) | ArtCraft trademark, all rights reserved; used with permission (not MIT/Apache) | +| `docs/brand/artcraft-logo.svg` | ArtCraft team | Original work: first-party ArtCraft brand mark (ArtCraft wordmark (symbol + "ARTCRAFT"), dark ink for light backgrounds) | ArtCraft trademark, all rights reserved; used with permission (not MIT/Apache) | +| `docs/brand/artcraft-mark-black.png` | ArtCraft team | Original work: first-party ArtCraft brand mark (ArtCraft symbol ("A" mark), black) | ArtCraft trademark, all rights reserved; used with permission (not MIT/Apache) | +| `docs/brand/artcraft-mark-black.svg` | ArtCraft team | Original work: first-party ArtCraft brand mark (ArtCraft symbol ("A" mark), black) | ArtCraft trademark, all rights reserved; used with permission (not MIT/Apache) | +| `docs/brand/artcraft-mark.png` | ArtCraft team | Original work: first-party ArtCraft brand mark (ArtCraft symbol ("A" mark), blue) | ArtCraft trademark, all rights reserved; used with permission (not MIT/Apache) | +| `docs/brand/artcraft-mark.svg` | ArtCraft team | Original work: first-party ArtCraft brand mark (ArtCraft symbol ("A" mark), blue) | ArtCraft trademark, all rights reserved; used with permission (not MIT/Apache) | diff --git a/Cargo.toml b/Cargo.toml index 4165cfef..fe25634f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = ["crates/*", "apps/*"] +members = ["crates/*", "apps/*", "xtask"] default-members = ["crates/*", "apps/*"] [workspace.package] diff --git a/docs/brand/artcraft-logo-white.png.attribution b/docs/brand/artcraft-logo-white.png.attribution new file mode 100644 index 00000000..98528540 --- /dev/null +++ b/docs/brand/artcraft-logo-white.png.attribution @@ -0,0 +1,8 @@ +asset: artcraft-logo-white.png +title: ArtCraft wordmark, light ink for dark backgrounds +author: ArtCraft team (Storyteller) +source: original work (first-party ArtCraft brand asset supplied by the ArtCraft team) +license: LicenseRef-ArtCraft-Trademark — ArtCraft name and logo are trademarks of the ArtCraft team, all rights reserved; used in EffectCraft with permission. Not covered by the project's MIT OR Apache-2.0 licence. +license-file: +added: 2026-10-01 by Claude (agent) for Brandon Thomas +notes: First-party ArtCraft brand mark kept in docs/brand for the README and brand use: the one exception to the open-licence rule (AGENTS.md §1.8). Forks and redistributions must remove or replace it unless they have permission. diff --git a/docs/brand/artcraft-logo-white.svg.attribution b/docs/brand/artcraft-logo-white.svg.attribution new file mode 100644 index 00000000..e526cefc --- /dev/null +++ b/docs/brand/artcraft-logo-white.svg.attribution @@ -0,0 +1,8 @@ +asset: artcraft-logo-white.svg +title: ArtCraft wordmark, light ink for dark backgrounds +author: ArtCraft team (Storyteller) +source: original work (first-party ArtCraft brand asset supplied by the ArtCraft team) +license: LicenseRef-ArtCraft-Trademark — ArtCraft name and logo are trademarks of the ArtCraft team, all rights reserved; used in EffectCraft with permission. Not covered by the project's MIT OR Apache-2.0 licence. +license-file: +added: 2026-10-01 by Claude (agent) for Brandon Thomas +notes: First-party ArtCraft brand mark kept in docs/brand for the README and brand use: the one exception to the open-licence rule (AGENTS.md §1.8). Forks and redistributions must remove or replace it unless they have permission. diff --git a/docs/brand/artcraft-logo.png.attribution b/docs/brand/artcraft-logo.png.attribution new file mode 100644 index 00000000..80558c89 --- /dev/null +++ b/docs/brand/artcraft-logo.png.attribution @@ -0,0 +1,8 @@ +asset: artcraft-logo.png +title: ArtCraft wordmark (symbol + "ARTCRAFT"), dark ink for light backgrounds +author: ArtCraft team (Storyteller) +source: original work (first-party ArtCraft brand asset supplied by the ArtCraft team) +license: LicenseRef-ArtCraft-Trademark — ArtCraft name and logo are trademarks of the ArtCraft team, all rights reserved; used in EffectCraft with permission. Not covered by the project's MIT OR Apache-2.0 licence. +license-file: +added: 2026-10-01 by Claude (agent) for Brandon Thomas +notes: First-party ArtCraft brand mark kept in docs/brand for the README and brand use: the one exception to the open-licence rule (AGENTS.md §1.8). Forks and redistributions must remove or replace it unless they have permission. diff --git a/docs/brand/artcraft-logo.svg.attribution b/docs/brand/artcraft-logo.svg.attribution new file mode 100644 index 00000000..7f03e5ad --- /dev/null +++ b/docs/brand/artcraft-logo.svg.attribution @@ -0,0 +1,8 @@ +asset: artcraft-logo.svg +title: ArtCraft wordmark (symbol + "ARTCRAFT"), dark ink for light backgrounds +author: ArtCraft team (Storyteller) +source: original work (first-party ArtCraft brand asset supplied by the ArtCraft team) +license: LicenseRef-ArtCraft-Trademark — ArtCraft name and logo are trademarks of the ArtCraft team, all rights reserved; used in EffectCraft with permission. Not covered by the project's MIT OR Apache-2.0 licence. +license-file: +added: 2026-10-01 by Claude (agent) for Brandon Thomas +notes: First-party ArtCraft brand mark kept in docs/brand for the README and brand use: the one exception to the open-licence rule (AGENTS.md §1.8). Forks and redistributions must remove or replace it unless they have permission. diff --git a/docs/brand/artcraft-mark-black.png.attribution b/docs/brand/artcraft-mark-black.png.attribution new file mode 100644 index 00000000..bdff9405 --- /dev/null +++ b/docs/brand/artcraft-mark-black.png.attribution @@ -0,0 +1,8 @@ +asset: artcraft-mark-black.png +title: ArtCraft symbol ("A" mark), black +author: ArtCraft team (Storyteller) +source: original work (first-party ArtCraft brand asset supplied by the ArtCraft team) +license: LicenseRef-ArtCraft-Trademark — ArtCraft name and logo are trademarks of the ArtCraft team, all rights reserved; used in EffectCraft with permission. Not covered by the project's MIT OR Apache-2.0 licence. +license-file: +added: 2026-10-01 by Claude (agent) for Brandon Thomas +notes: First-party ArtCraft brand mark kept in docs/brand for the README and brand use: the one exception to the open-licence rule (AGENTS.md §1.8). Forks and redistributions must remove or replace it unless they have permission. diff --git a/docs/brand/artcraft-mark-black.svg.attribution b/docs/brand/artcraft-mark-black.svg.attribution new file mode 100644 index 00000000..f0f74087 --- /dev/null +++ b/docs/brand/artcraft-mark-black.svg.attribution @@ -0,0 +1,8 @@ +asset: artcraft-mark-black.svg +title: ArtCraft symbol ("A" mark), black +author: ArtCraft team (Storyteller) +source: original work (first-party ArtCraft brand asset supplied by the ArtCraft team) +license: LicenseRef-ArtCraft-Trademark — ArtCraft name and logo are trademarks of the ArtCraft team, all rights reserved; used in EffectCraft with permission. Not covered by the project's MIT OR Apache-2.0 licence. +license-file: +added: 2026-10-01 by Claude (agent) for Brandon Thomas +notes: First-party ArtCraft brand mark kept in docs/brand for the README and brand use: the one exception to the open-licence rule (AGENTS.md §1.8). Forks and redistributions must remove or replace it unless they have permission. diff --git a/docs/brand/artcraft-mark.png.attribution b/docs/brand/artcraft-mark.png.attribution new file mode 100644 index 00000000..5f1953a4 --- /dev/null +++ b/docs/brand/artcraft-mark.png.attribution @@ -0,0 +1,8 @@ +asset: artcraft-mark.png +title: ArtCraft symbol ("A" mark), blue +author: ArtCraft team (Storyteller) +source: original work (first-party ArtCraft brand asset supplied by the ArtCraft team) +license: LicenseRef-ArtCraft-Trademark — ArtCraft name and logo are trademarks of the ArtCraft team, all rights reserved; used in EffectCraft with permission. Not covered by the project's MIT OR Apache-2.0 licence. +license-file: +added: 2026-10-01 by Claude (agent) for Brandon Thomas +notes: First-party ArtCraft brand mark kept in docs/brand for the README and brand use: the one exception to the open-licence rule (AGENTS.md §1.8). Forks and redistributions must remove or replace it unless they have permission. diff --git a/docs/brand/artcraft-mark.svg.attribution b/docs/brand/artcraft-mark.svg.attribution new file mode 100644 index 00000000..58c2ae4d --- /dev/null +++ b/docs/brand/artcraft-mark.svg.attribution @@ -0,0 +1,8 @@ +asset: artcraft-mark.svg +title: ArtCraft symbol ("A" mark), blue +author: ArtCraft team (Storyteller) +source: original work (first-party ArtCraft brand asset supplied by the ArtCraft team) +license: LicenseRef-ArtCraft-Trademark — ArtCraft name and logo are trademarks of the ArtCraft team, all rights reserved; used in EffectCraft with permission. Not covered by the project's MIT OR Apache-2.0 licence. +license-file: +added: 2026-10-01 by Claude (agent) for Brandon Thomas +notes: First-party ArtCraft brand mark kept in docs/brand for the README and brand use: the one exception to the open-licence rule (AGENTS.md §1.8). Forks and redistributions must remove or replace it unless they have permission. diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 00000000..df7dfa87 --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "xtask" +version.workspace = true +edition.workspace = true +license.workspace = true +publish = false + +[dependencies] +serde_json = { workspace = true } diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 00000000..92d6d64b --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,251 @@ +//! Workspace automation: `cargo xtask `. +//! +//! - `layers`: enforces the dependency layering of `docs/architecture.md` §1 (downward-only edges, +//! listed same-layer edges, no UI/OS crates below L5). +//! - `wasm`: `cargo check --target wasm32-unknown-unknown` for every crate in L0–L4 and the egui UI. +//! - `assets`: every asset file (image, icon, font, LUT, audio, video…) has a complete +//! `.attribution` sidecar and an entry in `ATTRIBUTION.md` (AGENTS.md §1). +//! - `ci`: fmt check, clippy -D warnings, tests, layers, assets, wasm. + +use std::process::{Command, ExitCode}; + +use serde_json::Value; + +/// (crate name without the `effectcraft-` prefix, layer). See `docs/architecture.md` §1. +const LAYERS: &[(&str, u8)] = &[ + ("time", 0), + ("geom", 0), + ("color", 0), + ("testkit", 0), + ("raster", 1), + ("keyframe", 1), + ("path", 1), + ("project", 2), + ("text", 2), + ("effects", 2), + ("render", 3), + ("media", 3), + ("expr", 3), + ("export", 3), + ("gpu", 3), + ("lottie", 3), + ("format", 3), + ("engine", 4), + ("host", 4), + ("ui-egui", 5), + ("automation", 5), + ("effectcraft", 6), + ("cli", 6), + ("web", 6), +]; + +/// Allowed same-layer edges (from, to). +const SAME_LAYER: &[(&str, &str)] = &[ + ("path", "keyframe"), + ("path", "raster"), + ("text", "path"), + ("effects", "project"), + ("effects", "text"), + ("effects", "path"), + ("media", "render"), + ("expr", "render"), + ("export", "render"), + ("export", "media"), + ("gpu", "render"), + ("lottie", "format"), + ("host", "engine"), + ("cli", "effectcraft"), +]; + +/// Crates that must not appear below L5 (UI toolkits, windowing, OS audio/menus). +const UI_ONLY: &[&str] = &["egui", "eframe", "egui-wgpu", "winit", "rfd", "cpal", "muda"]; + +fn short(name: &str) -> &str { + name.strip_prefix("effectcraft-").unwrap_or(name) +} + +fn layer_of(name: &str) -> Option { + LAYERS.iter().find(|(n, _)| *n == short(name)).map(|(_, l)| *l) +} + +fn metadata() -> Result { + let out = Command::new(env!("CARGO")).args(["metadata", "--format-version", "1", "--no-deps"]).output().map_err(|e| e.to_string())?; + if !out.status.success() { + return Err(String::from_utf8_lossy(&out.stderr).into()); + } + serde_json::from_slice(&out.stdout).map_err(|e| e.to_string()) +} + +fn workspace_crates(md: &Value) -> Vec<(String, Vec)> { + md["packages"] + .as_array() + .into_iter() + .flatten() + .filter(|p| p["name"] != "xtask") + .map(|p| { + let deps = p["dependencies"] + .as_array() + .into_iter() + .flatten() + .filter(|d| d["kind"].is_null() || d["kind"] == "build") + .filter_map(|d| d["name"].as_str().map(str::to_string)) + .collect(); + (p["name"].as_str().unwrap_or_default().to_string(), deps) + }) + .collect() +} + +fn layers() -> Result<(), String> { + let md = metadata()?; + let mut errors = Vec::new(); + for (name, deps) in workspace_crates(&md) { + let Some(l) = layer_of(&name) else { + errors.push(format!("{name}: not assigned a layer (add it to xtask LAYERS and docs/architecture.md §1)")); + continue; + }; + for d in &deps { + if l < 5 && UI_ONLY.contains(&d.as_str()) { + errors.push(format!("{name} (L{l}) depends on UI/OS crate `{d}`")); + } + if !d.starts_with("effectcraft-") { + continue; + } + let Some(dl) = layer_of(d) else { continue }; + let (a, b) = (short(&name), short(d)); + if dl > l { + errors.push(format!("{a} (L{l}) depends upward on {b} (L{dl})")); + } else if dl == l && l > 0 && !SAME_LAYER.contains(&(a, b)) { + errors.push(format!("{a} → {b}: same-layer edge (L{l}) not in the allowed list")); + } + } + } + if errors.is_empty() { + println!("layers: ok"); + Ok(()) + } else { + Err(errors.join("\n")) + } +} + +/// File extensions that count as assets (AGENTS.md §1). +const ASSET_EXT: &[&str] = &[ + "png", "jpg", "jpeg", "gif", "webp", "svg", "ico", "icns", "bmp", "tif", "tiff", "heic", "avif", "exr", "ttf", "otf", "ttc", "woff", "woff2", "cube", + "3dl", "lut", "look", "wav", "mp3", "aac", "flac", "ogg", "opus", "m4a", "aif", "aiff", "mp4", "mov", "m4v", "mkv", "webm", "avi", "mxf", "psd", "ai", + "eps", "pdf", "prproj", "ffx", "prfpset", "mogrt", "aep", +]; + +/// Sidecar fields that must be present and non-empty. +const REQUIRED_FIELDS: &[&str] = &["asset", "title", "author", "source", "license", "added"]; + +fn repo_files() -> Result, String> { + let out = Command::new("git").args(["ls-files", "--cached", "--others", "--exclude-standard"]).output().map_err(|e| e.to_string())?; + if !out.status.success() { + return Err(String::from_utf8_lossy(&out.stderr).into()); + } + Ok(String::from_utf8_lossy(&out.stdout).lines().map(str::to_string).filter(|f| std::path::Path::new(f).exists()).collect()) +} + +fn is_asset(path: &str) -> bool { + std::path::Path::new(path).extension().and_then(|e| e.to_str()).is_some_and(|e| ASSET_EXT.contains(&e.to_ascii_lowercase().as_str())) +} + +fn assets() -> Result<(), String> { + let files = repo_files()?; + let index = std::fs::read_to_string("ATTRIBUTION.md").map_err(|e| format!("ATTRIBUTION.md: {e}"))?; + let mut errors = Vec::new(); + let mut n = 0; + for f in files.iter().filter(|f| is_asset(f)) { + n += 1; + let lower = f.to_ascii_lowercase(); + if lower.contains("adobe") || lower.contains("aftereffects") || lower.contains("after-effects") { + errors.push(format!("{f}: asset paths must not reference Adobe/After Effects (AGENTS.md §1)")); + } + let side = format!("{f}.attribution"); + match std::fs::read_to_string(&side) { + Err(_) => errors.push(format!("{f}: missing attribution sidecar {side}")), + Ok(text) => { + for field in REQUIRED_FIELDS { + let ok = text.lines().any(|l| l.split_once(':').is_some_and(|(k, v)| k.trim() == *field && !v.trim().is_empty())); + if !ok { + errors.push(format!("{side}: field `{field}` missing or empty")); + } + } + let lic = text.lines().find_map(|l| l.split_once(':').filter(|(k, _)| k.trim() == "license").map(|(_, v)| v.trim().to_ascii_lowercase())); + if lic.is_some_and(|l| l.contains("-nc") || l.contains("-nd") || l.contains("adobe") || l.contains("proprietary")) { + errors.push(format!("{side}: licence not allowed (no NC/ND, Adobe or proprietary licences)")); + } + } + } + if !index.contains(&format!("`{f}`")) { + errors.push(format!("{f}: not listed in ATTRIBUTION.md")); + } + } + for f in files.iter().filter(|f| f.ends_with(".attribution")) { + let asset = f.trim_end_matches(".attribution"); + if !std::path::Path::new(asset).exists() { + errors.push(format!("{f}: sidecar for a file that does not exist")); + } + } + if errors.is_empty() { + println!("assets: ok ({n} assets attributed)"); + Ok(()) + } else { + Err(errors.join("\n")) + } +} + +fn run(cmd: &mut Command) -> Result<(), String> { + eprintln!("$ {cmd:?}"); + let st = cmd.status().map_err(|e| e.to_string())?; + if st.success() { Ok(()) } else { Err(format!("failed: {cmd:?}")) } +} + +/// Crates above L4 that must also build for the web. +const WEB_CRATES: &[&str] = &["effectcraft-ui-egui"]; + +fn wasm() -> Result<(), String> { + let md = metadata()?; + let mut cmd = Command::new(env!("CARGO")); + cmd.args(["check", "--target", "wasm32-unknown-unknown"]); + let mut n = 0; + for (name, _) in workspace_crates(&md) { + if layer_of(&name).is_some_and(|l| l <= 4) || WEB_CRATES.contains(&name.as_str()) { + cmd.args(["-p", &name]); + n += 1; + } + } + if n == 0 { + return Ok(()); + } + run(&mut cmd)?; + println!("wasm: ok ({n} crates)"); + Ok(()) +} + +fn ci() -> Result<(), String> { + let cargo = env!("CARGO"); + run(Command::new(cargo).args(["fmt", "--check"]))?; + run(Command::new(cargo).args(["clippy", "--workspace", "--all-targets", "--release", "--", "-D", "warnings"]))?; + run(Command::new(cargo).args(["test", "--workspace", "--release"]))?; + layers()?; + assets()?; + wasm() +} + +fn main() -> ExitCode { + let task = std::env::args().nth(1).unwrap_or_default(); + let r = match task.as_str() { + "layers" => layers(), + "wasm" => wasm(), + "assets" => assets(), + "ci" => ci(), + _ => Err("usage: cargo xtask ".into()), + }; + match r { + Ok(()) => ExitCode::SUCCESS, + Err(e) => { + eprintln!("{e}"); + ExitCode::FAILURE + } + } +}