Scaffold the Tessera Rust project workspace at /home/gjermund/projects/tessera. ## What to Create ### 1. Workspace Cargo.toml At /home/gjermund/projects/tessera/Cargo.toml: - [workspace] with members: crates/tessera-core, crates/tessera-api - [workspace.dependencies] with versions for: - axum 0.8.x - tokio 1.x (full features) - serde 1.x (derive feature) - serde_json 1.x - sqlx 0.8.x (runtime-tokio, tls-rustls, postgres features) - uuid 1.x (v4, serde features) - chrono 0.4.x (serde feature) - tracing 0.1.x, tracing-subscriber 0.3.x - tower 0.5.x, tower-http 0.6.x (cors feature) - tonic 0.12.x - prost 0.13.x - anyhow 1.x - thiserror 2.x - lettre 0.11.x (rustls-tls feature) - tera 1.x - figment 0.10.x (toml, env features) - dotenvy 0.15.x - clap 4.x (derive feature) ### 2. tessera-core crate At /home/gjermund/projects/tessera/crates/tessera-core/Cargo.toml: - name = "tessera-core" - Dependencies: serde, serde_json, sqlx, uuid, chrono, anyhow, thiserror, tracing, tera, lettre - [lib] crate-type = ["lib"] At /home/gjermund/projects/tessera/crates/tessera-core/src/lib.rs: - Module declarations: pub mod models; pub mod scrip; pub mod lifecycle; pub mod query; pub mod db; - Re-export key types Create these source files (with proper module structure): #### crates/tessera-core/src/models/mod.rs - Module declarations: mod ticket; mod queue; mod transaction; mod scrip; mod template; mod custom_field; mod user; - Re-exports #### crates/tessera-core/src/models/ticket.rs - Ticket struct with fields: id (Uuid), subject (String), queue_id (Uuid), status (String), owner_id (Option), creator_id (Uuid), created_at (DateTime), updated_at (DateTime), started_at (Option>), resolved_at (Option>) #### crates/tessera-core/src/models/queue.rs - Queue struct: id (Uuid), name (String), description (Option), lifecycle_id (Option), created_at (DateTime) #### crates/tessera-core/src/models/transaction.rs - Transaction struct: id (Uuid), ticket_id (Uuid), transaction_type (String), field (Option), old_value (Option), new_value (Option), data (Option), creator_id (Uuid), created_at (DateTime) #### crates/tessera-core/src/models/scrip.rs - Scrip struct: id (Uuid), queue_id (Option), name (String), description (Option), condition_type (String), condition_config (serde_json::Value), action_type (String), action_config (serde_json::Value), template_id (Option), stage (String), sort_order (i32), disabled (bool), created_at (DateTime) - ScripStage enum: TransactionCreate, TransactionBatch #### crates/tessera-core/src/models/template.rs - Template struct: id (Uuid), name (String), queue_id (Option), subject_template (String), body_template (String), created_at (DateTime) #### crates/tessera-core/src/models/custom_field.rs - CustomField struct: id (Uuid), name (String), field_type (String), values (Option), max_values (i32), pattern (Option), created_at (DateTime) - QueueCustomField struct: id (Uuid), queue_id (Uuid), custom_field_id (Uuid), sort_order (i32) - CustomFieldValue struct: id (Uuid), custom_field_id (Uuid), ticket_id (Uuid), value (String), created_at (DateTime) #### crates/tessera-core/src/models/user.rs - User struct: id (Uuid), username (String), email (Option), created_at (DateTime) #### crates/tessera-core/src/scrip/mod.rs - Module declarations: mod engine; mod conditions; mod actions; mod templates; - Re-export ScripEngine, PreparedScrip #### crates/tessera-core/src/scrip/engine.rs - ScripEngine struct with methods: - new(pool: PgPool) - async fn prepare(&self, ticket_id: Uuid, transactions: &[Transaction]) -> Result> - Load matching scrips from DB (global + queue-specific, filtered by transaction types matching condition_type, sorted by sort_order) - For each: evaluate condition → if matches, build PreparedScrip with template substitution - async fn commit(&self, prepared: Vec) -> Result> - Execute each action, record results - PreparedScrip struct: scrip_id, scrip_name, action_type, action_payload (serde_json::Value), dry_run bool - ScripResult struct: scrip_id, success bool, message String #### crates/tessera-core/src/scrip/conditions.rs - ConditionEvaluator trait with fn evaluate(&self, ticket: &Ticket, transactions: &[Transaction]) -> bool - OnCreate condition (true if any transaction is type "Create") - OnStatusChange condition (true if any transaction is type "StatusChange") - OnResolve condition (true if any transaction changes status to a "resolved" lifecycle state) #### crates/tessera-core/src/scrip/actions.rs - ActionExecutor trait with async fn execute(&self, payload: &serde_json::Value) -> Result - SendEmail action (placeholder — logs the email it would send) - Webhook action (placeholder — logs the webhook it would fire) - SetCustomField action (placeholder — logs the CF it would set) #### crates/tessera-core/src/scrip/templates.rs - TemplateRenderer struct that uses Tera - async fn render(&self, template: &Template, context: &serde_json::Value) -> Result<(String, String)> — returns (subject, body) #### crates/tessera-core/src/lifecycle/mod.rs - Lifecycle struct: id (Uuid), name (String), definition (serde_json::Value) - LifecycleValidator struct with methods: - fn validate_transition(&self, lifecycle_def: &serde_json::Value, from: &str, to: &str) -> Result<()> - fn is_valid_status(&self, lifecycle_def: &serde_json::Value, status: &str) -> bool #### crates/tessera-core/src/query/mod.rs - Placeholder module with comment: "TicketSQL query builder — post-MVP" #### crates/tessera-core/src/db/mod.rs - pub mod migrations; - Pub async fn run_migrations(pool: &PgPool) -> Result<()> #### crates/tessera-core/src/db/migrations.rs - Function signatures for migration runner (placeholder — actual migrations in sql files) ### 3. tessera-api crate At /home/gjermund/projects/tessera/crates/tessera-api/Cargo.toml: - name = "tessera-api" - Dependencies: tessera-core (path), axum, tokio, serde, serde_json, uuid, anyhow, tracing, tracing-subscriber, tower-http, clap, figment, dotenvy - [[bin]] name = "tessera-api" At /home/gjermund/projects/tessera/crates/tessera-api/src/main.rs: - Clap CLI with subcommands: serve, migrate - serve: start axum HTTP server - migrate: run database migrations - tracing_subscriber init with JSON formatting - Load config from TESSERA_CONFIG env or tessera.toml Create source files: #### crates/tessera-api/src/routes/mod.rs - Module declarations: mod health; mod tickets; mod queues; mod scrips; mod custom_fields; mod lifecycles; #### crates/tessera-api/src/routes/health.rs - GET /health → 200 with {"status": "ok", "version": "0.1.0"} #### crates/tessera-api/src/routes/tickets.rs - Router function returning axum Router with placeholder handlers: - GET /api/tickets - POST /api/tickets - GET /api/tickets/:id - PATCH /api/tickets/:id - POST /api/tickets/:id/preview - GET /api/tickets/:id/transactions - POST /api/tickets/:id/comment - Each handler returns 501 NotImplemented with a descriptive message #### crates/tessera-api/src/routes/queues.rs - GET /api/queues → 501 - POST /api/queues → 501 #### crates/tessera-api/src/routes/scrips.rs - GET /api/scrips → 501 - POST /api/scrips → 501 - GET /api/scrips/:id → 501 - PATCH /api/scrips/:id → 501 #### crates/tessera-api/src/routes/custom_fields.rs - GET /api/custom-fields → 501 - POST /api/custom-fields → 501 #### crates/tessera-api/src/routes/lifecycles.rs - GET /api/lifecycles → 501 - POST /api/lifecycles → 501 #### crates/tessera-api/src/config.rs - AppConfig struct: database_url (String), server_host (String), server_port (u16) - impl from figment ### 4. SQL Migrations At /home/gjermund/projects/tessera/migrations/0001_initial_schema.sql: - All CREATE TABLE statements from the architecture document (tickets, queues, lifecycles, transactions, scrips, templates, custom_fields, queue_custom_fields, custom_field_values, users) - CREATE INDEX statements on: tickets(queue_id), tickets(status), transactions(ticket_id), transactions(created_at), scrips(queue_id), custom_field_values(ticket_id), custom_field_values(custom_field_id) ### 5. Configuration At /home/gjermund/projects/tessera/tessera.example.toml: ```toml [database] url = "postgres://tessera:tessera@localhost:5432/tessera" [server] host = "127.0.0.1" port = 8080 ``` ### 6. .gitignore Standard Rust .gitignore: target/, .env, *.log ## Rules - Use `cargo init` where appropriate, or create files manually - All code must compile: run `cargo check --workspace` after creating everything - Fix any compilation errors - Use `cargo fmt` and `cargo clippy` on the workspace - Do NOT create a separate Cargo.lock — it will be generated - The workspace Cargo.toml must NOT have a [package] section ## Verification After creating all files, run: 1. `cargo check --workspace` — must succeed 2. `cargo fmt --check` — must pass 3. `cargo clippy --workspace` — should pass or have only minor warnings If any verification fails, fix the issues and re-run until clean.