Skip to main content

max / goingson

Add CONTRIBUTING.md Extract coding patterns (Rust-does-filtering, pre-computed fields, JS namespace, form builder, repository traits) from CLAUDE.md into a human-readable contributor guide. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-04-15 22:44 UTC
Commit: 75165488c1abfd6a19e2527d1030e5b6aa10c0c3
Parent: b8d7d23
1 file changed, +271 insertions, -0 deletions
@@ -0,0 +1,271 @@
1 + # Contributing to GoingsOn
2 +
3 + Patterns, conventions, and rules for working on the GoingsOn codebase.
4 +
5 + ## Project Structure
6 +
7 + ```
8 + goingson/ (workspace root)
9 + Cargo.toml # Workspace definition
10 + crates/
11 + core/ # Domain models, business logic, repository traits
12 + db-sqlite/ # SQLite repository implementations
13 + goingson-mcp/ # MCP server for AI task management
14 + plugin-runtime/ # Rhai plugin system
15 + src-tauri/
16 + src/
17 + main.rs # Tauri setup, command registration, background services
18 + commands/ # Tauri commands (thin wrappers)
19 + state.rs # AppState, sync client management
20 + sync_service.rs # SyncKit change tracking and push/pull
21 + frontend/
22 + js/ # JavaScript modules (IIFE pattern)
23 + css/ # Styles (edit styles.css, never styles.min.css)
24 + build-css.sh # CSS minification script
25 + tauri.conf.json # Tauri configuration
26 + ```
27 +
28 + ### Crate Boundaries
29 +
30 + | Crate | Role | May depend on |
31 + |-------|------|---------------|
32 + | `core` | Models, validation, urgency, recurrence, repository traits | Nothing internal |
33 + | `db-sqlite` | SQLite implementations of repository traits | `core` |
34 + | `plugin-runtime` | Rhai plugin system | `core` |
35 + | `goingson-mcp` | MCP server for AI integration | `core`, `db-sqlite` |
36 + | `src-tauri` | Tauri app, commands, state | All crates |
37 +
38 + **Strict rule:** Business logic lives in `core`. Repository implementations in `db-sqlite`. Commands in `src-tauri` are thin wrappers. JavaScript never duplicates logic that exists in Rust.
39 +
40 + ## Rust Does the Heavy Lifting
41 +
42 + This is the most important architectural rule. All data filtering, sorting, computation, and validation happens in Rust. JavaScript only renders pre-computed data and handles UI interactions.
43 +
44 + **Bad:**
45 + ```javascript
46 + // DON'T: Filter in JS
47 + const tasks = await GoingsOn.api.tasks.list();
48 + const filtered = tasks.filter(t => t.status === 'pending' && !isSnoozed(t));
49 + ```
50 +
51 + **Good:**
52 + ```javascript
53 + // DO: Send filter criteria to Rust, render the result
54 + const tasks = await GoingsOn.api.tasks.listFiltered({ status: 'pending', showSnoozed: false });
55 + ```
56 +
57 + ## Pre-Computed Response Fields
58 +
59 + Response structs include computed fields so JavaScript doesn't recalculate:
60 +
61 + ```rust
62 + #[derive(Serialize)]
63 + #[serde(rename_all = "camelCase")]
64 + pub struct TaskResponse {
65 + // Basic fields from DB
66 + pub id: String,
67 + pub description: String,
68 + pub due: Option<String>,
69 +
70 + // PRE-COMPUTED:
71 + pub is_snoozed: bool, // snoozed_until > now
72 + pub is_overdue: bool, // due < now
73 + pub urgency_class: String, // "overdue", "high", "medium", "low"
74 + pub subtask_progress: Option<u8>, // 0-100 percentage
75 + pub due_formatted: Option<String>, // "today", "tomorrow", "+3d", "2d ago"
76 + pub timer_active: bool,
77 + pub time_progress: Option<u8>, // actual vs estimate percentage
78 + }
79 + ```
80 +
81 + JavaScript renders these directly:
82 + ```javascript
83 + element.classList.add(`urgency-${task.urgencyClass}`);
84 + progressBar.style.width = `${task.subtaskProgress}%`;
85 + ```
86 +
87 + When adding new features, always ask: can this be computed once in Rust instead of repeatedly in JavaScript?
88 +
89 + ## Tauri Commands
90 +
91 + Commands are thin wrappers in `src-tauri/src/commands/`. They extract parameters, call repository methods, and map to response types:
92 +
93 + ```rust
94 + #[tauri::command]
95 + #[instrument(skip_all)]
96 + pub async fn list_projects(state: State<'_, Arc<AppState>>) -> Result<Vec<ProjectResponse>, ApiError> {
97 + Ok(state.projects.list_all(DESKTOP_USER_ID).await?
98 + .into_iter().map(ProjectResponse::from).collect())
99 + }
100 + ```
101 +
102 + **Rules:**
103 + - Every command gets `#[instrument(skip_all)]` for tracing.
104 + - Return type is always `Result<T, ApiError>`.
105 + - Commands call repository methods via `state.{repo}.{method}()`.
106 + - Domain types convert to response types via `From<T>` impls.
107 + - No SQL queries or business logic in commands.
108 +
109 + ## Repository Traits
110 +
111 + `crates/core/src/repository.rs` defines async traits for all data operations:
112 +
113 + ```rust
114 + #[async_trait]
115 + pub trait TaskRepository: Send + Sync {
116 + async fn list_all(&self, user_id: UserId) -> Result<Vec<Task>>;
117 + async fn list_filtered(&self, user_id: UserId, query: TaskFilterQuery) -> Result<(Vec<Task>, i64)>;
118 + async fn get_by_id(&self, id: TaskId, user_id: UserId) -> Result<Option<Task>>;
119 + async fn create(&self, user_id: UserId, task: NewTask) -> Result<Task>;
120 + async fn update(&self, id: TaskId, user_id: UserId, task: UpdateTask) -> Result<Option<Task>>;
121 + // ...
122 + }
123 + ```
124 +
125 + Implementations live in `crates/db-sqlite/src/repository/`. This indirection means commands work with any backend implementation.
126 +
127 + ## Error Handling
128 +
129 + ```rust
130 + #[derive(Debug, thiserror::Error)]
131 + pub enum CoreError {
132 + #[error("Database error: {message}")]
133 + Database { message: String, #[source] source: Option<Box<dyn std::error::Error + Send + Sync>> },
134 + #[error("Not found: {resource} with id {id}")]
135 + NotFound { resource: &'static str, id: String },
136 + #[error("Validation error: {field} - {message}")]
137 + Validation { field: &'static str, message: String },
138 + // ...
139 + }
140 + ```
141 +
142 + **Rules:**
143 + - Use typed error variants with context (resource name, field name).
144 + - Use helper constructors: `CoreError::database(err)`, `CoreError::not_found("task", id)`, `CoreError::validation("name", "too long")`.
145 + - Commands return `ApiError` which converts from `CoreError` via `From`.
146 + - Never `.unwrap()` in production code. Use `?` for propagation.
147 +
148 + ## JavaScript Architecture
149 +
150 + ### Namespace
151 +
152 + All JavaScript lives under the `GoingsOn` global namespace:
153 +
154 + ```javascript
155 + window.GoingsOn = {
156 + api: {}, // Tauri IPC wrappers
157 + state: null, // Centralized reactive state
158 + ui: {}, // Modal, toast, form utilities
159 + utils: {}, // escapeHtml, escapeAttr, formatDue, getErrorMessage
160 + projects: {}, // Project module
161 + tasks: {}, // Task module
162 + events: {}, // Events module
163 + emails: {}, // Email module
164 + dayPlan: {}, // Day planning
165 + snooze: {}, // Snooze management
166 + navigation: {}, // View switching
167 + };
168 + ```
169 +
170 + New code attaches to the appropriate namespace. Never use `window.X = ...` for new exports.
171 +
172 + ### Module Pattern (IIFE)
173 +
174 + Every JS file is a strict-mode IIFE:
175 +
176 + ```javascript
177 + (function() {
178 + 'use strict';
179 + const esc = GoingsOn.utils.escapeHtml;
180 +
181 + async function load() { /* ... */ }
182 + function render(data) { /* ... */ }
183 +
184 + GoingsOn.myModule = { load, render };
185 + })();
186 + ```
187 +
188 + ### State Management
189 +
190 + `GoingsOn.state` is a centralized store with pub/sub:
191 +
192 + ```javascript
193 + GoingsOn.state.set('tasks', newTasks); // Set + notify subscribers
194 + GoingsOn.state.tasks; // Read
195 + GoingsOn.state.subscribe('tasks', (newVal, oldVal) => { /* ... */ });
196 + ```
197 +
198 + All shared data goes through `GoingsOn.state`. No module-local caches for data that other modules need.
199 +
200 + ### Form Builder
201 +
202 + Use `openFormModal()` for all CRUD forms. Define fields as data, not HTML:
203 +
204 + ```javascript
205 + const fields = [
206 + { name: 'description', type: 'text', label: 'Task', required: true, value: task.description },
207 + { name: 'priority', type: 'select', label: 'Priority', options: PRIORITY_OPTIONS, value: task.priority },
208 + { name: 'due', type: 'datetime-local', label: 'Due Date', value: formatForDatetimeInput(task.due) },
209 + { name: 'tags', type: 'text', label: 'Tags', value: task.tags.join(', '), hint: 'Comma-separated' },
210 + ];
211 +
212 + openFormModal({
213 + title: 'Edit Task',
214 + fields,
215 + onSubmit: async (data) => {
216 + await GoingsOn.api.tasks.update(task.id, data);
217 + GoingsOn.ui.toast('Task updated!');
218 + await load();
219 + }
220 + });
221 + ```
222 +
223 + Supported field types: `text`, `textarea`, `select`, `datetime-local`, `checkbox`, `number`, `email`, `password`, `hidden`.
224 +
225 + ### XSS Prevention
226 +
227 + Always escape user content:
228 + ```javascript
229 + GoingsOn.utils.escapeHtml(str) // For text content
230 + GoingsOn.utils.escapeAttr(str) // For HTML attributes
231 + ```
232 +
233 + Never insert user-provided strings into innerHTML without escaping.
234 +
235 + ## CSS Workflow
236 +
237 + - **Edit:** `src-tauri/frontend/css/styles.css`
238 + - **Build:** Run `src-tauri/frontend/build-css.sh` to generate `styles.min.css`
239 + - **Never** edit `styles.min.css` directly — it's auto-generated via clean-css-cli
240 + - The HTML loads `styles.min.css`
241 +
242 + ## SyncKit Integration
243 +
244 + Cloud sync is optional. The `SyncKitClient` lives in `AppState.sync_client` behind `Arc<RwLock<Option<Arc<SyncKitClient>>>>`.
245 +
246 + **Setup flow:** User enters API key → validate against server → save to config → create client → setup encryption (new master key or import existing).
247 +
248 + **Sync flow:** The sync service tracks changes via a changelog table. Push sends encrypted change batches to the server. Pull downloads and applies remote changes. All operations go through the core repository traits.
249 +
250 + ## Testing
251 +
252 + - **Rust unit tests:** In-file `#[cfg(test)]` modules in each crate
253 + - **Rust integration tests:** `tests/` directories in each crate
254 + - **JS tests:** Manual testing via the app (no automated JS test runner yet)
255 + - Test databases use in-memory SQLite (`:memory:`) with migrations applied
256 + - Always verify the full pipeline: Rust command → repository → DB → response → JS render
257 +
258 + ## When Adding Features
259 +
260 + 1. **Start with the Rust types** — define the model in `crates/core/src/models/`
261 + 2. **Add repository trait method** in `crates/core/src/repository.rs`
262 + 3. **Implement in SQLite** in `crates/db-sqlite/src/repository/`
263 + 4. **Add Tauri command** in `src-tauri/src/commands/` (thin wrapper)
264 + 5. **Create response type** with pre-computed fields
265 + 6. **Build JS integration** — call command, render result using namespace pattern
266 +
267 + ## When Fixing Bugs
268 +
269 + 1. **Identify the layer** — is it core logic, repository, command, or UI?
270 + 2. **Fix at the right layer** — don't patch JS for a Rust bug
271 + 3. **Add pre-computation** if JS is doing repeated calculations that belong in Rust