commit 10c6af37363b6fa34b338a5353851a63275f73da Author: gilesb Date: Tue Jan 6 23:19:04 2026 +0000 feat: initial recipes repo with identity-cat - Recipe schema using artdag primitives (SOURCE, EFFECT) - identity-cat: applies identity effect to cat image - Test verifying identity property: output_hash == input_hash - Demonstrates content-addressed DAG execution Owner: @giles@artdag.rose-ash.com šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e777ab8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.py[cod] +.env +.venv diff --git a/README.md b/README.md new file mode 100644 index 0000000..304d90c --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# Art DAG Recipes + +Recipes that transform assets using effects from [art-dag](https://github.com/gilesbradshaw/art-dag). + +## Structure + +``` +recipes/ +└── identity-cat/ + ā”œā”€ā”€ recipe.yaml # DAG definition + └── README.md # Description +``` + +## Registry References + +Recipes reference assets and effects by content hash from: +- **Assets**: https://github.com/gilesbradshaw/art-dag/blob/main/registry/registry.json +- **Effects**: https://github.com/gilesbradshaw/art-dag/tree/main/effects + +## Recipe Schema + +A recipe is a DAG with: +- `name`: Unique recipe identifier +- `inputs`: Assets referenced by content_hash +- `nodes`: DAG nodes using primitives (SOURCE, TRANSFORM, etc.) and effects +- `output`: Expected output hash (for verification) + +## Owner + +Recipes owned by `@giles@artdag.rose-ash.com` diff --git a/recipes/identity-cat/README.md b/recipes/identity-cat/README.md new file mode 100644 index 0000000..693b16f --- /dev/null +++ b/recipes/identity-cat/README.md @@ -0,0 +1,29 @@ +# identity-cat + +The simplest possible recipe: apply the identity effect to the foundational cat image. + +## DAG + +``` +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ SOURCE(cat) │ ──▶ │ EFFECT(identity)│ ──▶ │ output │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + 33268b... passthrough 33268b... +``` + +## Properties + +Since `identity(x) = x`: +- Input hash: `33268b6e167deaf018cc538de12dbe562612b33e89a749391cef855b320a269b` +- Output hash: `33268b6e167deaf018cc538de12dbe562612b33e89a749391cef855b320a269b` + +The hashes are identical, proving the identity property. + +## References + +- **Asset**: [cat](https://rose-ash.com/content/images/2026/01/cat.jpg) +- **Effect**: [identity](https://github.com/gilesbradshaw/art-dag/tree/main/effects/identity) + +## Owner + +`@giles@artdag.rose-ash.com` diff --git a/recipes/identity-cat/recipe.yaml b/recipes/identity-cat/recipe.yaml new file mode 100644 index 0000000..0a6abb8 --- /dev/null +++ b/recipes/identity-cat/recipe.yaml @@ -0,0 +1,43 @@ +# identity-cat recipe +# Applies the identity effect to the foundational cat image +# Demonstrates: SOURCE → EFFECT → output + +name: identity-cat +version: "1.0" +description: "Apply identity effect to cat - output equals input" + +# Registry references (by content hash) +registry: + assets: + cat: + hash: "33268b6e167deaf018cc538de12dbe562612b33e89a749391cef855b320a269b" + url: "https://rose-ash.com/content/images/2026/01/cat.jpg" + effects: + identity: + hash: "640ea11ee881ebf4101af0a955439105ab11e763682b209e88ea08fc66e1cc03" + url: "https://github.com/gilesbradshaw/art-dag/tree/main/effects/identity" + +# DAG definition +dag: + nodes: + - id: source_cat + type: SOURCE + config: + asset: cat + + - id: apply_identity + type: EFFECT + config: + effect: identity + inputs: + - source_cat + + output: apply_identity + +# Verification +output: + # Identity property: output hash equals input hash + expected_hash: "33268b6e167deaf018cc538de12dbe562612b33e89a749391cef855b320a269b" + +# Ownership +owner: "@giles@artdag.rose-ash.com" diff --git a/test_identity_cat.py b/test_identity_cat.py new file mode 100644 index 0000000..5931162 --- /dev/null +++ b/test_identity_cat.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +""" +Test the identity-cat recipe. + +Verifies that identity(cat) produces the same content hash as cat. +""" + +import hashlib +import sys +import tempfile +from pathlib import Path + +# Add artdag to path +sys.path.insert(0, str(Path(__file__).parent.parent / "artdag")) + +from artdag.dag import DAG, Node, NodeType +from artdag.engine import Engine +from artdag import nodes # Register executors + + +def file_hash(path: Path) -> str: + """Compute SHA3-256 hash of a file.""" + hasher = hashlib.sha3_256() + # Resolve symlinks to get actual content + actual_path = path.resolve() if path.is_symlink() else path + with open(actual_path, "rb") as f: + for chunk in iter(lambda: f.read(65536), b""): + hasher.update(chunk) + return hasher.hexdigest() + + +def main(): + # Cat image location (from registry) + cat_url = "https://rose-ash.com/content/images/2026/01/cat.jpg" + cat_hash = "33268b6e167deaf018cc538de12dbe562612b33e89a749391cef855b320a269b" + + # Try to find cat locally + cat_paths = [ + Path("/home/giles/Pictures/cat.jpg"), + Path.home() / "Pictures" / "cat.jpg", + ] + + cat_path = None + for p in cat_paths: + if p.exists(): + cat_path = p + break + + if not cat_path: + print(f"Cat image not found locally. Download from: {cat_url}") + sys.exit(1) + + # Verify input hash + input_hash = file_hash(cat_path) + print(f"Input: {cat_path}") + print(f"Input hash: {input_hash}") + + if input_hash != cat_hash: + print(f"WARNING: Input hash doesn't match registry!") + print(f"Expected: {cat_hash}") + + # Build the DAG: SOURCE -> EFFECT(identity) -> output + dag = DAG() + + # Node 1: SOURCE(cat) + source_node = Node( + node_type=NodeType.SOURCE, + config={"path": str(cat_path)}, + inputs=[], + name="source_cat", + ) + source_id = dag.add_node(source_node) + + # Node 2: EFFECT(identity) + effect_node = Node( + node_type="EFFECT", + config={"effect": "identity"}, + inputs=[source_id], + name="apply_identity", + ) + effect_id = dag.add_node(effect_node) + + dag.set_output(effect_id) + + # Execute + with tempfile.TemporaryDirectory() as cache_dir: + engine = Engine(cache_dir) + result = engine.execute(dag) + + if not result.success: + print(f"FAILED: {result.error}") + sys.exit(1) + + # Verify output hash + output_hash = file_hash(result.output_path) + print(f"\nOutput: {result.output_path}") + print(f"Output hash: {output_hash}") + + # The identity property: output hash == input hash + if output_hash == input_hash: + print("\nāœ“ PASS: identity(cat) == cat") + print(" Output hash equals input hash - identity property verified!") + else: + print("\nāœ— FAIL: identity(cat) != cat") + print(f" Expected: {input_hash}") + print(f" Got: {output_hash}") + sys.exit(1) + + +if __name__ == "__main__": + main()