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 <[email protected]>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-01 20:49:29 -04:00
1 parent dee16181fe
commit 9f0cfb3373
14 files changed
+348 -1

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
[alias]
xtask = "run --quiet --package xtask --"
+5
View File
@@ -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
+16
View File
@@ -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) |
+1 -1
View File
@@ -1,6 +1,6 @@
[workspace]
resolver = "3"
members = ["crates/*", "apps/*"]
members = ["crates/*", "apps/*", "xtask"]
default-members = ["crates/*", "apps/*"]
[workspace.package]
@@ -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.
@@ -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.
+8
View File
@@ -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.
+8
View File
@@ -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.
@@ -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.
@@ -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.
+8
View File
@@ -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.
+8
View File
@@ -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.
+9
View File
@@ -0,0 +1,9 @@
[package]
name = "xtask"
version.workspace = true
edition.workspace = true
license.workspace = true
publish = false
[dependencies]
serde_json = { workspace = true }
+251
View File
@@ -0,0 +1,251 @@
//! Workspace automation: `cargo xtask <layers|assets|wasm|ci>`.
//!
//! - `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
//! `<file>.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<u8> {
LAYERS.iter().find(|(n, _)| *n == short(name)).map(|(_, l)| *l)
}
fn metadata() -> Result<Value, String> {
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<String>)> {
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<Vec<String>, 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 <layers|assets|wasm|ci>".into()),
};
match r {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("{e}");
ExitCode::FAILURE
}
}
}