commit b00fe689fae7b446ea82d21163660de9acbcb286 Author: Amolith Date: Sat Nov 8 00:07:20 2025 -0700 docs(agents): add diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000000000000000000000000000000000..15806d98435d6c175455f69583eab3c6b822e363 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,309 @@ +# AGENTS.md + +Guide for AI agents working in this codebase. + +## Project Overview + +Sift is a collaborative list application with real-time voting and ranking. Users create or join rooms via an ID, then collaboratively add items and vote (upvote, downvote, or veto). Items are automatically sorted by score, with vetoed items pushed to the bottom and dimmed. + +Tech stack: +- Runtime: Deno (TypeScript) +- Backend: WebSocket server with SQLite database +- Frontend: Vanilla JavaScript (no frameworks) +- Database: SQLite3 via https://deno.land/x/sqlite3@0.11.1/mod.ts +- Server: Deno std HTTP server https://deno.land/std@0.208.0/http/server.ts + +## Essential Commands + +Run the server +```bash +deno run --allow-net=:8294 --allow-read=./static/,./lists.db,$HOME/.cache/deno/plug --allow-write=./lists.db,$HOME/.cache/deno/plug --allow-env --allow-ffi server.ts +``` + +Required permissions: +- --allow-net=:8294: HTTP server and WebSocket connections (restricted to port 8294) +- --allow-read: Serve static files from ./static/, read database, cache SQLite library +- --allow-write: SQLite database operations and cache directory for native library +- --allow-env: SQLite library reads environment variables (unavoidable for sqlite3 FFI) +- --allow-ffi: SQLite3 native library access (unavoidable for sqlite3) + +Server listens on http://localhost:8294 + +Database inspection +```bash +sqlite3 lists.db ".schema" +sqlite3 lists.db "SELECT * FROM rooms;" +sqlite3 lists.db "SELECT * FROM items;" +sqlite3 lists.db "SELECT * FROM votes;" +``` + +## Architecture + +### Data Flow + +``` +User creates/joins room → WebSocket connection → Server sends current state +User adds items → Client sends 'add_items' → Server inserts to DB → Broadcasts to all clients in room +User votes → Client sends 'vote' → Server upserts vote → Broadcasts vote change +User deletes item → Client sends 'delete_item' → Server cascades deletion (and explicitly deletes votes) → Broadcasts deletion +``` + +### Component Structure + +- server.ts + - HTTP request handler serving static files and API endpoints + - WebSocket connection manager with room-based broadcasting + - SQLite database with schema initialization, migration, and queries + - Room ID generation via UUID +- static/app.js + - WebSocket client with message queuing before connection ready + - State management for items and votes + - Server-authoritative state (no optimistic updates) + - Keyboard shortcuts (j/k nav, 1/2/3 votes, Enter upvote, e edit, Del delete, Ctrl/Cmd+Z undo) + - Inline edit flow using 'edit_item' and FLIP-style reordering animation + - Reset votes action (broadcasts 'votes_reset') +- static/index.html + - Two-screen UI: start screen (create/join) and list screen + - Connection status indicators and header actions (copy link, copy items, reset votes, leave) + - Help modal markup and keyboard shortcut reference + - Input methods: single-item input and bulk textarea +- static/style.css + - CSS custom properties for theming + - Light/dark mode via prefers-color-scheme + - Responsive design + - Styles for connection status, help modal, selected item, veto state, and animations + +### Database Schema (schema_version = 2) + +rooms +- code (TEXT PRIMARY KEY NOT NULL) +- created_at (INTEGER, Unix timestamp) + +items +- id (TEXT PRIMARY KEY NOT NULL) +- room_code (TEXT NOT NULL) → FOREIGN KEY rooms(code) ON DELETE CASCADE +- text (TEXT NOT NULL) CHECK(LENGTH(text) BETWEEN 1 AND 200) +- created_at (INTEGER, Unix timestamp) +- Index: idx_items_room_code on room_code + +votes +- item_id (TEXT NOT NULL) → FOREIGN KEY items(id) ON DELETE CASCADE +- user_id (TEXT NOT NULL) +- vote_type (TEXT NOT NULL) CHECK(vote_type IN ('up','down','veto')) +- created_at (INTEGER, Unix timestamp) +- PRIMARY KEY (item_id, user_id) +- Indexes: idx_votes_item_id on item_id, idx_votes_user_id on user_id + +Constraints +- PRAGMA foreign_keys = ON +- CASCADE deletion: deleting a room deletes all items and votes; deleting an item deletes all votes (code also deletes votes explicitly before deleting an item) +- NOT NULL validation at DB level +- Text length enforced 1–200 characters +- Vote type enforced: 'up' | 'down' | 'veto' + +Migration +- On startup, migrates legacy data to schema v2 when needed +- From v0: creates new tables, sanitizes/truncates item text to 200 chars, filters vote types; sets user_version = 2 +- From v1: rebuilds rooms table (removes last_activity and length constraint); sets user_version = 2 +- Transactional (BEGIN/COMMIT with rollback on failure) + +### WebSocket Protocol + +Server → Client messages +```js +{ type: 'state', items: [...], userId: '...' } +{ type: 'items_added', items: [{id, text, votes}, ...] } +{ type: 'vote_changed', itemId, userId, voteType } +{ type: 'vote_removed', itemId, userId } +{ type: 'item_edited', itemId, text } +{ type: 'item_deleted', itemId } +{ type: 'votes_reset' } +``` + +Client → Server messages +```js +{ type: 'add_items', items: ['text1', 'text2', ...] } +{ type: 'vote', itemId, voteType } // voteType: 'up'|'down'|'veto' +{ type: 'unvote', itemId } +{ type: 'edit_item', itemId, text } +{ type: 'delete_item', itemId } +{ type: 'reset_votes' } +``` + +### State Synchronization + +Connection lifecycle +1. Client connects with ?room=CODE&user=UUID query params +2. Server verifies room exists, upgrades to WebSocket +3. Server sends full state via 'state' message +4. Client stores userId from server (may differ from localStorage if not provided) +5. Subsequent changes broadcast to all clients in room (sender included) + +Client-side optimism: None — server is authoritative. Client waits for broadcast confirmation. + +Room cleanup: When a socket closes, it's removed from the in-memory clients Map. No periodic expiration/cleanup is implemented. + +### Rendering Logic + +Client-side sort +1. Vetoed items always appear last +2. Within non-vetoed and vetoed groups, sort by score (descending) +3. Score = count(upvotes) - count(downvotes), vetoes don't affect score + +Visual states +- Vetoed items: 40% opacity, max-height constraint, hidden overflow +- Active vote buttons: darker background +- Items display current score with +/- prefix +- Reordering uses FLIP animation + +### Server-Side Validation + +Limits (server.ts) +- MAX_ITEM_TEXT_LEN: 200 characters +- MAX_BULK_ITEMS: 100 items per add_items request +- MAX_WS_MESSAGE_BYTES: 32768 bytes (32KB) + +Message validation +- Messages exceeding MAX_WS_MESSAGE_BYTES close the WebSocket with code 1009 +- Invalid JSON is logged and ignored +- add_items: items must be array length 1–100; each text sanitized (trim, normalize CR/LF); texts outside 1–200 chars are rejected +- vote/unvote/delete_item/edit_item: itemId must be string and belong to current room +- vote: voteType must be one of 'up'|'down'|'veto' +- edit_item: text sanitized and length-checked (1–200) +- reset_votes: clears all votes for items in the room + +Validation helpers +- safeParseMessage(): size check + JSON parse with error handling +- sanitizeItemText(): trim, normalize line endings; returns null if length invalid +- isValidVoteType(): type guard for vote types +- itemBelongsToRoom(): prevents cross-room item manipulation + +Invalid messages +- Logged with descriptive warnings +- Silently ignored (no error response) +- Never throw exceptions that crash the server + +## Code Patterns + +Naming conventions +- Variables: camelCase (roomCode, userId, messageQueue) +- Constant limits: UPPER_SNAKE_CASE (MAX_ITEM_TEXT_LEN, MAX_BULK_ITEMS, MAX_WS_MESSAGE_BYTES) +- Other constants/instances: camelCase (db, clients) +- Database columns: snake_case (room_code, created_at, vote_type) +- CSS custom properties: kebab-case with -- prefix (--bg-page, --accent-hover) + +TypeScript usage +- Minimal typing, relying on inference +- Explicit types for function parameters when needed +- Type assertions for DB results (e.g., as Array<{...}>) +- Type guard for vote types + +Error handling +- Server WebSocket messages: try/catch around message handling +- Server: no error responses for malformed WebSocket messages +- Client WebSocket: error handler logs to console +- Client close: alert on unexpected close, reset to start screen +- Clipboard actions: try/catch with console log +- Delete confirmation: browser confirm() dialog + +Security notes +- No authentication: rooms protected only by knowing the room ID +- User IDs: client-generated UUIDs in localStorage (spoofable) +- XSS prevention: escapeHtml() used when rendering item text +- Input validation: server-side sanitization and length limits +- Cross-room protection: server validates item-room membership +- Message size limit: WebSocket messages limited to 32KB +- Foreign keys: enabled with CASCADE deletion + +## Gotchas + +1) Vote uniqueness: DB enforces one vote per user per item via compound PK and UPSERT. Clicking the same vote button twice removes the vote. +2) Message queuing: Client queues messages sent before WebSocket opens and flushes them on connection. +3) Undo is limited: Only stores last vote action; doesn't cover add/delete operations. +4) Broadcast 'except' param unused by callers: all clients, including sender, receive broadcasts. +5) Item-room validation enforced: prevents cross-room vote/edit/delete. +6) Port is hardcoded: server always runs on port 8294. +7) Static files served via dedicated routes; no generic file server. +8) No build step: Deno runs TypeScript directly. +9) Database file location: lists.db is created in the project root. + +## Testing Approach + +No automated tests. Manual checks: + +Core functionality +1. Multi-client testing: open multiple tabs to the same room +2. Vote synchronization: vote in one client; others update +3. Bulk add: paste multi-line text; verify all items added +4. Edit item: edit an item; verify all clients update (item_edited) +5. Reset votes: use the reset button; verify votes clear (votes_reset) +6. Veto sorting: veto an item; verify it moves to bottom and dims +7. Disconnection: stop server; clients show alert and reset +8. Room joining: joining a non-existent room returns 404 + +Validation testing (check server logs) +1. Empty item: whitespace-only text → ignored, logged +2. Long item: >200 characters → rejected, logged (not truncated at runtime) +3. Bulk limit: >100 items in one request → rejected, logged +4. Invalid vote type: crafted invalid voteType → ignored, logged +5. Cross-room manipulation: vote/edit/delete for an item in another room → rejected, logged +6. Large message: >32KB WebSocket message → socket closed with code 1009 +7. DB constraints: sqlite3 lists.db ".schema" → verify CHECKs, NOT NULL, indexes +8. Migration: start with old-format data; verify migration to v2 runs and data preserved + +## Development Workflow + +Making changes +1. Edit source files (server.ts or static/*) +2. Restart Deno server (no hot reload) +3. Hard refresh browser (Ctrl+Shift+R) to bypass cache +4. Test in multiple tabs for WebSocket sync + +Adding new vote types +1. Update DB schema CHECK constraint for vote_type in CREATE TABLE votes (and migrations) +2. Update isValidVoteType in server.ts +3. Update vote aggregation/rendering logic on client if needed +4. Add button in static/index.html +5. Add handling in static/app.js vote() and render() +6. Add CSS styling for the new vote type + +Adding new message types +1. Add a case to the switch in handleWebSocket() in server.ts +2. Implement DB operations and broadcast() as needed +3. Add a case to handleMessage() in static/app.js +4. Update client state and trigger render() + +## File Organization + +``` +/ +├── server.ts # Deno server: HTTP, WebSocket, SQLite +├── lists.db # SQLite database (created on first run) +└── static/ + ├── index.html # HTML structure + ├── app.js # Client-side JavaScript + └── style.css # Styles with light/dark theme +``` + +No nested directories, no build artifacts, no dependencies beyond Deno stdlib and sqlite3. + +## Important Constants + +- Room ID generation: crypto.randomUUID() +- Server port: 8294 +- WebSocket path: /ws +- API endpoints: /api/create (POST, returns room code) +- Default userId: generated via crypto.randomUUID() and stored in localStorage + +Validation limits +- MAX_ITEM_TEXT_LEN: 200 characters +- MAX_BULK_ITEMS: 100 +- MAX_WS_MESSAGE_BYTES: 32768 (32KB) + +## Dependencies + +All dependencies are imported via HTTPS URLs from deno.land: +- https://deno.land/std@0.208.0/http/server.ts — HTTP server +- https://deno.land/x/sqlite3@0.11.1/mod.ts — SQLite database + +No package.json, no npm, no node_modules.