diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 50724061..f00275ed 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -21,15 +21,13 @@ Before making changes, read these documents in order: The project is split into multiple packages under the `packages/` directory: -- `/landing`: Frontend application built with React, TanStack Start, TanStack Router (deployed on Cloudflare Workers) +- `/web`: Frontend application built with React, TanStack Start, TanStack Router (deployed on Cloudflare Workers) - `/workers`: Backend services, API endpoints, and database migrations -- `/web`: Legacy SolidJS frontend (being removed -- all features migrated to landing) - `/shared`: Shared TypeScript utilities and error definitions - `/mcp`: MCP server for development tools and documentation -- `/mcp-memory`: Persistent agent memory system - `/docs`: Vitepress docs site containing internal documentation -The landing package is the main frontend. It is deployed as a single Cloudflare Worker. +The web package is the main frontend. It is deployed as a single Cloudflare Worker. Do not worry about migrations (client side or backend) unless specifically instructed. This project is not in production and has no users. @@ -39,12 +37,12 @@ Do not worry about migrations (client side or backend) unless specifically instr # Development pnpm dev:front # Frontend (port 3010) pnpm dev:workers # Backend workers (port 8787) -pnpm --filter landing build # Build frontend +pnpm --filter web build # Build frontend # Testing -pnpm --filter landing test # Frontend unit tests +pnpm --filter web test # Frontend unit tests pnpm --filter workers test # Backend tests only -pnpm --filter landing test:browser # Browser integration tests +pnpm --filter web test:browser # Browser integration tests # Code Quality pnpm lint # ESLint check @@ -132,14 +130,14 @@ retries += 1; ### React Patterns - **Import stores directly** - Use Zustand stores from `@/stores/` instead of prop-drilling shared state -- Shared state lives in Zustand stores under `packages/landing/src/stores/` +- Shared state lives in Zustand stores under `packages/web/src/stores/` - Use `useMemo` for derived values, `useCallback` for stable callbacks - Use `useEffect` with explicit dependency arrays (never omit deps) - Use `useLayoutEffect` for DOM measurements before paint - Use `useSyncExternalStore` for external store subscriptions (e.g., Yjs awareness) - Use `useId()` for unique IDs on form elements (radio buttons, checkboxes) - Move business logic to stores, hooks, or utilities (not components) -- Path aliases: `@/` maps to `packages/landing/src/` +- Path aliases: `@/` maps to `packages/web/src/` ## Documentation @@ -183,7 +181,7 @@ For specific complex areas, see: - Cloudflare Pages is NOT used; only Cloudflare Workers - Packages are under `packages/` directory with their own dependencies -- Path aliases: `@/` maps to `packages/landing/src/` (defined in tsconfig.json) +- Path aliases: `@/` maps to `packages/web/src/` (defined in tsconfig.json) - Adjust documentation if your changes would affect any existing documentation ## Anti-Patterns (Never Do These) @@ -220,94 +218,3 @@ Core workflow: 2. `agent-browser snapshot -i` - Get interactive elements with refs (@e1, @e2) 3. `agent-browser click @e1` / `fill @e2 "text"` - Interact using refs 4. Re-snapshot after page changes - -## Agent Memory System - -This repository has a persistent memory system (`@corates/mcp-memory`) that stores durable knowledge across sessions. Memory is stored in `.mcp/memory.db` and shared via git. - -### When to Search Memory - -**Always search memory before:** - -- Starting complex or multi-step tasks -- Making architectural decisions -- Working in unfamiliar areas of the codebase -- Implementing patterns that might already exist - -**Example searches:** - -- "authentication patterns" before working on auth -- "error handling" before adding try/catch blocks -- "database migrations" before schema changes -- "React patterns" before creating components - -### When to Write Memory - -**Propose memory writes when you discover:** - -- A non-obvious fact about the codebase -- The rationale behind an architectural decision -- A multi-step procedure that will be repeated -- A pattern that should be followed consistently - -**Do NOT write:** - -- Task-specific context (what you're currently working on) -- Temporary workarounds or debugging notes -- Information already in documentation -- Opinions without decisions - -### When to Update Memory - -**Use `propose_memory_update` when:** - -- Existing knowledge is outdated or incorrect -- You have additional context to add -- A decision has changed with new rationale -- A procedure needs correction - -**Update Actions:** - -- `refine` - Update in-place, keeps same ID, increments version -- `supersede` - Create new entry, mark old as replaced (use for major changes) - -### Knowledge Types - -| Type | When to Use | -| ----------- | ------------------------------------ | -| `fact` | Objective, verifiable information | -| `decision` | Choice with rationale (why X over Y) | -| `procedure` | Step-by-step instructions | -| `pattern` | Repeated structure to follow | - -### Memory Tool Usage - -``` -# Before starting work -search_memory({ query: "relevant topic" }) - -# After discovering durable knowledge -propose_memory_write({ - type: "decision", - title: "Concise title", - content: "Detailed explanation with rationale", - tags: ["relevant", "tags"] -}) - -# To update existing knowledge -propose_memory_update({ - target_id: "uuid-from-search", - action: "refine", - content: "Updated content with corrections", - justification: "Why this update is needed" -}) - -# To replace outdated knowledge -propose_memory_update({ - target_id: "uuid-from-search", - action: "supersede", - title: "New title for replacement", - content: "Completely revised content", - justification: "Original was incorrect because..." -}) -``` diff --git a/.github/Contributing.md b/.github/Contributing.md index 3360facd..9a6a7ce6 100644 --- a/.github/Contributing.md +++ b/.github/Contributing.md @@ -17,10 +17,9 @@ This is a pnpm monorepo with the following packages: | Package | Description | | ------------------ | -------------------------------------- | -| `packages/web` | Main SolidJS frontend application | +| `packages/web` | React/TanStack Start frontend | | `packages/workers` | Cloudflare Workers backend (Hono) | -| `packages/landing` | Landing/marketing site (SolidJS Start) | -| `packages/ui` | Shared UI component library (Zag.js) | +| `packages/ui` | Shared UI component library | | `packages/shared` | Shared error definitions and utilities | | `packages/mcp` | MCP server for AI agent integration | @@ -45,7 +44,6 @@ This is a pnpm monorepo with the following packages: ```sh cp packages/workers/.env.example packages/workers/.env cp packages/web/.env.example packages/web/.env - cp packages/landing/.env.example packages/landing/.env ``` 4. **Start development servers:** @@ -132,7 +130,7 @@ pnpm test:ui # Run tests with browser UI (broken sort of) Tests use Vitest. Place test files alongside source files in `__tests__/` using the pattern `*.test.{js,jsx,ts,tsx}`. -See [packages/web/TESTING.md](packages/web/TESTING.md) for detailed testing guidelines. +See `packages/docs/guides/testing.md` for detailed testing guidelines. ### Submitting Changes @@ -175,7 +173,7 @@ pnpm run initialize-mcp | Command | Description | | ------------------------------------------------- | --------------------------------- | -| `pnpm dev:front` | Start frontend (landing + web) | +| `pnpm dev` | Start frontend dev server | | `pnpm dev:workers` | Start backend workers | | `pnpm build` | Build all packages | | `pnpm test` | Run all tests | diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index f570c930..33a5cf3e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -19,15 +19,13 @@ Before making changes, read these documents in order: The project is split into multiple packages under the `packages/` directory: -- `/landing`: Frontend application built with React, TanStack Start, TanStack Router (deployed on Cloudflare Workers) +- `/web`: Frontend application built with React, TanStack Start, TanStack Router (deployed on Cloudflare Workers) - `/workers`: Backend services, API endpoints, and database migrations -- `/web`: Legacy SolidJS frontend (being removed -- all features migrated to landing) - `/shared`: Shared TypeScript utilities and error definitions - `/mcp`: MCP server for development tools and documentation -- `/mcp-memory`: Persistent agent memory system - `/docs`: Vitepress docs site containing internal documentation -The landing package is the main frontend. It is deployed as a single Cloudflare Worker. +The web package is the main frontend. It is deployed as a single Cloudflare Worker. Do not worry about migrations (client side or backend) unless specifically instructed. This project is not in production and has no users. @@ -37,12 +35,12 @@ Do not worry about migrations (client side or backend) unless specifically instr # Development pnpm dev:front # Frontend (port 3010) pnpm dev:workers # Backend workers (port 8787) -pnpm --filter landing build # Build frontend +pnpm --filter web build # Build frontend # Testing -pnpm --filter landing test # Frontend unit tests +pnpm --filter web test # Frontend unit tests pnpm --filter workers test # Backend tests only -pnpm --filter landing test:browser # Browser integration tests +pnpm --filter web test:browser # Browser integration tests # Code Quality pnpm lint # ESLint check @@ -130,14 +128,14 @@ retries += 1; ### React Patterns - **Import stores directly** - Use Zustand stores from `@/stores/` instead of prop-drilling shared state -- Shared state lives in Zustand stores under `packages/landing/src/stores/` +- Shared state lives in Zustand stores under `packages/web/src/stores/` - Use `useMemo` for derived values, `useCallback` for stable callbacks - Use `useEffect` with explicit dependency arrays (never omit deps) - Use `useLayoutEffect` for DOM measurements before paint - Use `useSyncExternalStore` for external store subscriptions (e.g., Yjs awareness) - Use `useId()` for unique IDs on form elements (radio buttons, checkboxes) - Move business logic to stores, hooks, or utilities (not components) -- Path aliases: `@/` maps to `packages/landing/src/` +- Path aliases: `@/` maps to `packages/web/src/` ## Documentation @@ -179,7 +177,7 @@ For specific complex areas, see: - Cloudflare Pages is NOT used; only Cloudflare Workers - Packages are under `packages/` directory with their own dependencies -- Path aliases: `@/` maps to `packages/landing/src/` (defined in tsconfig.json) +- Path aliases: `@/` maps to `packages/web/src/` (defined in tsconfig.json) - Adjust documentation if your changes would affect any existing documentation ## Anti-Patterns (Never Do These) @@ -216,94 +214,3 @@ Core workflow: 2. `agent-browser snapshot -i` - Get interactive elements with refs (@e1, @e2) 3. `agent-browser click @e1` / `fill @e2 "text"` - Interact using refs 4. Re-snapshot after page changes - -## Agent Memory System - -This repository has a persistent memory system (`@corates/mcp-memory`) that stores durable knowledge across sessions. Memory is stored in `.mcp/memory.db` and shared via git. - -### When to Search Memory - -**Always search memory before:** - -- Starting complex or multi-step tasks -- Making architectural decisions -- Working in unfamiliar areas of the codebase -- Implementing patterns that might already exist - -**Example searches:** - -- "authentication patterns" before working on auth -- "error handling" before adding try/catch blocks -- "database migrations" before schema changes -- "React patterns" before creating components - -### When to Write Memory - -**Propose memory writes when you discover:** - -- A non-obvious fact about the codebase -- The rationale behind an architectural decision -- A multi-step procedure that will be repeated -- A pattern that should be followed consistently - -**Do NOT write:** - -- Task-specific context (what you're currently working on) -- Temporary workarounds or debugging notes -- Information already in documentation -- Opinions without decisions - -### When to Update Memory - -**Use `propose_memory_update` when:** - -- Existing knowledge is outdated or incorrect -- You have additional context to add -- A decision has changed with new rationale -- A procedure needs correction - -**Update Actions:** - -- `refine` - Update in-place, keeps same ID, increments version -- `supersede` - Create new entry, mark old as replaced (use for major changes) - -### Knowledge Types - -| Type | When to Use | -| ----------- | ------------------------------------ | -| `fact` | Objective, verifiable information | -| `decision` | Choice with rationale (why X over Y) | -| `procedure` | Step-by-step instructions | -| `pattern` | Repeated structure to follow | - -### Memory Tool Usage - -``` -# Before starting work -search_memory({ query: "relevant topic" }) - -# After discovering durable knowledge -propose_memory_write({ - type: "decision", - title: "Concise title", - content: "Detailed explanation with rationale", - tags: ["relevant", "tags"] -}) - -# To update existing knowledge -propose_memory_update({ - target_id: "uuid-from-search", - action: "refine", - content: "Updated content with corrections", - justification: "Why this update is needed" -}) - -# To replace outdated knowledge -propose_memory_update({ - target_id: "uuid-from-search", - action: "supersede", - title: "New title for replacement", - content: "Completely revised content", - justification: "Original was incorrect because..." -}) -``` diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 352f17fd..3aceed46 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -23,9 +23,9 @@ updates: - '*' dependency-type: 'development' - # Landing package + # Web package - package-ecosystem: 'npm' - directory: '/packages/landing' + directory: '/packages/web' schedule: interval: 'weekly' day: 'monday' @@ -33,7 +33,7 @@ updates: open-pull-requests-limit: 3 labels: - 'dependencies' - - 'landing' + - 'web' versioning-strategy: increase commit-message: prefix: 'chore' @@ -41,11 +41,11 @@ updates: include: 'scope' rebase-strategy: 'auto' groups: - landing-dependencies: + web-dependencies: patterns: - '*' dependency-type: 'production' - landing-dev-dependencies: + web-dev-dependencies: patterns: - '*' dependency-type: 'development' diff --git a/.github/instructions/memory.instructions.md b/.github/instructions/memory.instructions.md deleted file mode 100644 index 061a0f7d..00000000 --- a/.github/instructions/memory.instructions.md +++ /dev/null @@ -1,134 +0,0 @@ ---- -applyTo: '**' -description: 'Agent memory system for persistent knowledge across sessions' ---- - -# Agent Memory System - -This repository has a persistent memory system (`@corates/mcp-memory`) that stores durable knowledge across sessions. Memory is stored in `.mcp/memory.db` and shared via git. - -## When to Search Memory - -**Always search memory before:** - -- Starting complex or multi-step tasks -- Making architectural decisions -- Working in unfamiliar areas of the codebase -- Implementing patterns that might already exist - -**Example searches:** - -``` -search_memory({ query: "authentication patterns" }) -search_memory({ query: "error handling", types: ["pattern", "decision"] }) -search_memory({ query: "database migrations" }) -search_memory({ query: "SolidJS props" }) -``` - -## When to Write Memory - -**Propose memory writes when you discover:** - -- A non-obvious fact about the codebase -- The rationale behind an architectural decision -- A multi-step procedure that will be repeated -- A pattern that should be followed consistently - -**Do NOT write:** - -- Task-specific context (what you're currently working on) -- Temporary workarounds or debugging notes -- Information already in documentation -- Opinions without decisions - -## When to Update Memory - -**Use `propose_memory_update` when:** - -- Existing knowledge is outdated or incorrect -- You have additional context to add -- A decision has changed with new rationale -- A procedure needs correction - -**Update Actions:** - -- `refine` - Update in-place, keeps same ID, increments version -- `supersede` - Create new entry, mark old as replaced (use for major changes) - -## Knowledge Types - -| Type | When to Use | -| ----------- | ------------------------------------ | -| `fact` | Objective, verifiable information | -| `decision` | Choice with rationale (why X over Y) | -| `procedure` | Step-by-step instructions | -| `pattern` | Repeated structure to follow | - -## Memory Tool Reference - -### search_memory - -Search for relevant knowledge before starting a task. - -```json -{ - "query": "authentication patterns", - "types": ["pattern", "decision"], - "tags": ["auth"], - "limit": 10, - "min_confidence": 0.3 -} -``` - -### propose_memory_write - -Submit new durable knowledge. - -```json -{ - "type": "decision", - "title": "Use Better-Auth over Lucia", - "content": "Better-Auth was chosen because it has native Cloudflare Workers support...", - "tags": ["auth", "architecture"], - "source": { - "type": "discussion", - "reference": "PR #142" - } -} -``` - -### propose_memory_update - -Update or replace existing knowledge. - -For refinements (minor updates): - -```json -{ - "target_id": "uuid-from-search-results", - "action": "refine", - "content": "Updated content with new information", - "justification": "Added details about v2 API changes" -} -``` - -For supersession (major changes): - -```json -{ - "target_id": "uuid-from-search-results", - "action": "supersede", - "title": "New Title (v2)", - "content": "Completely rewritten guidance", - "justification": "Original approach deprecated in v2" -} -``` - -## Best Practices - -1. **Search before writing** - Check if similar knowledge exists -2. **Be specific** - Titles should be clear and searchable -3. **Include rationale** - Explain why, not just what -4. **Use tags consistently** - Common: `auth`, `database`, `frontend`, `backend`, `architecture`, `patterns` -5. **Update, don't duplicate** - Use `propose_memory_update` for changes -6. **Cite sources** - Include file paths, PR numbers, or discussion references diff --git a/.gitignore b/.gitignore index 5a1459ac..d14b9d92 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,7 @@ reference/ .vinxi/ .output/ .astro/ -packages/landing/app.config.timestamp_*.js +packages/web/app.config.timestamp_*.js # testing /coverage diff --git a/.mcp.json b/.mcp.json index 447aa463..f5238314 100644 --- a/.mcp.json +++ b/.mcp.json @@ -4,14 +4,6 @@ "type": "stdio", "command": "node", "args": ["packages/mcp/dist/server.js"] - }, - "corates-memory": { - "type": "stdio", - "command": "node", - "args": ["packages/mcp-memory/dist/server.js"], - "env": { - "MCP_MEMORY_REPO_ROOT": "." - } } } } diff --git a/.mcp.md b/.mcp.md index 10922f2d..717fbceb 100644 --- a/.mcp.md +++ b/.mcp.md @@ -5,14 +5,6 @@ "command": "node", "args": ["packages/mcp/dist/server.js"] }, - "corates-memory": { - "type": "stdio", - "command": "node", - "args": ["packages/mcp-memory/dist/server.js"], - "env": { - "MCP_MEMORY_REPO_ROOT": "." - } - }, "ark-ui": { "type": "stdio", "command": "npx", diff --git a/.mcp/README.md b/.mcp/README.md deleted file mode 100644 index 33b6f6fa..00000000 --- a/.mcp/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# MCP Memory Storage - -This directory contains the SQLite database for the MCP memory system. - -The `memory.db` file stores persistent knowledge for this repository including: - -- Facts about the codebase -- Architectural decisions with rationale -- Operational procedures -- Code patterns and practices - -This file is committed to git so knowledge is shared across all contributors. diff --git a/.mcp/memory.db b/.mcp/memory.db deleted file mode 100644 index 3e232c10..00000000 Binary files a/.mcp/memory.db and /dev/null differ diff --git a/.mcp/memory.db-shm b/.mcp/memory.db-shm deleted file mode 100644 index fe9ac284..00000000 Binary files a/.mcp/memory.db-shm and /dev/null differ diff --git a/.mcp/memory.db-wal b/.mcp/memory.db-wal deleted file mode 100644 index e69de29b..00000000 diff --git a/.vscode/mcp.json b/.vscode/mcp.json index 1ee1de07..dbe6754a 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -5,14 +5,6 @@ "command": "node", "args": ["${workspaceFolder}/packages/mcp/dist/server.js"] }, - "corates-memory": { - "type": "stdio", - "command": "node", - "args": ["${workspaceFolder}/packages/mcp-memory/dist/server.js"], - "env": { - "MCP_MEMORY_REPO_ROOT": "${workspaceFolder}" - } - }, "ark-ui": { "command": "npx", "args": ["-y", "@ark-ui/mcp"] diff --git a/.vscode/settings.json b/.vscode/settings.json index 4db86f0b..1f1e9785 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -36,7 +36,7 @@ // Tailwind v4 is CSS-first; point the IntelliSense extension at the CSS entrypoint. // This enables hover/autocomplete for class strings across the whole monorepo, // including workspace packages like `packages/ui`. - "tailwindCSS.experimental.configFile": "packages/landing/src/styles.css", + "tailwindCSS.experimental.configFile": "packages/web/src/styles.css", "files.associations": { "*.css": "tailwindcss" } diff --git a/README.md b/README.md index 54bcbd10..4c42ac91 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,12 @@ CoRATES is a web application designed to streamline the entire quality and risk- ### Frontend -- **Framework**: SolidJS with @solidjs/router for client-side routing and SolidStart for landing page -- **Build**: Vite with vite-plugin-solid (web package) / SolidStart with Vinxi (landing package) +- **Framework**: React 19 with TanStack Start and TanStack Router +- **Build**: Vite with TanStack Start - **Styling**: Tailwind CSS v4 with @tailwindcss/vite -- **UI Components**: Ark UI (via @corates/ui package) + solid-icons -- **Data Fetching**: TanStack SolidQuery (reactive server state management) +- **UI Components**: shadcn/ui (Radix-based) + lucide-react +- **Data Fetching**: TanStack Query (server state management) +- **Client State**: Zustand - **Tables**: TanStack Solid Table - **Charts**: D3 for most user charts and Chart.js + solid-chartjs for admin - **PDF Viewer**: EmbedPDF with plugin ecosystem @@ -66,10 +67,9 @@ CoRATES is a web application designed to streamline the entire quality and risk- ### Monorepo Structure -- `packages/web` - SolidJS frontend application with Vite -- `packages/landing` - Marketing site built with SolidStart (meta-framework) and Vinxi, includes bundled web app +- `packages/web` - React/TanStack Start frontend (deployed on Cloudflare Workers) - `packages/workers` - Cloudflare Workers backend -- `packages/ui` - Shared Ark UI component library with prestyled + primitive variants +- `packages/ui` - Shared UI component library - `packages/shared` - Shared TypeScript utilities, types, and error definitions - `packages/docs` - Vitepress documentation and guides - `packages/mcp` - MCP server for development tools and documentation diff --git a/eslint.config.js b/eslint.config.js index 59f05fba..ce088714 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -236,8 +236,8 @@ export default [ }, }, { - // Landing package - React with hooks linting - files: ['packages/landing/**/*.{js,jsx,ts,tsx}'], + // Web package - React with hooks linting + files: ['packages/web/**/*.{js,jsx,ts,tsx}'], plugins: { 'react-hooks': reactHooks, }, diff --git a/package.json b/package.json index 0a8e9376..92f48ca1 100644 --- a/package.json +++ b/package.json @@ -14,20 +14,20 @@ "type": "module", "scripts": { "kill-ports": "for port in 5173 8787 3010 8080; do lsof -ti:$port | xargs kill -9 2>/dev/null; done || true", - "dev": "pnpm --filter landing dev", + "dev": "pnpm --filter web dev", "dev:workers": "pnpm --filter workers dev", "build": "pnpm --recursive run build", - "build:landing": "pnpm --filter landing run build", - "deploy": "pnpm run deploy:workers && pnpm run deploy:landing", + "build:web": "pnpm --filter web run build", + "deploy": "pnpm run deploy:workers && pnpm run deploy:web", "deploy:workers": "pnpm --filter workers run deploy", "deploy:reset:workers": "pnpm --filter workers run db:reset:prod", - "deploy:landing": "pnpm --filter landing run deploy", + "deploy:web": "pnpm --filter web run deploy", "clear-workers": "pnpm --filter workers run clear-workers", "format": "prettier --write .", "lint": "eslint .", "lint:fix": "eslint . --fix", "initialize-mcp": "pnpm --filter mcp run scrape:all", - "test:ui": "pnpm --filter landing test", + "test:ui": "pnpm --filter web test", "test": "pnpm --recursive run test", "typecheck": "pnpm --recursive run typecheck", "logs": "pnpm --filter workers run logs", diff --git a/packages/docs/.vitepress/config.js b/packages/docs/.vitepress/config.js index dad3c51e..6bcd77d2 100644 --- a/packages/docs/.vitepress/config.js +++ b/packages/docs/.vitepress/config.js @@ -13,7 +13,7 @@ export default withMermaid({ // Glossary links to source files outside docs folder /\.\.\//, // Links with line numbers to source files - /packages\/(web|workers|shared|ui|landing|mcp)\//, + /packages\/(web|workers|shared|ui|mcp)\//, // Cursor rules folder /\.cursor\//, ], diff --git a/packages/docs/STATUS.md b/packages/docs/STATUS.md index cbe4a7ae..e3d320ec 100644 --- a/packages/docs/STATUS.md +++ b/packages/docs/STATUS.md @@ -8,7 +8,7 @@ This document tracks the current implementation status of CoRATES features and s ## Current Focus -**React migration complete** - The frontend was fully migrated from SolidJS to React/TanStack Start in March 2026. The `packages/landing` package is now the sole frontend. `packages/web` (SolidJS) is deprecated and pending removal. The application is feature-complete for core workflows but not yet deployed to production. No active users. +**React migration complete** - The frontend was fully migrated from SolidJS to React/TanStack Start in March 2026. The `packages/web` package is the sole frontend. The application is feature-complete for core workflows but not yet deployed to production. No active users. --- @@ -32,7 +32,7 @@ This document tracks the current implementation status of CoRATES features and s ## System Components -### Frontend (packages/landing - React/TanStack Start) +### Frontend (packages/web - React/TanStack Start) | Component | Status | Notes | | ----------------- | -------- | ----------------------------------------------------------------- | @@ -166,7 +166,7 @@ Future planned: ROBINS-E, GRADE | Billing routes | Partial | Tests in billing/**tests**/ | | Admin routes | Partial | Tests in admin/**tests**/ | -### Frontend (packages/landing) +### Frontend (packages/web) | Area | Coverage | Notes | | ------------- | -------- | -------------------------------------------------- | diff --git a/packages/docs/architecture/diagrams/01-package-architecture.md b/packages/docs/architecture/diagrams/01-package-architecture.md index 3a62b0a3..e61f7ec4 100644 --- a/packages/docs/architecture/diagrams/01-package-architecture.md +++ b/packages/docs/architecture/diagrams/01-package-architecture.md @@ -5,10 +5,8 @@ Overview of the monorepo structure and how packages relate to each other. ```mermaid graph TB subgraph "Monorepo Packages" - landing["landing
(Marketing Site)"] - web["web
(SolidJS App)"] + web["web
(React Frontend)"] workers["workers
(Cloudflare Workers API)"] - ui["ui
(Shared Components)"] shared["shared
(Error Definitions)"] mcp["mcp
(Dev Tooling)"] end @@ -19,9 +17,7 @@ graph TB DO["Durable Objects"] end - web -->|"copied into"| landing web -->|"API calls"| workers - ui -->|"shared components"| web shared -->|"error utilities"| web shared -->|"error utilities"| workers workers --> D1 @@ -31,11 +27,9 @@ graph TB ## Package Details -| Package | Purpose | Tech | -| --------- | -------------------------------------- | ------------------------------- | -| `web` | Main SolidJS application | SolidJS, Vite, Tailwind | -| `workers` | Backend API and real-time sync | OpenAPIHono, Cloudflare Workers | -| `landing` | Marketing site (includes web app) | SolidStart | -| `ui` | Shared component library | SolidJS, Ark UI | -| `shared` | Shared error definitions and utilities | TypeScript | -| `mcp` | Development tooling (docs, linting) | Node.js | +| Package | Purpose | Tech | +| --------- | -------------------------------------- | ------------------------------------- | +| `web` | React frontend application | React, TanStack Start, Vite, Tailwind | +| `workers` | Backend API and real-time sync | OpenAPIHono, Cloudflare Workers | +| `shared` | Shared error definitions and utilities | TypeScript | +| `mcp` | Development tooling (docs, linting) | Node.js | diff --git a/packages/docs/audits/architecture-analysis.md b/packages/docs/audits/architecture-analysis.md index 4047ab4b..c9e5a452 100644 --- a/packages/docs/audits/architecture-analysis.md +++ b/packages/docs/audits/architecture-analysis.md @@ -50,13 +50,11 @@ The project uses a well-organized monorepo structure with clear package boundari ``` packages/ - ├── web/ # SolidJS frontend + ├── web/ # React/TanStack Start frontend ├── workers/ # Cloudflare Workers backend - ├── landing/ # Marketing site ├── shared/ # Shared error types and utilities ├── docs/ # VitePress documentation - ├── mcp/ # Development tooling - └── mcp-memory/ # Agent memory system + └── mcp/ # Development tooling ``` **Strengths:** @@ -73,8 +71,6 @@ shared (no dependencies) ↑ ├── web (depends on shared) └── workers (depends on shared) - ↑ - └── landing (depends on web via build artifact copy) ``` ### 1.2 Package Boundaries @@ -88,9 +84,8 @@ Each package has well-defined responsibilities: | shared | Domain errors, types, validators | Excellent | | web | UI components, stores, primitives | Excellent | | workers | API routes, middleware, Durable Obj | Excellent | -| landing | Marketing content, includes web app | Good (by design) | -**Finding:** No inappropriate cross-package dependencies detected. The `landing` package's inclusion of `web` is intentional for deployment purposes. +**Finding:** No inappropriate cross-package dependencies detected. ### 1.3 Build and Deployment Strategy @@ -98,7 +93,7 @@ Each package has well-defined responsibilities: - Independent build processes per package - Workers can deploy independently of frontend -- Frontend builds copy to landing for single-worker deployment +- Frontend deploys as a single Cloudflare Worker - No circular dependencies in build pipeline --- diff --git a/packages/docs/audits/codebase-audit-2026-03-17.md b/packages/docs/audits/codebase-audit-2026-03-17.md index 3022e18d..12add266 100644 --- a/packages/docs/audits/codebase-audit-2026-03-17.md +++ b/packages/docs/audits/codebase-audit-2026-03-17.md @@ -1,6 +1,6 @@ # Codebase Audit - 2026-03-17 -Comprehensive audit of the CoRATES monorepo covering the React frontend (`packages/landing`), Hono backend (`packages/workers`), and cross-cutting concerns (shared code, configuration, dependencies, documentation). +Comprehensive audit of the CoRATES monorepo covering the React frontend (`packages/web`), Hono backend (`packages/workers`), and cross-cutting concerns (shared code, configuration, dependencies, documentation). --- @@ -82,7 +82,7 @@ The Zod schema only checks `z.string().min(1)` -- no character set restriction. ### C5. `window.location.href` navigation bypasses TanStack Router -**File:** `packages/landing/src/components/dashboard/Dashboard.tsx:61, 65, 69` +**File:** `packages/web/src/components/dashboard/Dashboard.tsx:61, 65, 69` Three navigation handlers use `window.location.href` assignments, causing full page reloads and losing scroll state, query cache, and React context: @@ -100,7 +100,7 @@ function handleStartROBINSI() { **Files:** `.claude/CLAUDE.md:38`, `.github/copilot-instructions.md:38`, `package.json` -Both documentation files instruct agents to run `pnpm dev:front` to start the frontend. The root `package.json` has no such script. The actual script is `pnpm dev`, which delegates to `pnpm --filter landing dev`. +Both documentation files instruct agents to run `pnpm dev:front` to start the frontend. The root `package.json` has no such script. The actual script is `pnpm dev`, which delegates to `pnpm --filter web dev`. **Fix:** Either add a `dev:front` script or update documentation. @@ -118,7 +118,7 @@ A full Sentry DSN is committed in the `vars` block. The production comment block ### C8. `SubscriptionCard` uses `subscription: any` on exported prop interface -**File:** `packages/landing/src/components/billing/SubscriptionCard.tsx:53` +**File:** `packages/web/src/components/billing/SubscriptionCard.tsx:53` The component destructures `sub.tierInfo`, `sub.status`, `sub.tier`, `sub.cancelAtPeriodEnd`, `sub.currentPeriodEnd` without any type safety. Shape changes from the subscription API will silently produce `undefined`. @@ -130,7 +130,7 @@ The component destructures `sub.tierInfo`, `sub.status`, `sub.tier`, `sub.cancel ### I1. `useOAuthError` suppressed exhaustive-deps hides stale closure -**File:** `packages/landing/src/hooks/useOAuthError.ts:37` +**File:** `packages/web/src/hooks/useOAuthError.ts:37` The `useEffect` captures `location.pathname` from TanStack Router's `useLocation()` via closure but uses `[]` deps. The URL cleanup at lines 27 and 35 uses this captured value. While unlikely to manifest in practice (pathname is stable on mount), the suppressed lint rule hides a genuine dependency. Using `window.location.pathname` directly inside `cleanupUrl` would remove the closure dependency entirely. @@ -233,27 +233,19 @@ Not declared in the `Env` type. The cast silences TypeScript without fixing the --- -### I10. `CHECKLIST_STATUS` duplicated between shared and landing +### I10. `CHECKLIST_STATUS` duplicated between shared and web -**Files:** `packages/landing/src/constants/checklist-status.ts`, `packages/shared/src/checklists/status.ts` +**Files:** `packages/web/src/constants/checklist-status.ts`, `packages/shared/src/checklists/status.ts` -Near-exact copy. 12 files in landing import the local copy. If values diverge, the frontend silently uses stale logic. +Near-exact copy. 12 files in web import the local copy. If values diverge, the frontend silently uses stale logic. **Fix:** Delete the local copy and update 12 import sites to use `@corates/shared`. --- -### I11. `packages/web` (SolidJS legacy) still in workspace +### I12. Module-level auth cache in `web/src/lib/auth.ts` never invalidates -`packages/web` is still a workspace member. Its `build` script calls `copy-to-landing.js` which writes into landing's build output. SolidJS dependencies are still resolved by pnpm. - -**Fix:** Remove `packages/web` from the workspace or delete the directory. - ---- - -### I12. Module-level auth cache in `landing/src/lib/auth.ts` never invalidates - -**File:** `packages/landing/src/lib/auth.ts` +**File:** `packages/web/src/lib/auth.ts` A module-level `cachedAuth` variable deduplicates session requests but is never cleared. After signout, the marketing Navbar still shows the cached session until full page reload. @@ -263,7 +255,7 @@ A module-level `cachedAuth` variable deduplicates session requests but is never ### I13. `AuthProvider` useEffect captures stale `session.refetch` reference -**File:** `packages/landing/src/components/auth/AuthProvider.tsx:76-84` +**File:** `packages/web/src/components/auth/AuthProvider.tsx:76-84` Mount-only `useEffect` closes over the initial `session.refetch` reference with exhaustive-deps disabled. If the hook returns a new reference on hydration, the stale one is used. @@ -275,10 +267,10 @@ Mount-only `useEffect` closes over the initial `session.refetch` reference with **Files:** -- `packages/landing/src/components/Audience.tsx:44` -- `packages/landing/src/components/settings/PlansSettings.tsx:173` -- `packages/landing/src/components/HowItWorks.tsx:50` -- `packages/landing/src/components/FeatureShowcase.tsx:513, 609` +- `packages/web/src/components/Audience.tsx:44` +- `packages/web/src/components/settings/PlansSettings.tsx:173` +- `packages/web/src/components/HowItWorks.tsx:50` +- `packages/web/src/components/FeatureShowcase.tsx:513, 609` **Fix:** Use stable identifiers (title, id) as keys. @@ -286,7 +278,7 @@ Mount-only `useEffect` closes over the initial `session.refetch` reference with ### I15. `PlansSettings` mount effect captures stale `useCallback` closures -**File:** `packages/landing/src/components/settings/PlansSettings.tsx:96-98` +**File:** `packages/web/src/components/settings/PlansSettings.tsx:96-98` `processPendingPlan` depends on `[navigate, refetch]`, but the calling `useEffect` has `[]` deps with the lint rule suppressed. @@ -298,9 +290,9 @@ Mount-only `useEffect` closes over the initial `session.refetch` reference with **Files:** -- `packages/landing/src/components/checklist/ROB2Checklist/DomainSection.tsx:32` -- `packages/landing/src/components/checklist/ROBINSIChecklist/DomainSection.tsx:34` -- `packages/landing/src/stores/localChecklistsStore.ts:103, 117` +- `packages/web/src/components/checklist/ROB2Checklist/DomainSection.tsx:32` +- `packages/web/src/components/checklist/ROBINSIChecklist/DomainSection.tsx:34` +- `packages/web/src/stores/localChecklistsStore.ts:103, 117` **Fix:** Add index signatures or union types to checklist registries; type the Dexie database properly. @@ -308,7 +300,7 @@ Mount-only `useEffect` closes over the initial `session.refetch` reference with ### I17. `renderSidebarContent` is a plain function, not a component -**File:** `packages/landing/src/components/layout/Sidebar.tsx:168` +**File:** `packages/web/src/components/layout/Sidebar.tsx:168` Defined as a bare function inside the component body, called as `{renderSidebarContent()}`. Adding hooks inside it would cause React to throw. Same pattern in `SettingsSidebar.tsx:97`. @@ -318,7 +310,7 @@ Defined as a bare function inside the component body, called as `{renderSidebarC ### I18. `NotificationsSettings` toggles are non-functional stubs -**File:** `packages/landing/src/components/settings/NotificationsSettings.tsx:11-13` +**File:** `packages/web/src/components/settings/NotificationsSettings.tsx:11-13` All toggles reset to `false` on every mount. The `darkMode` switch is disabled. Users who toggle settings see no effect on reload. @@ -326,12 +318,6 @@ All toggles reset to `false` on every mount. The `darkMode` switch is disabled. --- -### I19. `better-sqlite3` major version mismatch across packages - -`packages/workers` uses `^12.6.2`, `packages/mcp-memory` uses `^11.10.0`. - -**Fix:** Align to the same major version. - --- ### I20. `vitest` pinned without comment in workers @@ -350,13 +336,13 @@ Workers pins `"vitest": "4.1.0"` (no caret) while all other packages use `^4.x`. Returns Tailwind class strings from a shared package. The file acknowledges this may not belong here. -**Fix:** Move `getStatusStyle` to a landing-side utility. +**Fix:** Move `getStatusStyle` to a web-side utility. --- ### I22. `BillingSettings` mount effect with stale `refetch` closure -**File:** `packages/landing/src/components/settings/BillingSettings.tsx:69-85` +**File:** `packages/web/src/components/settings/BillingSettings.tsx:69-85` The checkout redirect handler at lines 69-85 calls `refetch()` (line 73) and `usageQuery.refetch()` (line 74) inside a `useEffect` with `[]` deps. Both references are captured from closures that may change identity. Same stale-closure pattern as I13 and I15. @@ -366,7 +352,7 @@ The checkout redirect handler at lines 69-85 calls `refetch()` (line 73) and `us ### I23. `ActivityFeed` uses non-unique composite key -**File:** `packages/landing/src/components/dashboard/ActivityFeed.tsx:72` +**File:** `packages/web/src/components/dashboard/ActivityFeed.tsx:72` ```tsx key={`${activity.title}-${activity.timestamp}`} @@ -392,7 +378,7 @@ The on-disk `.claude/CLAUDE.md` has been updated to describe a React/TanStack St **File:** `packages/docs/STATUS.md:93` -Lists `@corates/ui` as a complete shared package. No such package exists in the workspace. UI components live in `packages/landing/src/components/ui/` as shadcn/ui wrappers. +Lists `@corates/ui` as a complete shared package. No such package exists in the workspace. UI components live in `packages/web/src/components/ui/` as shadcn/ui wrappers. **Fix:** Remove the `@corates/ui` entry from STATUS.md. @@ -400,7 +386,7 @@ Lists `@corates/ui` as a complete shared package. No such package exists in the ### D3. `isEditable` type signature differs between shared and local copy -The shared version accepts `ChecklistStatus | string`; the landing copy accepts only `string`. Migration to the shared import will introduce type errors for callers passing optional status fields. +The shared version accepts `ChecklistStatus | string`; the web copy accepts only `string`. Migration to the shared import will introduce type errors for callers passing optional status fields. **Fix:** Reconcile types during the deduplication (I10). diff --git a/packages/docs/audits/complexity-audit-2026-03.md b/packages/docs/audits/complexity-audit-2026-03.md index ea5a8fbe..0ead60a0 100644 --- a/packages/docs/audits/complexity-audit-2026-03.md +++ b/packages/docs/audits/complexity-audit-2026-03.md @@ -6,9 +6,9 @@ A full-codebase audit focused on identifying sources of complexity, maintenance ## Executive Summary -The codebase has solid architectural foundations -- good package boundaries, a well-structured shared package, and clean backend patterns. The SolidJS web package is being retired imminently (within the week) in favor of the React-based landing package, so frontend findings focus on the landing package and shared concerns only. The primary sources of complexity are: +The codebase has solid architectural foundations -- good package boundaries, a well-structured shared package, and clean backend patterns. The SolidJS web package is being retired imminently (within the week) in favor of the React-based web package, so frontend findings focus on the web package and shared concerns only. The primary sources of complexity are: -1. **Framework-agnostic utilities duplicated in landing** that should be extracted to `@corates/shared` before the web package is deleted (13 files) +1. **Framework-agnostic utilities duplicated in web** that should be extracted to `@corates/shared` before the web package is deleted (13 files) 2. **Security gaps** in the backend (CSRF, user deletion, auth bypass) 3. **Large backend files** concentrating too many concerns 4. **Duplicated backend patterns** (error schemas, middleware helpers, invitation logic) @@ -73,7 +73,7 @@ Every `DB_ERROR` construction passes `originalError: error.message` unconditiona ### A1. Framework-agnostic utilities must be extracted before web package deletion -The web package is being deleted within the week. 13 framework-agnostic utility files currently live as local copies in the landing package. Once web is gone, these become the only copies with no upstream reference. They should be extracted to `@corates/shared` or a new `@corates/lib` package now. +The web package is being deleted within the week. 13 framework-agnostic utility files currently live as local copies in the web package. Once web is gone, these become the only copies with no upstream reference. They should be extracted to `@corates/shared` or a new `@corates/lib` package now. **Immediately shareable (identical or import-path-only differences):** @@ -89,13 +89,13 @@ The web package is being deleted within the week. 13 framework-agnostic utility | File | Issue | | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `checklist-domain.js` | Landing has more accurate JSDoc types (`string \| null` for userId) -- use landing version | +| `checklist-domain.js` | Landing has more accurate JSDoc types (`string \| null` for userId) -- use web version | | `checklist-registry/index.js` | Landing already imports from `@corates/shared` (correct); web still imports from component-local files | -| `useProject/sync.js` | Framework-specific store access (`useProjectStore.getState()` vs `projectStore`). Core sync logic (buildStudyFromYMap, etc.) is identical and extractable; store interaction stays in landing | +| `useProject/sync.js` | Framework-specific store access (`useProjectStore.getState()` vs `projectStore`). Core sync logic (buildStudyFromYMap, etc.) is identical and extractable; store interaction stays in web | | `useProject/studies.js` | Same store access difference. CRUD logic is identical and extractable | -| `useProject/annotations.js` | Landing version is cleaner (removed debug console.logs from web). Use landing version | +| `useProject/annotations.js` | Landing version is cleaner (removed debug console.logs from web). Use web version | | `error-utils` | Most diverged file. Landing has TypeScript types, different navigate API shape (`{ to: '/signin' }` vs `('/signin')`), and different route name (`/check-email` vs `/verify-email`). Needs a navigate abstraction to share | -| `account-merge` | Landing has TypeScript interfaces. Runtime logic identical. Use landing `.ts` version | +| `account-merge` | Landing has TypeScript interfaces. Runtime logic identical. Use web `.ts` version | ### A2. `runMiddleware` helper duplicated in 5 backend files @@ -136,11 +136,11 @@ Backend files over 500 lines that should be split: Landing package files to watch as the migration continues: -| Lines | File | Notes | -| ----- | ---------------------------------------------------------------- | ----------------------------------------------------------------- | -| 570 | `landing/components/FeatureShowcase.tsx` | Marketing component, acceptable for now | -| 508 | `landing/components/billing/PricingTable.tsx` | Plan card, feature comparison, billing toggle could be split | -| 501 | `landing/components/project/overview-tab/ReviewerAssignment.tsx` | Percentage slider, preset selector, allocation logic as utilities | +| Lines | File | Notes | +| ----- | ------------------------------------------------------------ | ----------------------------------------------------------------- | +| 570 | `web/components/FeatureShowcase.tsx` | Marketing component, acceptable for now | +| 508 | `web/components/billing/PricingTable.tsx` | Plan card, feature comparison, billing toggle could be split | +| 501 | `web/components/project/overview-tab/ReviewerAssignment.tsx` | Percentage slider, preset selector, allocation logic as utilities | --- @@ -225,8 +225,8 @@ Workers pins `vitest@3.2.0` for `@cloudflare/vitest-pool-workers` compatibility. 2. S2 -- Add CSRF protection to state-changing endpoints 3. S3 -- Add org-level auth to Google Drive import -**Short-term (before web package deletion):** 4. A1 -- Extract framework-agnostic utilities to `@corates/shared` before the web package is deleted and the landing copies become orphaned 5. S4 -- Gate error detail leaking in production +**Short-term (before web package deletion):** 4. A1 -- Extract framework-agnostic utilities to `@corates/shared` before the web package is deleted and the web copies become orphaned 5. S4 -- Gate error detail leaking in production **Medium-term (reduce backend complexity):** 6. A2 -- Extract runMiddleware helper 7. A3 -- Unify invitation acceptance logic 8. Split the largest backend route files (orgs/index.ts, orgs/invitations.ts, orgs/pdfs.ts) 9. B1 -- Cache orgBilling resolution across middleware 10. Q2 -- Upgrade workers to vitest 4 + pool-workers 0.13.0 to unify test infrastructure -**Longer-term (structural improvements):** 11. T1 -- Generate API types from OpenAPI schema for landing package 12. T3 -- Unify subscription tier naming 13. B4 -- Move rate limiting to Cloudflare's native solution 14. B5 -- Address the 106 `@ts-expect-error` comments with a route factory pattern +**Longer-term (structural improvements):** 11. T1 -- Generate API types from OpenAPI schema for web package 12. T3 -- Unify subscription tier naming 13. B4 -- Move rate limiting to Cloudflare's native solution 14. B5 -- Address the 106 `@ts-expect-error` comments with a route factory pattern diff --git a/packages/docs/audits/e2e-dual-reviewer-flow-plan.md b/packages/docs/audits/e2e-dual-reviewer-flow-plan.md index 6bd27bf3..a669dba0 100644 --- a/packages/docs/audits/e2e-dual-reviewer-flow-plan.md +++ b/packages/docs/audits/e2e-dual-reviewer-flow-plan.md @@ -43,7 +43,7 @@ TanStack Router handles all navigation internally. The test clicks links/buttons ### Step 1: Clean up old files -Delete from `packages/landing/src/primitives/useProject/__tests__/`: +Delete from `packages/web/src/primitives/useProject/__tests__/`: - `helpers.ts` - `domain-operations.browser.test.ts` @@ -51,9 +51,9 @@ Delete from `packages/landing/src/primitives/useProject/__tests__/`: - `yjs-sync.browser.test.ts` - `__screenshots__/` directory -Delete `packages/landing/src/__e2e__/commands.d.ts` (no longer needed). +Delete `packages/web/src/__e2e__/commands.d.ts` (no longer needed). -Keep `packages/landing/vitest.browser.config.ts` (for future data-layer tests). +Keep `packages/web/vitest.browser.config.ts` (for future data-layer tests). ### Step 2: Add `testUtils` plugin to Better Auth config @@ -80,7 +80,7 @@ Mount at `/api/test/*` in the main Hono router, gated behind `DEV_MODE`. ### Step 4: E2E test config (already created, needs minor updates) -File: `packages/landing/vitest.e2e.config.ts` +File: `packages/web/vitest.e2e.config.ts` Already created during POC with: @@ -93,7 +93,7 @@ Already created during POC with: ### Step 5: Create test helpers -Directory: `packages/landing/src/__e2e__/helpers/` +Directory: `packages/web/src/__e2e__/helpers/` **`app.tsx`** -- Creates and renders the full app: @@ -125,7 +125,7 @@ function createTestApp(initialPath: string) { ### Step 6: Write the main test -File: `packages/landing/src/__e2e__/dual-reviewer-flow.browser.test.tsx` +File: `packages/web/src/__e2e__/dual-reviewer-flow.browser.test.tsx` ``` describe('Dual-Reviewer AMSTAR2 Workflow') @@ -192,38 +192,38 @@ Discover during implementation. Likely candidates: ## Files to Create/Modify -| File | Action | -| ------------------------------------------------------------------ | ---------------------------------------- | -| `packages/workers/src/auth/config.ts` | Add testUtils plugin (conditional) | -| `packages/workers/src/routes/test-seed.ts` | Create -- seed/session/cleanup endpoints | -| `packages/workers/src/index.ts` (or router) | Mount test-seed routes | -| `packages/landing/vitest.e2e.config.ts` | Already created -- minor updates | -| `packages/landing/src/__e2e__/helpers/app.tsx` | Create | -| `packages/landing/src/__e2e__/helpers/seed.ts` | Create | -| `packages/landing/src/__e2e__/helpers/auth.ts` | Create | -| `packages/landing/src/__e2e__/helpers/interactions.ts` | Create | -| `packages/landing/src/__e2e__/dual-reviewer-flow.browser.test.tsx` | Create | -| Various components | Add data-testid as needed | +| File | Action | +| -------------------------------------------------------------- | ---------------------------------------- | +| `packages/workers/src/auth/config.ts` | Add testUtils plugin (conditional) | +| `packages/workers/src/routes/test-seed.ts` | Create -- seed/session/cleanup endpoints | +| `packages/workers/src/index.ts` (or router) | Mount test-seed routes | +| `packages/web/vitest.e2e.config.ts` | Already created -- minor updates | +| `packages/web/src/__e2e__/helpers/app.tsx` | Create | +| `packages/web/src/__e2e__/helpers/seed.ts` | Create | +| `packages/web/src/__e2e__/helpers/auth.ts` | Create | +| `packages/web/src/__e2e__/helpers/interactions.ts` | Create | +| `packages/web/src/__e2e__/dual-reviewer-flow.browser.test.tsx` | Create | +| Various components | Add data-testid as needed | ## Key Source Files (reference) - `packages/workers/src/routes/orgs/dev-routes.ts` -- existing DEV_MODE route pattern - `packages/workers/src/__tests__/helpers.ts` -- seed functions -- `packages/landing/src/routes/__root.tsx` -- RootLayout (QueryClientProvider + AuthProvider + Outlet) -- `packages/landing/src/routeTree.gen.ts` -- generated route tree -- `packages/landing/src/routes/_auth/signin.tsx` -- #email-input, password form -- `packages/landing/src/stores/authStore.ts` -- auth state, cached user -- `packages/landing/src/components/project/CreateProjectModal.tsx` -- #project-name input -- `packages/landing/src/components/project/all-studies-tab/AllStudiesTab.tsx` -- `packages/landing/src/components/project/all-studies-tab/AssignReviewersModal.tsx` -- `packages/landing/src/components/project/todo-tab/ToDoTab.tsx` -- `packages/landing/src/components/project/reconcile-tab/ReconcileTab.tsx` -- `packages/landing/src/components/project/reconcile-tab/ReconciliationWrapper.tsx` -- `packages/landing/src/config/api.ts` -- API_BASE, getWsBaseUrl +- `packages/web/src/routes/__root.tsx` -- RootLayout (QueryClientProvider + AuthProvider + Outlet) +- `packages/web/src/routeTree.gen.ts` -- generated route tree +- `packages/web/src/routes/_auth/signin.tsx` -- #email-input, password form +- `packages/web/src/stores/authStore.ts` -- auth state, cached user +- `packages/web/src/components/project/CreateProjectModal.tsx` -- #project-name input +- `packages/web/src/components/project/all-studies-tab/AllStudiesTab.tsx` +- `packages/web/src/components/project/all-studies-tab/AssignReviewersModal.tsx` +- `packages/web/src/components/project/todo-tab/ToDoTab.tsx` +- `packages/web/src/components/project/reconcile-tab/ReconcileTab.tsx` +- `packages/web/src/components/project/reconcile-tab/ReconciliationWrapper.tsx` +- `packages/web/src/config/api.ts` -- API_BASE, getWsBaseUrl ## Bug Found During Data-Layer Testing -ROBINS-I auto-fill of sectionA.outcome Y.Text fails silently because the auto-fill runs on a detached Y.Map (answersYMap not yet added to the Y.Doc). Fix: move `checklistsMap.set(checklistId, checklistYMap)` before the auto-fill block in `packages/landing/src/primitives/useProject/checklists/index.js` (line 193 should come before line 172). +ROBINS-I auto-fill of sectionA.outcome Y.Text fails silently because the auto-fill runs on a detached Y.Map (answersYMap not yet added to the Y.Doc). Fix: move `checklistsMap.set(checklistId, checklistYMap)` before the auto-fill block in `packages/web/src/primitives/useProject/checklists/index.js` (line 193 should come before line 172). ## Verification @@ -232,5 +232,5 @@ ROBINS-I auto-fill of sectionA.outcome Y.Text fails silently because the auto-fi pnpm dev:workers # Run e2e tests (no frontend dev server needed) -pnpm --filter landing test:e2e +pnpm --filter web test:e2e ``` diff --git a/packages/docs/audits/landing-simplify-2026-03-21.md b/packages/docs/audits/landing-simplify-2026-03-21.md index c019310f..1e3d6100 100644 --- a/packages/docs/audits/landing-simplify-2026-03-21.md +++ b/packages/docs/audits/landing-simplify-2026-03-21.md @@ -1,6 +1,6 @@ # Landing Package Simplify Audit - 2026-03-21 -Full review of `/packages/landing/src` (477 source files) for code reuse, quality, and efficiency. +Full review of `/packages/web/src` (477 source files) for code reuse, quality, and efficiency. ## Preferences diff --git a/packages/docs/audits/local-first-analysis.md b/packages/docs/audits/local-first-analysis.md index 0ebdb795..022846c7 100644 --- a/packages/docs/audits/local-first-analysis.md +++ b/packages/docs/audits/local-first-analysis.md @@ -95,7 +95,7 @@ CoRATES demonstrates **strong local-first fundamentals** with a sophisticated Yj **Application Shell:** ```javascript -// packages/landing/src/entry-client.jsx:39-58 +// packages/web/src/entry-client.jsx:39-58 if ('serviceWorker' in navigator) { window.addEventListener('load', async () => { const registrations = await navigator.serviceWorker.getRegistrations(); diff --git a/packages/docs/audits/prod-audit-2026-01-19-full.md b/packages/docs/audits/prod-audit-2026-01-19-full.md index 30bc64e0..c68593c5 100644 --- a/packages/docs/audits/prod-audit-2026-01-19-full.md +++ b/packages/docs/audits/prod-audit-2026-01-19-full.md @@ -68,12 +68,10 @@ CoRATES has solid code architecture and security fundamentals, but critical gaps ``` packages/ - web/ - Frontend SolidJS application + web/ - React/TanStack Start frontend workers/ - Backend Hono API + Durable Objects - landing/ - Marketing site (embeds web at build) shared/ - Common utilities and error definitions mcp/ - MCP server for development tools - mcp-memory/ - Persistent agent memory docs/ - Internal documentation ``` @@ -241,7 +239,7 @@ Test documents that title-cleaning regex only works for prefixes, not mid-title #### LOW: Incomplete Contact Page FAQ -**Location:** `packages/landing/src/routes/contact.jsx:111` +**Location:** `packages/web/src/routes/contact.jsx:111` ```jsx { @@ -529,7 +527,7 @@ Given low traffic expectation (<100 MAU), these are acceptable risks for launch. ```bash # From package.json -pnpm deploy # runs deploy:workers && deploy:landing +pnpm deploy # runs deploy:workers && deploy:web ``` Deployment via `wrangler deploy --env production`. No CI/CD pipeline. diff --git a/packages/docs/audits/rob2-visualizations-plan.md b/packages/docs/audits/rob2-visualizations-plan.md index d2872975..1aa02402 100644 --- a/packages/docs/audits/rob2-visualizations-plan.md +++ b/packages/docs/audits/rob2-visualizations-plan.md @@ -39,7 +39,7 @@ Uses existing `getActiveDomainKeys()` to determine which domain2 variant is acti ### Step 2: Wire up consolidatedAnswers for ROB2 in sync.ts -**File**: `packages/landing/src/primitives/useProject/sync.ts` (line ~247) +**File**: `packages/web/src/primitives/useProject/sync.ts` (line ~247) Add a parallel block after the AMSTAR2 consolidation: @@ -51,13 +51,13 @@ if (checklistType === 'ROB2') { ### Step 3: Extract shared `exportChart` utility -**File (new)**: `packages/landing/src/components/charts/export-chart.ts` +**File (new)**: `packages/web/src/components/charts/export-chart.ts` Move the `exportChart` function from `ChartSection.tsx` into a shared utility. Update `ChartSection.tsx` to import from the new location. ### Step 4: Create `ROB2Robvis.tsx` -- Traffic light heatmap -**File (new)**: `packages/landing/src/components/charts/ROB2Robvis.tsx` +**File (new)**: `packages/web/src/components/charts/ROB2Robvis.tsx` D3-based SVG component mirroring `AMSTARRobvis.tsx` with these differences: @@ -81,7 +81,7 @@ interface ROB2RobvisDataItem { ### Step 5: Create `ROB2Distribution.tsx` -- Stacked bar chart -**File (new)**: `packages/landing/src/components/charts/ROB2Distribution.tsx` +**File (new)**: `packages/web/src/components/charts/ROB2Distribution.tsx` D3-based SVG component mirroring `AMSTARDistribution.tsx` with these differences: @@ -94,7 +94,7 @@ D3-based SVG component mirroring `AMSTARDistribution.tsx` with these differences ### Step 6: Create `ROB2ChartSection.tsx` -- Orchestrator -**File (new)**: `packages/landing/src/components/project/overview-tab/ROB2ChartSection.tsx` +**File (new)**: `packages/web/src/components/project/overview-tab/ROB2ChartSection.tsx` Parallel to existing `ChartSection.tsx`. Responsibilities: @@ -108,26 +108,26 @@ Parallel to existing `ChartSection.tsx`. Responsibilities: ### Step 7: Integrate into OverviewTab -**File**: `packages/landing/src/components/project/overview-tab/OverviewTab.tsx` (~line 374) +**File**: `packages/web/src/components/project/overview-tab/OverviewTab.tsx` (~line 374) Add `ROB2ChartSection` alongside `ChartSection` in the Figures collapsible section. Each handles its own empty state. When both have data, they stack vertically with spacing. ## Files Modified (summary) -| File | Action | -| --------------------------------------------------------------------------- | ----------------------------------- | -| `packages/shared/src/checklists/rob2/answers.ts` | Add `getConsolidatedAnswers` | -| `packages/landing/src/primitives/useProject/sync.ts` | Add ROB2 consolidation block | -| `packages/landing/src/components/charts/export-chart.ts` | **New** -- extracted utility | -| `packages/landing/src/components/charts/ROB2Robvis.tsx` | **New** -- traffic light chart | -| `packages/landing/src/components/charts/ROB2Distribution.tsx` | **New** -- distribution chart | -| `packages/landing/src/components/project/overview-tab/ROB2ChartSection.tsx` | **New** -- orchestrator | -| `packages/landing/src/components/project/overview-tab/OverviewTab.tsx` | Add ROB2ChartSection | -| `packages/landing/src/components/project/overview-tab/ChartSection.tsx` | Import exportChart from shared util | +| File | Action | +| ----------------------------------------------------------------------- | ----------------------------------- | +| `packages/shared/src/checklists/rob2/answers.ts` | Add `getConsolidatedAnswers` | +| `packages/web/src/primitives/useProject/sync.ts` | Add ROB2 consolidation block | +| `packages/web/src/components/charts/export-chart.ts` | **New** -- extracted utility | +| `packages/web/src/components/charts/ROB2Robvis.tsx` | **New** -- traffic light chart | +| `packages/web/src/components/charts/ROB2Distribution.tsx` | **New** -- distribution chart | +| `packages/web/src/components/project/overview-tab/ROB2ChartSection.tsx` | **New** -- orchestrator | +| `packages/web/src/components/project/overview-tab/OverviewTab.tsx` | Add ROB2ChartSection | +| `packages/web/src/components/project/overview-tab/ChartSection.tsx` | Import exportChart from shared util | ## Verification -1. `pnpm --filter landing build` -- confirm no type/build errors +1. `pnpm --filter web build` -- confirm no type/build errors 2. `pnpm typecheck` -- verify types across packages 3. `pnpm lint` -- pass linting 4. Manual test: open a project with finalized ROB2 checklists, expand Figures section, verify both charts render with correct domain judgments and colors diff --git a/packages/docs/audits/yjs-browser-e2e-test-plan.md b/packages/docs/audits/yjs-browser-e2e-test-plan.md index e0347e33..9790b547 100644 --- a/packages/docs/audits/yjs-browser-e2e-test-plan.md +++ b/packages/docs/audits/yjs-browser-e2e-test-plan.md @@ -2,7 +2,7 @@ ## Context -The landing package has a comprehensive Yjs implementation for real-time collaborative research projects. Existing tests cover basic Dexie round-trips, basic sync pipeline, and basic multi-client convergence (8 tests). The domain operation layer (studies, checklists, PDFs, annotations, reconciliation, outcomes) has zero test coverage. These operations are where production bugs cause data loss. +The web package has a comprehensive Yjs implementation for real-time collaborative research projects. Existing tests cover basic Dexie round-trips, basic sync pipeline, and basic multi-client convergence (8 tests). The domain operation layer (studies, checklists, PDFs, annotations, reconciliation, outcomes) has zero test coverage. These operations are where production bugs cause data loss. ## Approach @@ -123,14 +123,14 @@ describe('Full sync pipeline') ## Files to Modify/Create -| File | Action | -| ----------------------------------------------------------------------------------------------- | ---------------------------- | -| `packages/landing/src/primitives/useProject/__tests__/helpers.ts` | Create | -| `packages/landing/src/primitives/useProject/__tests__/domain-operations.browser.test.ts` | Create | -| `packages/landing/src/primitives/useProject/__tests__/checklist-handlers.browser.test.ts` | Create | -| `packages/landing/src/primitives/useProject/__tests__/reconciliation.browser.test.ts` | Create | -| `packages/landing/src/primitives/useProject/__tests__/multi-client-convergence.browser.test.ts` | Create | -| `packages/landing/src/primitives/useProject/__tests__/yjs-sync.browser.test.ts` | Update to use shared helpers | +| File | Action | +| ------------------------------------------------------------------------------------------- | ---------------------------- | +| `packages/web/src/primitives/useProject/__tests__/helpers.ts` | Create | +| `packages/web/src/primitives/useProject/__tests__/domain-operations.browser.test.ts` | Create | +| `packages/web/src/primitives/useProject/__tests__/checklist-handlers.browser.test.ts` | Create | +| `packages/web/src/primitives/useProject/__tests__/reconciliation.browser.test.ts` | Create | +| `packages/web/src/primitives/useProject/__tests__/multi-client-convergence.browser.test.ts` | Create | +| `packages/web/src/primitives/useProject/__tests__/yjs-sync.browser.test.ts` | Update to use shared helpers | ## Key Source Files (read-only reference) @@ -174,13 +174,13 @@ ydoc.destroy(); ```bash # Run all browser tests -pnpm --filter landing test:browser +pnpm --filter web test:browser # Run specific file -pnpm --filter landing test:browser -- domain-operations +pnpm --filter web test:browser -- domain-operations # Interactive UI -pnpm --filter landing test:browser:ui +pnpm --filter web test:browser:ui ``` All 8 existing tests should continue passing. New tests should add ~45 more. diff --git a/packages/docs/audits/yjs-sync-pipeline-redesign-a.md b/packages/docs/audits/yjs-sync-pipeline-redesign-a.md index cdf2ef8c..d0b4e1da 100644 --- a/packages/docs/audits/yjs-sync-pipeline-redesign-a.md +++ b/packages/docs/audits/yjs-sync-pipeline-redesign-a.md @@ -4,7 +4,7 @@ RFC for restructuring the Y.js-to-React data pipeline in CoRATES. **Status**: Draft **Date**: 2026-03-24 -**Scope**: `packages/landing` sync infrastructure, `packages/workers` ProjectDoc (minor) +**Scope**: `packages/web` sync infrastructure, `packages/workers` ProjectDoc (minor) **Supersedes**: `yjs-sync-pipeline-redesign.md` (earlier draft) --- @@ -854,7 +854,7 @@ Check whether these are still imported after the migration: - `y-websocket` direct imports (should only be used inside `WebSocketSyncProvider`) - If `zustand/middleware/immer` is no longer needed after `projectStore` shrinks, remove it -Verify with: `pnpm --filter landing build` (tree-shaking will flag unused imports as errors if `verbatimModuleSyntax` is on). +Verify with: `pnpm --filter web build` (tree-shaking will flag unused imports as errors if `verbatimModuleSyntax` is on). #### 6e: Clean Up the `primitives/useProject/` Directory @@ -928,10 +928,10 @@ Before opening the PR to merge the feature branch: - [ ] `pnpm typecheck` passes with no errors - [ ] `pnpm lint` passes (no unused imports, no unused variables) -- [ ] `pnpm --filter landing build` succeeds (tree-shaking catches dead code) -- [ ] `pnpm --filter landing test` passes (unit tests) +- [ ] `pnpm --filter web build` succeeds (tree-shaking catches dead code) +- [ ] `pnpm --filter web test` passes (unit tests) - [ ] `pnpm --filter workers test` passes (ProjectDoc tests) -- [ ] `pnpm --filter landing test:browser` passes (E2E: all checklist workflows, reconciliation, multi-user) +- [ ] `pnpm --filter web test:browser` passes (E2E: all checklist workflows, reconciliation, multi-user) - [ ] `grep -r "connectionPool" src/` returns zero results (fully removed) - [ ] `grep -r "setProjectData" src/` returns zero results (fully removed) - [ ] `grep -r "selectStudies\|selectStudy\|selectChecklist\|selectMembers\|selectMeta" src/` returns zero results (all old selectors removed) diff --git a/packages/docs/glossary.md b/packages/docs/glossary.md index dff21e9b..4ef574dc 100644 --- a/packages/docs/glossary.md +++ b/packages/docs/glossary.md @@ -146,7 +146,7 @@ Browser background script for offline capabilities (currently disabled): - Enables offline UI access - Uses network-first strategy -**Related:** Currently commented out in landing package +**Related:** Currently commented out in web package ### Drizzle ORM diff --git a/packages/docs/guides/configuration.md b/packages/docs/guides/configuration.md index 80a1e16f..c43cc6e5 100644 --- a/packages/docs/guides/configuration.md +++ b/packages/docs/guides/configuration.md @@ -12,9 +12,8 @@ The project is organized as a monorepo with packages under `packages/`: ``` packages/ -├── web/ # Frontend application (SolidJS) +├── web/ # Frontend application (React/TanStack Start) ├── workers/ # Backend API (Cloudflare Workers) -├── landing/ # Landing/marketing site ├── ui/ # Shared UI component library ├── shared/ # Shared TypeScript utilities ├── mcp/ # MCP server for development tools diff --git a/packages/landing/.env.example b/packages/landing/.env.example deleted file mode 100644 index 788c37c1..00000000 --- a/packages/landing/.env.example +++ /dev/null @@ -1,17 +0,0 @@ -# Copy to .env and .env.production and set appropriate values -VITE_API_URL="http://localhost:8787" -VITE_PUBLIC_APP_URL="http://localhost:3010" - -# URL Base Path -VITE_BASEPATH="/" - -# For Google Picker integration -VITE_GOOGLE_PICKER_API_KEY="your-google-picker-api-key-here" -VITE_GOOGLE_PICKER_APP_ID="your-google-project-number" - -# Sentry error monitoring (optional) -# Get DSN from https://sentry.io/settings/projects/YOUR_PROJECT/keys/ -VITE_SENTRY_DSN="" - -# Set to "true" to send errors to Sentry in development -VITE_SENTRY_DEV=false diff --git a/packages/landing/.gitignore b/packages/landing/.gitignore deleted file mode 100644 index 55afd9f9..00000000 --- a/packages/landing/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -node_modules -dist -.DS_Store -*.local -.env -.env.local -.env.production -.output -.wrangler -.vinxi -.tanstack -routeTree.gen.ts diff --git a/packages/landing/package.json b/packages/landing/package.json deleted file mode 100644 index a32ae6f3..00000000 --- a/packages/landing/package.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "name": "landing", - "version": "1.0.0", - "private": true, - "license": "PolyForm-Noncommercial-1.0.0", - "type": "module", - "scripts": { - "dev": "vite dev --port 3010", - "build": "vite build", - "build:prod": "pnpm --filter @corates/shared build && pnpm run build", - "preview": "vite preview", - "deploy": "wrangler deploy", - "lint": "eslint", - "format": "prettier --write .", - "check": "prettier --write . && eslint --fix", - "typecheck": "tsc --noEmit", - "test": "vitest run --config vitest.config.ts --reporter=verbose", - "test:watch": "vitest --config vitest.config.ts", - "test:e2e": "playwright test" - }, - "dependencies": { - "@ark-ui/react": "^5.35.0", - "@corates/shared": "workspace:*", - "@embedpdf/core": "^2.10.1", - "@embedpdf/engines": "^2.10.1", - "@embedpdf/models": "^2.10.1", - "@embedpdf/pdfium": "^2.10.1", - "@embedpdf/plugin-annotation": "^2.10.1", - "@embedpdf/plugin-capture": "^2.10.1", - "@embedpdf/plugin-commands": "^2.10.1", - "@embedpdf/plugin-document-manager": "^2.10.1", - "@embedpdf/plugin-export": "^2.10.1", - "@embedpdf/plugin-fullscreen": "^2.10.1", - "@embedpdf/plugin-history": "^2.10.1", - "@embedpdf/plugin-i18n": "^2.10.1", - "@embedpdf/plugin-interaction-manager": "^2.10.1", - "@embedpdf/plugin-pan": "^2.10.1", - "@embedpdf/plugin-print": "^2.10.1", - "@embedpdf/plugin-redaction": "^2.10.1", - "@embedpdf/plugin-render": "^2.10.1", - "@embedpdf/plugin-rotate": "^2.10.1", - "@embedpdf/plugin-scroll": "^2.10.1", - "@embedpdf/plugin-search": "^2.10.1", - "@embedpdf/plugin-selection": "^2.10.1", - "@embedpdf/plugin-spread": "^2.10.1", - "@embedpdf/plugin-thumbnail": "^2.10.1", - "@embedpdf/plugin-tiling": "^2.10.1", - "@embedpdf/plugin-ui": "^2.10.1", - "@embedpdf/plugin-view-manager": "^2.10.1", - "@embedpdf/plugin-viewport": "^2.10.1", - "@embedpdf/plugin-zoom": "^2.10.1", - "@fontsource-variable/geist": "^5.2.8", - "@sentry/react": "^10.46.0", - "@tanstack/react-query": "^5.95.2", - "@tanstack/react-router": "^1.168.8", - "@tanstack/react-start": "^1.167.13", - "@tanstack/react-table": "^8.21.3", - "better-auth": "^1.5.6", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "countup.js": "^2.10.0", - "d3": "^7.9.0", - "dexie": "^4.4.1", - "hono": "^4.12.9", - "immer": "^11.1.4", - "input-otp": "^1.4.2", - "lucide-react": "^0.577.0", - "radix-ui": "^1.4.3", - "react": "19.2.4", - "react-dom": "19.2.4", - "recharts": "^3.8.1", - "shadcn": "^4.1.1", - "sonner": "^2.0.7", - "tailwind-merge": "^3.5.0", - "tw-animate-css": "^1.4.0", - "y-dexie": "^4.2.2", - "y-websocket": "^3.0.0", - "yjs": "^13.6.30", - "zustand": "^5.0.12" - }, - "devDependencies": { - "@cloudflare/vite-plugin": "^1.30.2", - "@playwright/test": "^1.58.2", - "@tailwindcss/vite": "^4.2.2", - "@tanstack/eslint-config": "^0.4.0", - "@tanstack/router-plugin": "^1.167.9", - "@testing-library/jest-dom": "^6.9.1", - "@testing-library/react": "^16.3.2", - "@types/react": "19.2.14", - "@types/react-dom": "19.2.3", - "@vitejs/plugin-react": "^5.0.4", - "@vitest/browser": "^4.1.2", - "@vitest/browser-playwright": "^4.1.2", - "fake-indexeddb": "^6.2.5", - "jsdom": "^29.0.1", - "tailwindcss": "^4.2.2", - "typescript": "^5.9.3", - "vite": "^7.3.1", - "vite-tsconfig-paths": "^5.1.4", - "vitest": "^4.1.2", - "vitest-browser-react": "^2.1.0", - "wrangler": "^4.78.0" - } -} diff --git a/packages/landing/public/favicon.ico b/packages/landing/public/favicon.ico deleted file mode 100644 index 7f850e4b..00000000 Binary files a/packages/landing/public/favicon.ico and /dev/null differ diff --git a/packages/landing/public/logo.svg b/packages/landing/public/logo.svg deleted file mode 100644 index 6116ff7b..00000000 --- a/packages/landing/public/logo.svg +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/landing/public/logos/drive.svg b/packages/landing/public/logos/drive.svg deleted file mode 100644 index c8b52eff..00000000 --- a/packages/landing/public/logos/drive.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/packages/landing/public/logos/google.svg b/packages/landing/public/logos/google.svg deleted file mode 100644 index 986851c0..00000000 --- a/packages/landing/public/logos/google.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/landing/public/logos/orcid.svg b/packages/landing/public/logos/orcid.svg deleted file mode 100644 index 97a51e19..00000000 --- a/packages/landing/public/logos/orcid.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/packages/landing/src/api/__tests__/better-auth-store.test.js b/packages/landing/src/api/__tests__/better-auth-store.test.js deleted file mode 100644 index 4a529db3..00000000 --- a/packages/landing/src/api/__tests__/better-auth-store.test.js +++ /dev/null @@ -1,614 +0,0 @@ -/** - * Tests for authStore (Zustand) - Authentication flows and state management - * - * Note: These tests focus on the business logic and state management. - * Better Auth client internals are mocked. - */ - -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import 'fake-indexeddb/auto'; - -// Mock dependencies before importing the store -vi.mock('@/api/auth-client', () => ({ - authClient: { - signUp: { - email: vi.fn(), - }, - signIn: { - email: vi.fn(), - social: vi.fn(), - oauth2: vi.fn(), - magicLink: vi.fn(), - }, - signOut: vi.fn(), - updateUser: vi.fn(), - changePassword: vi.fn(), - requestPasswordReset: vi.fn(), - resetPassword: vi.fn(), - twoFactor: { - enable: vi.fn(), - verifyTotp: vi.fn(), - disable: vi.fn(), - }, - sendVerificationEmail: vi.fn(), - }, - listSessions: vi.fn(), - revokeSession: vi.fn(), - revokeOtherSessions: vi.fn(), - revokeSessions: vi.fn(), -})); - -vi.mock('@/lib/queryClient', () => ({ - queryClient: { clear: vi.fn() }, -})); - -vi.mock('@/config/api', () => ({ - API_BASE: 'http://localhost:8787', - BASEPATH: '', -})); - -vi.mock('@/lib/lastLoginMethod', () => ({ - saveLastLoginMethod: vi.fn(), - LOGIN_METHODS: { - EMAIL: 'email', - GOOGLE: 'google', - ORCID: 'orcid', - MAGIC_LINK: 'magic-link', - }, -})); - -vi.mock('@/primitives/avatarCache.js', () => ({ - getCachedAvatar: vi.fn().mockResolvedValue(null), - pruneExpiredAvatars: vi.fn(), -})); - -vi.mock('@/primitives/db.js', () => ({ - clearAllData: vi.fn().mockResolvedValue(undefined), -})); - -// Mock BroadcastChannel -global.BroadcastChannel = vi.fn(function () { - this.postMessage = vi.fn(); - this.addEventListener = vi.fn(); - this.removeEventListener = vi.fn(); - this.close = vi.fn(); -}); - -// Mock localStorage -const localStorageMock = { - store: {}, - getItem(key) { - return this.store[key] || null; - }, - setItem(key, value) { - this.store[key] = value.toString(); - }, - removeItem(key) { - delete this.store[key]; - }, - clear() { - this.store = {}; - }, -}; -global.localStorage = localStorageMock; - -describe('authStore - Signup Flow', () => { - let authStore; - let authClient; - - beforeEach(async () => { - vi.clearAllMocks(); - localStorage.clear(); - - authClient = (await import('@/api/auth-client')).authClient; - const { useAuthStore } = await import('@/stores/authStore'); - authStore = useAuthStore; - - // Reset store state - authStore.setState({ authError: null }); - }); - - it('should signup successfully with email and password', async () => { - authClient.signUp.email.mockResolvedValue({ - data: { user: { id: 'user-1', email: 'test@example.com' } }, - error: null, - }); - - const result = await authStore - .getState() - .signup('test@example.com', 'password123', 'Test User'); - - expect(authClient.signUp.email).toHaveBeenCalledWith({ - email: 'test@example.com', - password: 'password123', - name: 'Test User', - }); - - expect(localStorage.getItem('pendingEmail')).toBe('test@example.com'); - expect(result).toEqual({ user: { id: 'user-1', email: 'test@example.com' } }); - }); - - it('should signup with role when provided', async () => { - authClient.signUp.email.mockResolvedValue({ - data: { user: { id: 'user-1', email: 'test@example.com', role: 'researcher' } }, - error: null, - }); - - await authStore.getState().signup('test@example.com', 'password123', 'Test User', 'researcher'); - - expect(authClient.signUp.email).toHaveBeenCalledWith({ - email: 'test@example.com', - password: 'password123', - name: 'Test User', - role: 'researcher', - }); - }); - - it('should handle signup errors', async () => { - authClient.signUp.email.mockResolvedValue({ - data: null, - error: { message: 'Email already exists' }, - }); - - await expect( - authStore.getState().signup('test@example.com', 'password123', 'Test User'), - ).rejects.toThrow('Email already exists'); - - expect(authStore.getState().authError).toBe('Email already exists'); - }); -}); - -describe('authStore - Signin Flow', () => { - let authStore; - let authClient; - let saveLastLoginMethod; - - beforeEach(async () => { - vi.clearAllMocks(); - localStorage.clear(); - - authClient = (await import('@/api/auth-client')).authClient; - saveLastLoginMethod = (await import('@/lib/lastLoginMethod')).saveLastLoginMethod; - - const { useAuthStore } = await import('@/stores/authStore'); - authStore = useAuthStore; - authStore.setState({ authError: null }); - }); - - it('should signin successfully with email and password', async () => { - authClient.signIn.email.mockResolvedValue({ - data: { user: { id: 'user-1', email: 'test@example.com' } }, - error: null, - }); - - localStorage.setItem('pendingEmail', 'test@example.com'); - - const result = await authStore.getState().signin('test@example.com', 'password123'); - - expect(authClient.signIn.email).toHaveBeenCalledWith({ - email: 'test@example.com', - password: 'password123', - }); - - expect(saveLastLoginMethod).toHaveBeenCalledWith('email'); - expect(localStorage.getItem('pendingEmail')).toBeNull(); - expect(result).toBeDefined(); - }); - - it('should return 2FA required flag when 2FA is needed', async () => { - authClient.signIn.email.mockResolvedValue({ - data: { twoFactorRedirect: true }, - error: null, - }); - - const result = await authStore.getState().signin('test@example.com', 'password123'); - - expect(result.twoFactorRequired).toBe(true); - }); - - it('should handle signin errors', async () => { - authClient.signIn.email.mockResolvedValue({ - data: null, - error: { message: 'Invalid credentials' }, - }); - - await expect(authStore.getState().signin('test@example.com', 'wrong-password')).rejects.toThrow( - 'Invalid credentials', - ); - - expect(authStore.getState().authError).toBe('Invalid credentials'); - }); -}); - -describe('authStore - Social Auth', () => { - let authStore; - let authClient; - let saveLastLoginMethod; - let LOGIN_METHODS; - - beforeEach(async () => { - vi.clearAllMocks(); - - authClient = (await import('@/api/auth-client')).authClient; - const lastLoginModule = await import('@/lib/lastLoginMethod'); - saveLastLoginMethod = lastLoginModule.saveLastLoginMethod; - LOGIN_METHODS = lastLoginModule.LOGIN_METHODS; - - delete global.window.location; - global.window.location = { origin: 'http://localhost:5173' }; - - const { useAuthStore } = await import('@/stores/authStore'); - authStore = useAuthStore; - authStore.setState({ authError: null }); - }); - - it('should signin with Google', async () => { - authClient.signIn.social.mockResolvedValue({ - data: { redirectUrl: 'https://accounts.google.com/...' }, - error: null, - }); - - await authStore.getState().signinWithGoogle('/dashboard'); - - expect(saveLastLoginMethod).toHaveBeenCalledWith(LOGIN_METHODS.GOOGLE); - expect(authClient.signIn.social).toHaveBeenCalledWith({ - provider: 'google', - callbackURL: 'http://localhost:5173/dashboard', - errorCallbackURL: 'http://localhost:5173/signin', - }); - }); - - it('should signin with ORCID', async () => { - authClient.signIn.oauth2.mockResolvedValue({ - data: { redirectUrl: 'https://orcid.org/...' }, - error: null, - }); - - await authStore.getState().signinWithOrcid('/projects'); - - expect(saveLastLoginMethod).toHaveBeenCalledWith(LOGIN_METHODS.ORCID); - expect(authClient.signIn.oauth2).toHaveBeenCalledWith({ - providerId: 'orcid', - callbackURL: 'http://localhost:5173/projects', - errorCallbackURL: 'http://localhost:5173/signin', - }); - }); - - it('should send magic link', async () => { - authClient.signIn.magicLink.mockResolvedValue({ - data: { success: true }, - error: null, - }); - - await authStore.getState().signinWithMagicLink('test@example.com', '/verify'); - - expect(authClient.signIn.magicLink).toHaveBeenCalledWith({ - email: 'test@example.com', - callbackURL: 'http://localhost:5173/verify', - }); - - expect(localStorage.getItem('pendingEmail')).toBe('test@example.com'); - expect(saveLastLoginMethod).toHaveBeenCalledWith(LOGIN_METHODS.MAGIC_LINK); - }); -}); - -describe('authStore - Signout', () => { - let authStore; - let authClient; - - beforeEach(async () => { - vi.clearAllMocks(); - - authClient = (await import('@/api/auth-client')).authClient; - - const { useAuthStore } = await import('@/stores/authStore'); - authStore = useAuthStore; - authStore.setState({ authError: null, sessionRefetch: null }); - }); - - it('should signout successfully', async () => { - authClient.signOut.mockResolvedValue({ - error: null, - }); - - await authStore.getState().signout(); - - expect(authClient.signOut).toHaveBeenCalled(); - }); - - it('should handle signout errors', async () => { - authClient.signOut.mockResolvedValue({ - error: { message: 'Signout failed' }, - }); - - await expect(authStore.getState().signout()).rejects.toThrow('Signout failed'); - expect(authStore.getState().authError).toBe('Signout failed'); - }); -}); - -describe('authStore - Password Management', () => { - let authStore; - let authClient; - - beforeEach(async () => { - vi.clearAllMocks(); - - authClient = (await import('@/api/auth-client')).authClient; - - global.window.location = { origin: 'http://localhost:5173' }; - - const { useAuthStore } = await import('@/stores/authStore'); - authStore = useAuthStore; - authStore.setState({ authError: null }); - }); - - it('should change password', async () => { - authClient.changePassword.mockResolvedValue({ - error: null, - }); - - await authStore.getState().changePassword('oldPassword123', 'newPassword456'); - - expect(authClient.changePassword).toHaveBeenCalledWith({ - currentPassword: 'oldPassword123', - newPassword: 'newPassword456', - }); - - expect(authStore.getState().authError).toBeNull(); - }); - - it('should request password reset', async () => { - authClient.requestPasswordReset.mockResolvedValue({ - error: null, - }); - - await authStore.getState().resetPassword('test@example.com'); - - expect(authClient.requestPasswordReset).toHaveBeenCalledWith({ - email: 'test@example.com', - redirectTo: 'http://localhost:5173/reset-password', - }); - }); - - it('should confirm password reset with token', async () => { - authClient.resetPassword.mockResolvedValue({ - error: null, - }); - - await authStore.getState().confirmPasswordReset('reset-token-123', 'newPassword789'); - - expect(authClient.resetPassword).toHaveBeenCalledWith({ - token: 'reset-token-123', - newPassword: 'newPassword789', - }); - }); - - it('should handle password change errors', async () => { - authClient.changePassword.mockResolvedValue({ - error: { message: 'Current password is incorrect' }, - }); - - await expect(authStore.getState().changePassword('wrong', 'new')).rejects.toThrow( - 'Current password is incorrect', - ); - - expect(authStore.getState().authError).toBe('Current password is incorrect'); - }); -}); - -describe('authStore - Two-Factor Authentication', () => { - let authStore; - let authClient; - - beforeEach(async () => { - vi.clearAllMocks(); - - authClient = (await import('@/api/auth-client')).authClient; - - const { useAuthStore } = await import('@/stores/authStore'); - authStore = useAuthStore; - authStore.setState({ authError: null }); - }); - - it('should enable 2FA with password', async () => { - authClient.twoFactor.enable.mockResolvedValue({ - data: { - totpURI: 'otpauth://totp/...', - secret: 'ABC123', - backupCodes: ['code1', 'code2'], - }, - error: null, - }); - - const result = await authStore.getState().enableTwoFactor('myPassword123'); - - expect(authClient.twoFactor.enable).toHaveBeenCalledWith({ - password: 'myPassword123', - }); - - expect(result.totpURI).toBeDefined(); - expect(result.secret).toBe('ABC123'); - expect(result.backupCodes).toEqual(['code1', 'code2']); - }); - - it('should verify 2FA setup with code', async () => { - authClient.twoFactor.verifyTotp.mockResolvedValue({ - data: { success: true }, - error: null, - }); - - const result = await authStore.getState().verifyTwoFactorSetup('123456'); - - expect(authClient.twoFactor.verifyTotp).toHaveBeenCalledWith({ - code: '123456', - }); - - expect(result).toEqual({ success: true }); - }); - - it('should disable 2FA with password', async () => { - authClient.twoFactor.disable.mockResolvedValue({ - data: { success: true }, - error: null, - }); - - const result = await authStore.getState().disableTwoFactor('myPassword123'); - - expect(authClient.twoFactor.disable).toHaveBeenCalledWith({ - password: 'myPassword123', - }); - - expect(result).toEqual({ success: true }); - }); - - it('should verify 2FA code during signin', async () => { - authClient.twoFactor.verifyTotp.mockResolvedValue({ - data: { user: { id: 'user-1', email: 'test@example.com' } }, - error: null, - }); - - const result = await authStore.getState().verifyTwoFactor('654321'); - - expect(authClient.twoFactor.verifyTotp).toHaveBeenCalledWith({ - code: '654321', - }); - - expect(result.user).toBeDefined(); - }); - - it('should handle 2FA errors', async () => { - authClient.twoFactor.enable.mockResolvedValue({ - data: null, - error: { message: 'Invalid password' }, - }); - - await expect(authStore.getState().enableTwoFactor('wrongPassword')).rejects.toThrow( - 'Invalid password', - ); - expect(authStore.getState().authError).toBe('Invalid password'); - }); -}); - -describe('authStore - Profile Management', () => { - let authStore; - let authClient; - - beforeEach(async () => { - vi.clearAllMocks(); - - authClient = (await import('@/api/auth-client')).authClient; - - const { useAuthStore } = await import('@/stores/authStore'); - authStore = useAuthStore; - authStore.setState({ authError: null, sessionRefetch: null }); - }); - - it('should update user profile', async () => { - authClient.updateUser.mockResolvedValue({ - data: { user: { id: 'user-1', name: 'New Name' } }, - error: null, - }); - - const result = await authStore.getState().updateProfile({ name: 'New Name' }); - - expect(authClient.updateUser).toHaveBeenCalledWith({ name: 'New Name' }); - expect(result.user.name).toBe('New Name'); - }); - - it('should handle profile update errors', async () => { - authClient.updateUser.mockResolvedValue({ - data: null, - error: { message: 'Update failed' }, - }); - - await expect(authStore.getState().updateProfile({ name: 'Test' })).rejects.toThrow( - 'Update failed', - ); - expect(authStore.getState().authError).toBe('Update failed'); - }); -}); - -describe('authStore - Account Deletion', () => { - let authStore; - let authClient; - - beforeEach(async () => { - vi.clearAllMocks(); - localStorage.clear(); - - authClient = (await import('@/api/auth-client')).authClient; - authClient.signOut.mockResolvedValue({ error: null }); - - global.fetch = vi.fn(); - - const { useAuthStore } = await import('@/stores/authStore'); - authStore = useAuthStore; - authStore.setState({ authError: null, sessionRefetch: null }); - }); - - it('should delete account successfully', async () => { - global.fetch.mockResolvedValue({ - ok: true, - json: async () => ({ success: true }), - }); - - localStorage.setItem('pendingEmail', 'test@example.com'); - - const result = await authStore.getState().deleteAccount(); - - expect(global.fetch).toHaveBeenCalledWith( - expect.stringContaining('/api/users/me'), - expect.objectContaining({ - method: 'DELETE', - credentials: 'include', - }), - ); - - expect(localStorage.getItem('pendingEmail')).toBeNull(); - expect(authClient.signOut).toHaveBeenCalled(); - expect(result.success).toBe(true); - }); - - it('should handle account deletion errors', async () => { - global.fetch.mockResolvedValue({ - ok: false, - json: async () => ({ error: 'Deletion failed' }), - }); - - await expect(authStore.getState().deleteAccount()).rejects.toThrow('Deletion failed'); - expect(authStore.getState().authError).toBe('Deletion failed'); - }); -}); - -describe('authStore - Utility Functions', () => { - let authStore; - - beforeEach(async () => { - vi.clearAllMocks(); - localStorage.clear(); - - const { useAuthStore } = await import('@/stores/authStore'); - authStore = useAuthStore; - authStore.setState({ authError: null }); - }); - - it('should clear auth error', async () => { - // Trigger an error first - const authClient = (await import('@/api/auth-client')).authClient; - authClient.signIn.email.mockResolvedValue({ - data: null, - error: { message: 'Test error' }, - }); - - await authStore - .getState() - .signin('test@example.com', 'wrong') - .catch(() => {}); - - expect(authStore.getState().authError).toBe('Test error'); - - authStore.getState().setAuthError(null); - - expect(authStore.getState().authError).toBeNull(); - }); -}); diff --git a/packages/landing/src/components/checklist/AMSTAR2Checklist/__tests__/checklist-compare.test.js b/packages/landing/src/components/checklist/AMSTAR2Checklist/__tests__/checklist-compare.test.js deleted file mode 100644 index bdbe20ee..00000000 --- a/packages/landing/src/components/checklist/AMSTAR2Checklist/__tests__/checklist-compare.test.js +++ /dev/null @@ -1,276 +0,0 @@ -/** - * Tests for AMSTAR2 Checklist Comparison Module - * - * INTENDED BEHAVIOR: - * - compareChecklists: Compares two checklists and identifies agreements/disagreements - * - compareQuestion: Compares a single question between two checklists - * - getFinalAnswer: Extracts the final answer from a question's last column - * - answersMatch: Checks if two answer arrays are identical - * - createReconciledChecklist: Creates a merged checklist from two sources - * - getReconciliationSummary: Summarizes what needs reconciliation - */ - -import { describe, it, expect } from 'vitest'; -import { - compareChecklists, - compareQuestion, - getFinalAnswer, - answersMatch, - createReconciledChecklist, - getReconciliationSummary, - getQuestionKeys, -} from '../checklist-compare.js'; -import { createChecklist } from '../checklist.js'; - -describe('getQuestionKeys', () => { - it('should return all question keys from the checklist map', () => { - const keys = getQuestionKeys(); - expect(keys).toContain('q1'); - expect(keys).toContain('q16'); - expect(keys.length).toBeGreaterThan(10); - }); -}); - -describe('getFinalAnswer', () => { - it('should return Yes for first selected option in 2-option column', () => { - const answers = [[false], [true, false]]; - expect(getFinalAnswer(answers, 'q1')).toBe('Yes'); - }); - - it('should return No for second selected option in 2-option column', () => { - const answers = [[false], [false, true]]; - expect(getFinalAnswer(answers, 'q1')).toBe('No'); - }); - - it('should return Partial Yes for second option in 3-option column', () => { - const answers = [[false], [false, true, false]]; - expect(getFinalAnswer(answers, 'q2')).toBe('Partial Yes'); - }); - - it('should return null if no answer is selected', () => { - const answers = [[false], [false, false]]; - expect(getFinalAnswer(answers, 'q1')).toBe(null); - }); - - it('should return null for invalid input', () => { - expect(getFinalAnswer(null, 'q1')).toBe(null); - expect(getFinalAnswer([], 'q1')).toBe(null); - }); -}); - -describe('answersMatch', () => { - it('should return true for identical answers', () => { - const a1 = [ - [true, false], - [false, true], - ]; - const a2 = [ - [true, false], - [false, true], - ]; - expect(answersMatch(a1, a2)).toBe(true); - }); - - it('should return false for different answers', () => { - const a1 = [ - [true, false], - [false, true], - ]; - const a2 = [ - [false, true], - [false, true], - ]; - expect(answersMatch(a1, a2)).toBe(false); - }); - - it('should return false for different lengths', () => { - const a1 = [ - [true, false], - [false, true], - ]; - const a2 = [[true, false]]; - expect(answersMatch(a1, a2)).toBe(false); - }); - - it('should return false for null inputs', () => { - expect(answersMatch(null, [[true]])).toBe(false); - expect(answersMatch([[true]], null)).toBe(false); - }); -}); - -describe('compareQuestion', () => { - it('should identify agreement when final answers match', () => { - const q1 = { - answers: [ - [true, true], - [true, false], - ], - critical: false, - }; - const q2 = { - answers: [ - [true, true], - [true, false], - ], - critical: false, - }; - - const result = compareQuestion('q1', q1, q2); - - expect(result.isAgreement).toBe(true); - expect(result.finalMatch).toBe(true); - expect(result.criticalMatch).toBe(true); - expect(result.reviewer1.finalAnswer).toBe('Yes'); - expect(result.reviewer2.finalAnswer).toBe('Yes'); - }); - - it('should identify disagreement when final answers differ', () => { - const q1 = { - answers: [ - [true, true], - [true, false], - ], - critical: false, - }; - const q2 = { - answers: [ - [false, false], - [false, true], - ], - critical: false, - }; - - const result = compareQuestion('q1', q1, q2); - - expect(result.isAgreement).toBe(false); - expect(result.finalMatch).toBe(false); - expect(result.reviewer1.finalAnswer).toBe('Yes'); - expect(result.reviewer2.finalAnswer).toBe('No'); - }); - - it('should identify disagreement when critical status differs', () => { - const q1 = { - answers: [ - [true, true], - [true, false], - ], - critical: true, - }; - const q2 = { - answers: [ - [true, true], - [true, false], - ], - critical: false, - }; - - const result = compareQuestion('q1', q1, q2); - - expect(result.isAgreement).toBe(false); - expect(result.finalMatch).toBe(true); - expect(result.criticalMatch).toBe(false); - }); -}); - -describe('compareChecklists', () => { - it('should return empty results for null inputs', () => { - const result = compareChecklists(null, null); - expect(result.agreements).toHaveLength(0); - expect(result.disagreements).toHaveLength(0); - }); - - it('should identify agreements and disagreements', () => { - const cl1 = createChecklist({ id: 'test-1', name: 'Test 1' }); - const cl2 = createChecklist({ id: 'test-2', name: 'Test 2' }); - - // Make cl1.q1 have Yes as the final answer - cl1.q1.answers[2] = [true, false]; // Yes selected - cl2.q1.answers[2] = [true, false]; // Yes selected (same) - - // Make q2 different - cl1.q2.answers[2] = [true, false, false]; // Yes - cl2.q2.answers[2] = [false, false, true]; // No - - const result = compareChecklists(cl1, cl2); - - expect(result.agreements.length).toBeGreaterThan(0); - expect(result.disagreements.length).toBeGreaterThan(0); - expect(result.stats.total).toBe(result.stats.agreed + result.stats.disagreed); - }); -}); - -describe('getReconciliationSummary', () => { - it('should provide counts of agreements and disagreements', () => { - const comparison = { - agreements: [{ key: 'q1' }, { key: 'q3' }], - disagreements: [{ key: 'q2', reviewer1: { critical: true }, reviewer2: { critical: false } }], - stats: { total: 3, agreed: 2, disagreed: 1, agreementRate: 0.67 }, - }; - - const summary = getReconciliationSummary(comparison); - - expect(summary.totalQuestions).toBe(3); - expect(summary.agreementCount).toBe(2); - expect(summary.disagreementCount).toBe(1); - expect(summary.needsReconciliation).toBe(true); - expect(summary.criticalDisagreements).toBe(1); - }); - - it('should report no reconciliation needed when no disagreements', () => { - const comparison = { - agreements: [{ key: 'q1' }], - disagreements: [], - stats: { total: 1, agreed: 1, disagreed: 0, agreementRate: 1 }, - }; - - const summary = getReconciliationSummary(comparison); - - expect(summary.needsReconciliation).toBe(false); - }); -}); - -describe('createReconciledChecklist', () => { - it('should create a checklist using selections from both reviewers', () => { - const cl1 = createChecklist({ id: 'test-1', name: 'Test 1' }); - const cl2 = createChecklist({ id: 'test-2', name: 'Test 2' }); - - // Set different answers for q1 - cl1.q1.answers[2] = [true, false]; // Yes - cl2.q1.answers[2] = [false, true]; // No - - const selections = { - q1: 'reviewer2', // Use reviewer 2's answer for q1 - q2: 'reviewer1', // Use reviewer 1's answer for q2 - }; - - const reconciled = createReconciledChecklist(cl1, cl2, selections, { - name: 'Reconciled', - id: 'reconciled-1', - }); - - expect(reconciled.name).toBe('Reconciled'); - expect(reconciled.id).toBe('reconciled-1'); - expect(reconciled.q1.answers[2]).toEqual([false, true]); // From reviewer 2 - expect(reconciled.q2.answers).toEqual(cl1.q2.answers); // From reviewer 1 - }); - - it('should default to reviewer1 when no selection is made', () => { - const cl1 = createChecklist({ id: 'test-1', name: 'Test 1' }); - const cl2 = createChecklist({ id: 'test-2', name: 'Test 2' }); - - const reconciled = createReconciledChecklist(cl1, cl2, {}); - - // All questions should come from reviewer 1 - expect(reconciled.q1).toEqual(cl1.q1); - }); - - it('should include source checklist IDs', () => { - const cl1 = createChecklist({ id: 'source-1', name: 'Test 1' }); - const cl2 = createChecklist({ id: 'source-2', name: 'Test 2' }); - - const reconciled = createReconciledChecklist(cl1, cl2, {}); - - expect(reconciled.sourceChecklists).toContain('source-1'); - expect(reconciled.sourceChecklists).toContain('source-2'); - }); -}); diff --git a/packages/landing/src/components/checklist/AMSTAR2Checklist/__tests__/checklist.test.js b/packages/landing/src/components/checklist/AMSTAR2Checklist/__tests__/checklist.test.js deleted file mode 100644 index 7cc16554..00000000 --- a/packages/landing/src/components/checklist/AMSTAR2Checklist/__tests__/checklist.test.js +++ /dev/null @@ -1,586 +0,0 @@ -/** - * Tests for AMSTAR2 Checklist Module - * - * INTENDED BEHAVIOR: - * - createChecklist: Creates a valid AMSTAR2 checklist with all 16 questions initialized - * - scoreChecklist: Scores a checklist according to AMSTAR2 methodology - * - "High" = No critical or non-critical weaknesses - * - "Moderate" = More than 1 non-critical weakness, no critical flaws - * - "Low" = 1 critical flaw with/without non-critical weaknesses - * - "Critically Low" = More than 1 critical flaw - * - getAnswers: Extracts selected answers from a checklist, consolidating q9a/b and q11a/b - * - * Domain Reference: https://amstar.ca/Amstar-2.php - */ - -import { describe, it, expect, beforeEach } from 'vitest'; -import { createChecklist, scoreChecklist, getAnswers } from '../checklist.js'; - -describe('createChecklist', () => { - describe('validation', () => { - it('should throw error when id is missing', () => { - expect(() => createChecklist({ name: 'Test' })).toThrow( - 'AMSTAR2Checklist requires a non-empty string id.', - ); - }); - - it('should throw error when id is empty string', () => { - expect(() => createChecklist({ id: '', name: 'Test' })).toThrow( - 'AMSTAR2Checklist requires a non-empty string id.', - ); - }); - - it('should throw error when id is whitespace only', () => { - expect(() => createChecklist({ id: ' ', name: 'Test' })).toThrow( - 'AMSTAR2Checklist requires a non-empty string id.', - ); - }); - - it('should throw error when name is missing', () => { - expect(() => createChecklist({ id: 'test-id' })).toThrow( - 'AMSTAR2Checklist requires a non-empty string name.', - ); - }); - - it('should throw error when name is empty string', () => { - expect(() => createChecklist({ id: 'test-id', name: '' })).toThrow( - 'AMSTAR2Checklist requires a non-empty string name.', - ); - }); - - it('should throw error when name is whitespace only', () => { - expect(() => createChecklist({ id: 'test-id', name: ' ' })).toThrow( - 'AMSTAR2Checklist requires a non-empty string name.', - ); - }); - - it('should throw error when id is not a string', () => { - expect(() => createChecklist({ id: 123, name: 'Test' })).toThrow(); - }); - - it('should throw error when name is not a string', () => { - expect(() => createChecklist({ id: 'test-id', name: 123 })).toThrow(); - }); - }); - - describe('checklist structure', () => { - let checklist; - - beforeEach(() => { - checklist = createChecklist({ - id: 'test-checklist-1', - name: 'Sleep Study Review', - reviewerName: 'Dr. Smith', - }); - }); - - it('should include the provided id', () => { - expect(checklist.id).toBe('test-checklist-1'); - }); - - it('should include the provided name', () => { - expect(checklist.name).toBe('Sleep Study Review'); - }); - - it('should include the reviewer name', () => { - expect(checklist.reviewerName).toBe('Dr. Smith'); - }); - - it('should format createdAt as YYYY-MM-DD string', () => { - expect(checklist.createdAt).toMatch(/^\d{4}-\d{2}-\d{2}$/); - }); - - it('should handle custom createdAt timestamp', () => { - const customChecklist = createChecklist({ - id: 'test-2', - name: 'Test', - createdAt: new Date('2025-06-15T10:00:00Z').getTime(), - }); - expect(customChecklist.createdAt).toBe('2025-06-15'); - }); - - it('should default reviewerName to empty string when not provided', () => { - const cl = createChecklist({ id: 'test-3', name: 'Test' }); - expect(cl.reviewerName).toBe(''); - }); - - // AMSTAR2 has 16 questions: q1-q16, with q9 and q11 split into a/b parts - const expectedQuestions = [ - 'q1', - 'q2', - 'q3', - 'q4', - 'q5', - 'q6', - 'q7', - 'q8', - 'q9a', - 'q9b', - 'q10', - 'q11a', - 'q11b', - 'q12', - 'q13', - 'q14', - 'q15', - 'q16', - ]; - - expectedQuestions.forEach(q => { - it(`should include question ${q}`, () => { - expect(checklist[q]).toBeDefined(); - expect(checklist[q]).toHaveProperty('answers'); - expect(checklist[q]).toHaveProperty('critical'); - expect(Array.isArray(checklist[q].answers)).toBe(true); - }); - }); - - // Critical questions per AMSTAR2: 2, 4, 7, 9, 11, 13, 15 - const criticalQuestions = ['q2', 'q4', 'q7', 'q9a', 'q9b', 'q11a', 'q11b', 'q13', 'q15']; - const nonCriticalQuestions = ['q1', 'q3', 'q5', 'q6', 'q8', 'q10', 'q12', 'q14', 'q16']; - - criticalQuestions.forEach(q => { - it(`should mark ${q} as critical`, () => { - expect(checklist[q].critical).toBe(true); - }); - }); - - nonCriticalQuestions.forEach(q => { - it(`should mark ${q} as non-critical`, () => { - expect(checklist[q].critical).toBe(false); - }); - }); - - it('should initialize all answers to false (no selection)', () => { - // Check that answers are arrays of arrays with all false values - // All answers should be initialized to false (no default selection) - Object.keys(checklist).forEach(key => { - if (!/^q\d+[a-z]*$/i.test(key)) return; - const question = checklist[key]; - question.answers.forEach((col, _colIdx) => { - // All columns should have all false values by default - const allFalse = col.every(v => v === false); - expect(allFalse).toBe(true); - }); - }); - }); - }); -}); - -describe('scoreChecklist', () => { - // Helper to create a checklist with specific answers selected - function createScoredChecklist(selections) { - const checklist = createChecklist({ id: 'test', name: 'Test' }); - - // Apply selections - format: { q1: 'Yes', q2: 'No', ... } - Object.entries(selections).forEach(([question, answer]) => { - if (!checklist[question]) return; - - const answers = checklist[question].answers; - const lastCol = answers[answers.length - 1]; - - // Reset last column to all false - lastCol.fill(false); - - // Set the appropriate answer - // Most questions have [Yes, Partial Yes, No, No MA] or [Yes, No] - const answerMap = { - Yes: 0, - 'Partial Yes': 1, - No: lastCol.length === 2 ? 1 : 2, - 'No MA': - lastCol.length === 2 ? 1 - : lastCol.length === 3 ? 2 - : 3, - }; - - if (answerMap[answer] !== undefined && answerMap[answer] < lastCol.length) { - lastCol[answerMap[answer]] = true; - } - }); - - return checklist; - } - - it('should return "Error" for null input', () => { - expect(scoreChecklist(null)).toBe('Error'); - }); - - it('should return "Error" for undefined input', () => { - expect(scoreChecklist(undefined)).toBe('Error'); - }); - - it('should return "Error" for non-object input', () => { - expect(scoreChecklist('not an object')).toBe('Error'); - }); - - it('should return "High" when all questions are Yes or Partial Yes', () => { - const checklist = createScoredChecklist({ - q1: 'Yes', - q2: 'Yes', - q3: 'Yes', - q4: 'Yes', - q5: 'Yes', - q6: 'Yes', - q7: 'Yes', - q8: 'Yes', - q9a: 'Yes', - q9b: 'Yes', - q10: 'Yes', - q11a: 'Yes', - q11b: 'Yes', - q12: 'Yes', - q13: 'Yes', - q14: 'Yes', - q15: 'Yes', - q16: 'Yes', - }); - - expect(scoreChecklist(checklist)).toBe('High'); - }); - - it('should return "High" when all questions are Partial Yes', () => { - // Partial Yes should be treated same as Yes per AMSTAR2 methodology - const checklist = createScoredChecklist({ - q1: 'Partial Yes', - q2: 'Partial Yes', - q3: 'Yes', - q4: 'Partial Yes', - q5: 'Yes', - q6: 'Yes', - q7: 'Yes', - q8: 'Partial Yes', - q9a: 'Yes', - q9b: 'Yes', - q10: 'Yes', - q11a: 'Yes', - q11b: 'Yes', - q12: 'Yes', - q13: 'Yes', - q14: 'Yes', - q15: 'Yes', - q16: 'Yes', - }); - - expect(scoreChecklist(checklist)).toBe('High'); - }); - - it('should return "Moderate" when there is 1 non-critical weakness (No answer)', () => { - const checklist = createScoredChecklist({ - q1: 'No', // non-critical - q2: 'Yes', - q3: 'Yes', - q4: 'Yes', - q5: 'Yes', - q6: 'Yes', - q7: 'Yes', - q8: 'Yes', - q9a: 'Yes', - q9b: 'Yes', - q10: 'Yes', - q11a: 'Yes', - q11b: 'Yes', - q12: 'Yes', - q13: 'Yes', - q14: 'Yes', - q15: 'Yes', - q16: 'Yes', - }); - - // One non-critical weakness should still be "High" per AMSTAR2 - // Only >1 non-critical weakness = Moderate - expect(scoreChecklist(checklist)).toBe('High'); - }); - - it('should return "Moderate" when there are >1 non-critical weaknesses', () => { - const checklist = createScoredChecklist({ - q1: 'No', // non-critical - q3: 'No', // non-critical - q2: 'Yes', - q4: 'Yes', - q5: 'Yes', - q6: 'Yes', - q7: 'Yes', - q8: 'Yes', - q9a: 'Yes', - q9b: 'Yes', - q10: 'Yes', - q11a: 'Yes', - q11b: 'Yes', - q12: 'Yes', - q13: 'Yes', - q14: 'Yes', - q15: 'Yes', - q16: 'Yes', - }); - - expect(scoreChecklist(checklist)).toBe('Moderate'); - }); - - it('should return "Low" when there is exactly 1 critical flaw', () => { - const checklist = createScoredChecklist({ - q2: 'No', // critical - q1: 'Yes', - q3: 'Yes', - q4: 'Yes', - q5: 'Yes', - q6: 'Yes', - q7: 'Yes', - q8: 'Yes', - q9a: 'Yes', - q9b: 'Yes', - q10: 'Yes', - q11a: 'Yes', - q11b: 'Yes', - q12: 'Yes', - q13: 'Yes', - q14: 'Yes', - q15: 'Yes', - q16: 'Yes', - }); - - expect(scoreChecklist(checklist)).toBe('Low'); - }); - - it('should return "Critically Low" when there are >1 critical flaws', () => { - const checklist = createScoredChecklist({ - q2: 'No', // critical - q4: 'No', // critical - q1: 'Yes', - q3: 'Yes', - q5: 'Yes', - q6: 'Yes', - q7: 'Yes', - q8: 'Yes', - q9a: 'Yes', - q9b: 'Yes', - q10: 'Yes', - q11a: 'Yes', - q11b: 'Yes', - q12: 'Yes', - q13: 'Yes', - q14: 'Yes', - q15: 'Yes', - q16: 'Yes', - }); - - expect(scoreChecklist(checklist)).toBe('Critically Low'); - }); - - it('should not count "No MA" (Not Applicable) as a flaw', () => { - // "No MA" means meta-analysis was not conducted, which is not a weakness - const checklist = createScoredChecklist({ - q1: 'No MA', - q2: 'Yes', - q3: 'Yes', - q4: 'Yes', - q5: 'Yes', - q6: 'Yes', - q7: 'Yes', - q8: 'Yes', - q9a: 'Yes', - q9b: 'Yes', - q10: 'Yes', - q11a: 'Yes', - q11b: 'Yes', - q12: 'Yes', - q13: 'Yes', - q14: 'Yes', - q15: 'Yes', - q16: 'Yes', - }); - - expect(scoreChecklist(checklist)).toBe('High'); - }); - - it('should not count "No MA" on q11a/q11b as a critical flaw after consolidation', () => { - // q11 is critical; "No MA" on both should not count as a flaw. - // This tests the consolidation path: q11a/q11b merge into q11, - // which must use the 3-option label mapping (Yes/No/No MA) - // rather than the 4-option default (Yes/Partial Yes/No/No MA). - const checklist = createChecklist({ id: 'test', name: 'Test' }); - - // q11a and q11b have 3-option last columns: [Yes, No, No MA] - // Set both to "No MA" (index 2) - checklist.q11a.answers[1] = [false, false, true]; - checklist.q11b.answers[1] = [false, false, true]; - - // Set all other questions to Yes - const otherQuestions = [ - 'q1', - 'q2', - 'q3', - 'q4', - 'q5', - 'q6', - 'q7', - 'q8', - 'q9a', - 'q9b', - 'q10', - 'q12', - 'q13', - 'q14', - 'q15', - 'q16', - ]; - for (const q of otherQuestions) { - const lastCol = checklist[q].answers[checklist[q].answers.length - 1]; - lastCol.fill(false); - lastCol[0] = true; // Yes - } - - expect(scoreChecklist(checklist)).toBe('High'); - }); - - it('should count "No" on q11a as a critical flaw after consolidation', () => { - const checklist = createChecklist({ id: 'test', name: 'Test' }); - - // q11a: "No" (index 1), q11b: "Yes" (index 0) - checklist.q11a.answers[1] = [false, true, false]; - checklist.q11b.answers[1] = [true, false, false]; - - // Set all other questions to Yes - const otherQuestions = [ - 'q1', - 'q2', - 'q3', - 'q4', - 'q5', - 'q6', - 'q7', - 'q8', - 'q9a', - 'q9b', - 'q10', - 'q12', - 'q13', - 'q14', - 'q15', - 'q16', - ]; - for (const q of otherQuestions) { - const lastCol = checklist[q].answers[checklist[q].answers.length - 1]; - lastCol.fill(false); - lastCol[0] = true; // Yes - } - - // q11 is critical, "No" on q11a should be 1 critical flaw = Low - expect(scoreChecklist(checklist)).toBe('Low'); - }); -}); - -describe('getAnswers', () => { - it('should return null for null input', () => { - expect(getAnswers(null)).toBe(null); - }); - - it('should return null for non-object input', () => { - expect(getAnswers('string')).toBe(null); - }); - - it('should extract selected answers from all questions', () => { - const checklist = createChecklist({ id: 'test', name: 'Test' }); - const answers = getAnswers(checklist); - - expect(answers).toBeDefined(); - expect(typeof answers).toBe('object'); - }); - - it('should consolidate q9a and q9b into q9', () => { - const checklist = createChecklist({ id: 'test', name: 'Test' }); - const answers = getAnswers(checklist); - - expect(answers.q9).toBeDefined(); - expect(answers.q9a).toBeUndefined(); - expect(answers.q9b).toBeUndefined(); - }); - - it('should consolidate q11a and q11b into q11', () => { - const checklist = createChecklist({ id: 'test', name: 'Test' }); - const answers = getAnswers(checklist); - - expect(answers.q11).toBeDefined(); - expect(answers.q11a).toBeUndefined(); - expect(answers.q11b).toBeUndefined(); - }); - - it('should return q9="No" if either q9a or q9b is "No"', () => { - // This tests the conservative consolidation logic - const checklist = createChecklist({ id: 'test', name: 'Test' }); - - // Set q9a to Yes (index 0 in last column) - checklist.q9a.answers[2] = [true, false, false, false]; - // Set q9b to No (index 2 in last column for 4-option columns) - checklist.q9b.answers[2] = [false, false, true, false]; - - const answers = getAnswers(checklist); - expect(answers.q9).toBe('No'); - }); - - it('should return q9="No MA" if both q9a and q9b are "No MA"', () => { - const checklist = createChecklist({ id: 'test', name: 'Test' }); - - // Set both to No MA (last option) - checklist.q9a.answers[2] = [false, false, false, true]; - checklist.q9b.answers[2] = [false, false, false, true]; - - const answers = getAnswers(checklist); - expect(answers.q9).toBe('No MA'); - }); - - it('should return q9="Yes" if both q9a and q9b are "Yes" or "Partial Yes"', () => { - const checklist = createChecklist({ id: 'test', name: 'Test' }); - - // Set both to Yes - checklist.q9a.answers[2] = [true, false, false, false]; - checklist.q9b.answers[2] = [true, false, false, false]; - - const answers = getAnswers(checklist); - expect(answers.q9).toBe('Yes'); - }); - - it('should return q11="No MA" when both q11a and q11b are "No MA"', () => { - const checklist = createChecklist({ id: 'test', name: 'Test' }); - - // q11a/q11b last column is 3-option: [Yes, No, No MA] - checklist.q11a.answers[1] = [false, false, true]; // No MA - checklist.q11b.answers[1] = [false, false, true]; // No MA - - const answers = getAnswers(checklist); - expect(answers.q11).toBe('No MA'); - }); - - it('should return q11="No" when q11a is "No" and q11b is "Yes"', () => { - const checklist = createChecklist({ id: 'test', name: 'Test' }); - - checklist.q11a.answers[1] = [false, true, false]; // No - checklist.q11b.answers[1] = [true, false, false]; // Yes - - const answers = getAnswers(checklist); - expect(answers.q11).toBe('No'); - }); - - it('should return q11="No" when q11a is "Yes" and q11b is "No"', () => { - const checklist = createChecklist({ id: 'test', name: 'Test' }); - - checklist.q11a.answers[1] = [true, false, false]; // Yes - checklist.q11b.answers[1] = [false, true, false]; // No - - const answers = getAnswers(checklist); - expect(answers.q11).toBe('No'); - }); - - it('should return q11="Yes" when both q11a and q11b are "Yes"', () => { - const checklist = createChecklist({ id: 'test', name: 'Test' }); - - checklist.q11a.answers[1] = [true, false, false]; // Yes - checklist.q11b.answers[1] = [true, false, false]; // Yes - - const answers = getAnswers(checklist); - expect(answers.q11).toBe('Yes'); - }); -}); - -// Note: exportChecklistsToCSV tests are skipped because the function -// has not been migrated to the landing package. diff --git a/packages/landing/src/components/checklist/ROB2Checklist/scoring/decision-diagrams/domain-1.md b/packages/landing/src/components/checklist/ROB2Checklist/scoring/decision-diagrams/domain-1.md deleted file mode 100644 index 26feefca..00000000 --- a/packages/landing/src/components/checklist/ROB2Checklist/scoring/decision-diagrams/domain-1.md +++ /dev/null @@ -1,138 +0,0 @@ -Signalling questions Elaboration Response options -1.1 Was the allocation -sequence random? - -Answer ‘Yes’ if a random component was used in the sequence generation process. Examples include -computer-generated random numbers; reference to a random number table; coin tossing; shuffling cards -or envelopes; throwing dice; or drawing lots. Minimization is generally implemented with a random -element (at least when the scores are equal), so an allocation sequence that is generated using -minimization should generally be considered to be random. -Answer ‘No’ if no random element was used in generating the allocation sequence or the sequence is -predictable. Examples include alternation; methods based on dates (of birth or admission); patient -record numbers; allocation decisions made by clinicians or participants; allocation based on the -availability of the intervention; or any other systematic or haphazard method. -Answer ‘No information’ if the only information about randomization methods is a statement that the -study is randomized. -In some situations a judgement may be made to answer ‘Probably no’ or ‘Probably yes’. For example, , in -the context of a large trial run by an experienced clinical trials unit, absence of specific information about -generation of the randomization sequence, in a paper published in a journal with rigorously enforced word -count limits, is likely to result in a response of ‘Probably yes’ rather than ‘No information’. Alternatively, if -other (contemporary) trials by the same investigator team have clearly used non-random sequences, it -might be reasonable to assume that the current study was done using similar methods. - -Y/PY/PN/N/NI - -1.2 Was the allocation -sequence concealed until -participants were -enrolled and assigned to -interventions? - -Answer ‘Yes’ if the trial used any form of remote or centrally administered method to allocate -interventions to participants, where the process of allocation is controlled by an external unit or -organization, independent of the enrolment personnel (e.g. independent central pharmacy, telephone or -internet-based randomization service providers). -Answer ‘Yes’ if envelopes or drug containers were used appropriately. Envelopes should be opaque, -sequentially numbered, sealed with a tamper-proof seal and opened only after the envelope has been -irreversibly assigned to the participant. Drug containers should be sequentially numbered and of -identical appearance, and dispensed or administered only after they have been irreversibly assigned to -the participant. This level of detail is rarely provided in reports, and a judgement may be required to -justify an answer of ‘Probably yes’ or ‘Probably no’. -Answer ‘No’ if there is reason to suspect that the enrolling investigator or the participant had knowledge -of the forthcoming allocation. - -Y/PY/PN/N/NI - -5 - -1.3 Did baseline -differences between -intervention groups -suggest a problem with -the randomization -process? - -Note that differences that are compatible with chance do not lead to a risk of bias. A small number of -differences identified as ‘statistically significant’ at the conventional 0.05 threshold should usually be -considered to be compatible with chance. -Answer ‘No’ if no imbalances are apparent or if any observed imbalances are compatible with chance. -Answer ‘Yes’ if there are imbalances that indicate problems with the randomization process, including: -(1) substantial differences between intervention group sizes, compared with the intended allocation -ratio; -or -(2) a substantial excess in statistically significant differences in baseline characteristics between -intervention groups, beyond that expected by chance; or -(3) imbalance in one or more key prognostic factors, or baseline measures of outcome variables, -that is very unlikely to be due to chance and for which the between-group difference is big -enough to result in bias in the intervention effect estimate. -Also answer ‘Yes’ if there are other reasons to suspect that the randomization process was problematic: -(4) excessive similarity in baseline characteristics that is not compatible with chance. -Answer ‘No information’ when there is no useful baseline information available (e.g. abstracts, or studies -that reported only baseline characteristics of participants in the final analysis). -The answer to this question should not influence answers to questions 1.1 or 1.2. For example, if the trial -has large baseline imbalances, but authors report adequate randomization methods, questions 1.1 and -1.2 should still be answered on the basis of the reported adequate methods, and any concerns about the - -imbalance should be raised in the answer to the question 1.3 and reflected in the domain-level risk-of- -bias judgement. - -Trialists may undertake analyses that attempt to deal with flawed randomization by controlling for -imbalances in prognostic factors at baseline. To remove the risk of bias caused by problems in the -randomization process, it would be necessary to know, and measure, all the prognostic factors that were -imbalanced at baseline. It is unlikely that all important prognostic factors are known and measured, so -such analyses will at best reduce the risk of bias. If review authors wish to assess the risk of bias in a trial -that controlled for baseline imbalances in order to mitigate failures of randomization, the study should -be assessed using the ROBINS-I tool. - -Y/PY/PN/N/NI - -Risk-of-bias judgement See algorithm. Low / High / Some -concerns - -6 - -Algorithm for suggested judgement of risk of bias arising from the randomization process - -Optional: What is the -predicted direction of -bias arising from the -randomization process? - -If the likely direction of bias can be predicted, it is helpful to state this. The direction might be -characterized either as being towards (or away from) the null, or as being in favour of one of the -interventions. - -NA / Favours -experimental / -Favours comparator / -Towards null /Away -from null / -Unpredictable - -flowchart LR -A["1.2 Allocation sequence concealed?"] - - B["1.1 Allocation sequence random?"] - C1["1.3 Baseline imbalances suggest a problem?"] - C2["1.3 Baseline imbalances suggest a problem?"] - - L["Low risk"] - M["Some concerns"] - H["High risk"] - - %% Paths from allocation concealment - A -- "Y/PY" --> B - A -- "NI" --> C2 - A -- "N/PN" --> H - - %% Paths from random sequence - B -- "Y/PY/NI" --> C1 - B -- "N/PN" --> M - - %% Baseline imbalance (top branch) - C1 -- "N/PN/NI" --> L - C1 -- "Y/PY" --> M - - %% Baseline imbalance (middle branch) - C2 -- "N/PN/NI" --> M - C2 -- "Y/PY" --> H diff --git a/packages/landing/src/components/checklist/ROB2Checklist/scoring/decision-diagrams/domain-2-a.md b/packages/landing/src/components/checklist/ROB2Checklist/scoring/decision-diagrams/domain-2-a.md deleted file mode 100644 index 444468d9..00000000 --- a/packages/landing/src/components/checklist/ROB2Checklist/scoring/decision-diagrams/domain-2-a.md +++ /dev/null @@ -1,206 +0,0 @@ -Domain 2: Risk of bias due to deviations from the intended interventions (effect of assignment to intervention) -Signalling questions Elaboration Response options -2.1. Were participants -aware of their assigned -intervention during the -trial? - -If participants are aware of their assigned intervention it is more likely that health-related behaviours will -differ between the intervention groups. Blinding participants, most commonly through use of a placebo -or sham intervention, may prevent such differences. If participants experienced side effects or toxicities -that they knew to be specific to one of the interventions, answer this question ‘Yes’ or ‘Probably yes’. - -Y/PY/PN/N/NI - -2.2. Were carers and -people delivering the -interventions aware of -participants' assigned -intervention during the -trial? - -If carers or people delivering the interventions are aware of the assigned intervention then its -implementation, or administration of non-protocol interventions, may differ between the intervention -groups. Blinding may prevent such differences. If participants experienced side effects or toxicities that -carers or people delivering the interventions knew to be specific to one of the interventions, answer -question ‘Yes’ or ‘Probably yes’. If randomized allocation was not concealed, then it is likely that carers -and people delivering the interventions were aware of participants' assigned intervention during the -trial. - -Y/PY/PN/N/NI - -8 - -2.3. If Y/PY/NI to 2.1 or -2.2: Were there -deviations from the -intended intervention -that arose because of the -trial context? - -For the effect of assignment to intervention, this domain assesses problems that arise when changes from -assigned intervention that are inconsistent with the trial protocol arose because of the trial context. We -use the term trial context to refer to effects of recruitment and engagement activities on trial participants -and when trial personnel (carers or people delivering the interventions) undermine the implementation of -the trial protocol in ways that would not happen outside the trial. For example, the process of securing -informed consent may lead participants subsequently assigned to the comparator group to feel unlucky -and therefore seek the experimental intervention, or other interventions that improve their prognosis. -Answer ‘Yes’ or ‘Probably yes’ only if there is evidence, or strong reason to believe, that the trial context -led to failure to implement the protocol interventions or to implementation of interventions not allowed -by the protocol. -Answer ‘No’ or ‘Probably no’ if there were changes from assigned intervention that are inconsistent with -the trial protocol, such as non-adherence to intervention, but these are consistent with what could occur -outside the trial context. -Answer ‘No’ or ‘Probably no’ for changes to intervention that are consistent with the trial protocol, for -example cessation of a drug intervention because of acute toxicity or use of additional interventions whose -aim is to treat consequences of one of the intended interventions. -If blinding is compromised because participants report side effects or toxicities that are specific to one of -the interventions, answer ‘Yes’ or ‘Probably yes’ only if there were changes from assigned intervention -that are inconsistent with the trial protocol and arose because of the trial context. -The answer ‘No information’ may be appropriate, because trialists do not always report whether -deviations arose because of the trial context. - -NA/Y/PY/PN/N/NI - -2.4 If Y/PY to 2.3: Were -these deviations likely to -have affected the -outcome? - -Changes from assigned intervention that are inconsistent with the trial protocol and arose because of the -trial context will impact on the intervention effect estimate if they affect the outcome, but not -otherwise. - -NA/Y/PY/PN/N/NI - -9 - -2.5. If Y/PY/NI to 2.4: -Were these deviations -from intended -intervention balanced -between groups? - -Changes from assigned intervention that are inconsistent with the trial protocol and arose because of the -trial context are more likely to impact on the intervention effect estimate if they are not balanced -between the intervention groups. - -NA/Y/PY/PN/N/NI - -2.6 Was an appropriate -analysis used to estimate -the effect of assignment -to intervention? - -Both intention-to-treat (ITT) analyses and modified intention-to-treat (mITT) analyses excluding -participants with missing outcome data should be considered appropriate. Both naïve ‘per-protocol’ -analyses (excluding trial participants who did not receive their assigned intervention) and ‘as treated’ -analyses (in which trial participants are grouped according to the intervention that they received, rather -than according to their assigned intervention) should be considered inappropriate. Analyses excluding - -eligible trial participants post-randomization should also be considered inappropriate, but post- -randomization exclusions of ineligible participants (when eligibility was not confirmed until after - -randomization, and could not have been influenced by intervention group assignment) can be -considered appropriate. - -Y/PY/PN/N/NI - -2.7 If N/PN/NI to 2.6: -Was there potential for a -substantial impact (on -the result) of the failure -to analyse participants in -the group to which they -were randomized? - -This question addresses whether the number of participants who were analysed in the wrong -intervention group, or excluded from the analysis, was sufficient that there could have been a substantial -impact on the result. It is not possible to specify a precise rule: there may be potential for substantial -impact even if fewer than 5% of participants were analysed in the wrong group or excluded, if the -outcome is rare or if exclusions are strongly related to prognostic factors. - -NA/Y/PY/PN/N/NI - -Risk-of-bias judgement See algorithm. Low / High / Some -concerns - -Optional: What is the -predicted direction of -bias due to deviations -from intended -interventions? - -If the likely direction of bias can be predicted, it is helpful to state this. The direction might be -characterized either as being towards (or away from) the null, or as being in favour of one of the -interventions. - -NA / Favours -experimental / Favours -comparator / Towards -null /Away from null / -Unpredictable - -flowchart LR -%% ----------------------- -%% Part 1 -%% ----------------------- -subgraph P1["Part 1: Questions 2.1 to 2.5"] -Q21["2.1 Participants aware of intervention?\n\n2.2 Personnel aware of intervention?"] -Q23["2.3 Deviations that arose because of the trial context?"] -Q24["2.4 Deviations affect outcome?"] -Q25["2.5 Deviations balanced between groups?"] - - P1Low["Low risk"] - P1Some["Some concerns"] - P1High["High risk"] - - Q21 -- "Both N/PN" --> P1Low - Q21 -- "Either Y/PY/NI" --> Q23 - - Q23 -- "N/PN" --> P1Low - Q23 -- "NI" --> P1Some - Q23 -- "Y/PY" --> Q24 - - Q24 -- "N/PN" --> P1Some - Q24 -- "Y/PY/NI" --> Q25 - - Q25 -- "Y/PY" --> P1Some - Q25 -- "N/PN/NI" --> P1High - end - - %% ----------------------- - %% Part 2 - %% ----------------------- - subgraph P2["Part 2: Questions 2.6 & 2.7"] - Q26["2.6 Appropriate analysis to estimate the effect of assignment?"] - Q27["2.7 Substantial impact of the failure to analyse participants in randomized groups?"] - - P2Low["Low risk"] - P2Some["Some concerns"] - P2High["High risk"] - - Q26 -- "Y/PY" --> P2Low - Q26 -- "N/PN/NI" --> Q27 - - Q27 -- "N/PN" --> P2Some - Q27 -- "Y/PY/NI" --> P2High - end - - %% ----------------------- - %% Overall domain judgement - %% ----------------------- - subgraph D["Criteria for the domain"] - DLow["Low risk"] - DSome["Some concerns"] - DHigh["High risk"] - end - - P1Low --> DLow - P2Low --> DLow - - P1Some --> DSome - P2Some --> DSome - - P1High --> DHigh - P2High --> DHigh diff --git a/packages/landing/src/components/checklist/ROB2Checklist/scoring/decision-diagrams/domain-2-b.md b/packages/landing/src/components/checklist/ROB2Checklist/scoring/decision-diagrams/domain-2-b.md deleted file mode 100644 index f1de2344..00000000 --- a/packages/landing/src/components/checklist/ROB2Checklist/scoring/decision-diagrams/domain-2-b.md +++ /dev/null @@ -1,157 +0,0 @@ -Domain 2: Risk of bias due to deviations from the intended interventions (effect of adhering to intervention) -Signalling questions Elaboration Response options -2.1. Were participants -aware of their assigned -intervention during the -trial? - -If participants are aware of their assigned intervention it is more likely that health-related behaviours will -differ between the intervention groups. Blinding participants, most commonly through use of a placebo -or sham intervention, may prevent such differences. If participants experienced side effects or toxicities -that they knew to be specific to one of the interventions, answer this question ‘Yes’ or ‘Probably yes’. - -Y/PY/PN/N/NI - -2.2. Were carers and -people delivering the -interventions aware of -participants' assigned -intervention during the -trial? - -If carers or people delivering the interventions are aware of the assigned intervention then its -implementation, or administration of non-protocol interventions, may differ between the intervention -groups. Blinding may prevent such differences. If participants experienced side effects or toxicities that -carers or people delivering the interventions knew to be specific to one of the interventions, answer ‘Yes’ -or ‘Probably yes’. If randomized allocation was not concealed, then it is likely that carers and people -delivering the interventions were aware of participants' assigned intervention during the trial. - -Y/PY/PN/N/NI - -2.3. [If applicable:] If -Y/PY/NI to 2.1 or 2.2: - -Were important non- -protocol interventions - -balanced across -intervention groups? - -This question is asked only if the preliminary considerations specify that the assessment will address - -imbalance of important non-protocol interventions between intervention groups. Important non- -protocol interventions are the additional interventions or exposures that: (1) are inconsistent with the - -trial protocol; (2) trial participants might receive with or after starting their assigned intervention; and (3) -are prognostic for the outcome. Risk of bias will be higher if there is imbalance in such interventions -between the intervention groups. - -NA/Y/PY/PN/N/NI - -2.4. [If applicable:] Were -there failures in -implementing the -intervention that could -have affected the -outcome? - -This question is asked only if the preliminary considerations specify that the assessment will address -failures in implementing the intervention that could have affected the outcome. Risk of bias will be -higher if the intervention was not implemented as intended by, for example, the health care -professionals delivering care. Answer ‘No’ or ‘Probably no’ if implementation of the intervention was -successful for most participants. - -NA/Y/PY/PN/N/NI - -2.5. [If applicable:] Was -there non-adherence to -the assigned intervention -regimen that could have -affected participants’ -outcomes? - -This question is asked only if the preliminary considerations specify that the assessment will address non- -adherence that could have affected participants’ outcomes. Non-adherence includes imperfect - -compliance with a sustained intervention, cessation of intervention, crossovers to the comparator -intervention and switches to another active intervention. Consider available information on the -proportion of study participants who continued with their assigned intervention throughout follow up, -and answer ‘Yes’ or ‘Probably yes’ if the proportion who did not adhere is high enough to raise concerns. -Answer ‘No’ for studies of interventions that are administered once, so that imperfect adherence is not -possible, and all or most participants received the assigned intervention. - -NA/Y/PY/PN/N/NI - -12 - -2.6. If N/PN/NI to 2.3, or -Y/PY/NI to 2.4 or 2.5: -Was an appropriate -analysis used to estimate -the effect of adhering to -the intervention? - -Both ‘ naïve ‘per-protocol’ analyses (excluding trial participants who did not receive their allocated -intervention) and ‘as treated’ analyses (comparing trial participants according to the intervention they -actually received) will usually be inappropriate for estimating the effect of adhering to intervention (the -‘per-protocol’ effect). However, it is possible to use data from a randomized trial to derive an unbiased -estimate of the effect of adhering to intervention. Examples of appropriate methods include: (1) -instrumental variable analyses to estimate the effect of receiving the assigned intervention in trials in -which a single intervention, administered only at baseline and with all-or-nothing adherence, is compared -with standard care; and (2) inverse probability weighting to adjust for censoring of participants who cease -adherence to their assigned intervention, in trials of sustained treatment strategies. These methods -depend on strong assumptions, which should be appropriate and justified if the answer to this question is -‘Yes’ or ‘Probably yes’. It is possible that a paper reports an analysis based on such methods without -reporting information on the deviations from intended intervention, but it would be hard to judge such an -analysis to be appropriate in the absence of such information. -If an important non-protocol intervention was administered to all participants in one intervention group, -adjustments cannot be made to overcome this. -Some examples of analysis strategies that would not be appropriate to estimate the effect of adhering to -intervention are (i) ‘Intention to treat (ITT) analysis’, (ii) ‘per protocol analysis’, (iii) ‘as-treated analysis’, -(iv) ‘analysis by treatment received’. - -NA/Y/PY/PN/N/NI - -Risk-of-bias judgement See algorithm. Low / High / Some -concerns - -Optional: What is the -predicted direction of -bias due to deviations -from intended -interventions? - -If the likely direction of bias can be predicted, it is helpful to state this. The direction might be -characterized either as being towards (or away from) the null, or as being in favour of one of the -interventions. - -NA / Favours -experimental / Favours -comparator / Towards -null /Away from null - -flowchart LR -Q21["2.1 Participants aware of intervention?\n\n2.2 Personnel aware of intervention?"] -Q23["2.3 Balanced non-protocol interventions?"] -Q24["2.4 Failures in implementation affecting outcome?\n\n2.5 Non-adherence affecting outcome?"] -Q26["2.6 Appropriate analysis to estimate the effect of adhering?"] - - L["Low risk"] - M["Some concerns"] - H["High risk"] - - %% Awareness - Q21 -- "Both N/PN" --> Q24 - Q21 -- "Either Y/PY/NI" --> Q23 - - %% Balanced non-protocol interventions - Q23 -- "NA/Y/PY" --> Q24 - Q23 -- "N/PN/NI" --> Q26 - - %% Failures / non-adherence - Q24 -- "Both NA/N/PN" --> L - Q24 -- "Either Y/PY/NI" --> Q26 - - %% Analysis appropriateness - Q26 -- "Y/PY" --> M - Q26 -- "N/PN/NI" --> H diff --git a/packages/landing/src/components/checklist/ROB2Checklist/scoring/decision-diagrams/domain-3.md b/packages/landing/src/components/checklist/ROB2Checklist/scoring/decision-diagrams/domain-3.md deleted file mode 100644 index dfabfa4c..00000000 --- a/packages/landing/src/components/checklist/ROB2Checklist/scoring/decision-diagrams/domain-3.md +++ /dev/null @@ -1,132 +0,0 @@ -Domain 3: Risk of bias due to missing outcome data -Signalling questions Elaboration Response options -3.1 Were data for this -outcome available for all, -or nearly all, participants -randomized? - -The appropriate study population for an analysis of the intention to treat effect is all randomized -participants. -“Nearly all” should be interpreted as that the number of participants with missing outcome data is -sufficiently small that their outcomes, whatever they were, could have made no important difference to -the estimated effect of intervention. -For continuous outcomes, availability of data from 95% of the participants will often be sufficient. For -dichotomous outcomes, the proportion required is directly linked to the risk of the event. If the observed -number of events is much greater than the number of participants with missing outcome data, the bias -would necessarily be small. -Only answer ‘No information’ if the trial report provides no information about the extent of missing -outcome data. This situation will usually lead to a judgement that there is a high risk of bias due to missing -outcome data. -Note that imputed data should be regarded as missing data, and not considered as ‘outcome data’ in -the context of this question. - -Y/PY/PN/N/NI - -3.2 If N/PN/NI to 3.1: Is -there evidence that the -result was not biased by -missing outcome data? - -Evidence that the result was not biased by missing outcome data may come from: (1) analysis methods -that correct for bias; or (2) sensitivity analyses showing that results are little changed under a range of -plausible assumptions about the relationship between missingness in the outcome and its true value. - -However, imputing the outcome variable, either through methods such as ‘last-observation-carried- -forward’ or via multiple imputation based only on intervention group, should not be assumed to correct - -for bias due to missing outcome data. - -NA/Y/PY/PN/N - -3.3 If N/PN to 3.2: Could -missingness in the -outcome depend on its -true value? - -If loss to follow up, or withdrawal from the study, could be related to participants’ health status, then it -is possible that missingness in the outcome was influenced by its true value. However, if all missing -outcome data occurred for documented reasons that are unrelated to the outcome then the risk of bias -due to missing outcome data will be low (for example, failure of a measuring device or interruptions to -routine data collection). -In time-to-event analyses, participants censored during trial follow-up, for example because they -withdrew from the study, should be regarded as having missing outcome data, even though some of their -follow up is included in the analysis. Note that such participants may be shown as included in analyses in -CONSORT flow diagrams. - -NA/Y/PY/PN/N/NI - -15 - -3.4 If Y/PY/NI to 3.3: Is it -likely that missingness in -the outcome depended on -its true value? - -This question distinguishes between situations in which (i) missingness in the outcome could depend on -its true value (assessed as ‘Some concerns’) from those in which (ii) it is likely that missingness in the -outcome depended on its true value (assessed as ‘High risk of bias’). Five reasons for answering ‘Yes’ are: - -1. Differences between intervention groups in the proportions of missing outcome data. If there is a - difference between the effects of the experimental and comparator interventions on the outcome, - and the missingness in the outcome is influenced by its true value, then the proportions of missing - outcome data are likely to differ between intervention groups. Such a difference suggests a risk of - bias due to missing outcome data, because the trial result will be sensitive to missingness in the - outcome being related to its true value. For time-to-event-data, the analogue is that rates of - censoring (loss to follow-up) differ between the intervention groups. -2. Reported reasons for missing outcome data provide evidence that missingness in the outcome - depends on its true value; -3. Reported reasons for missing outcome data differ between the intervention groups; -4. The circumstances of the trial make it likely that missingness in the outcome depends on its true - value. For example, in trials of interventions to treat schizophrenia it is widely understood that - continuing symptoms make drop out more likely. -5. In time-to-event analyses, participants’ follow up is censored when they stop or change their - assigned intervention, for example because of drug toxicity or, in cancer trials, when participants - switch to second-line chemotherapy. - Answer ‘No’ if the analysis accounted for participant characteristics that are likely to explain the - relationship between missingness in the outcome and its true value. - -NA/Y/PY/PN/N/NI - -Risk-of-bias judgement See algorithm. Low / High / Some -concerns - -Optional: What is the -predicted direction of bias -due to missing outcome -data? - -If the likely direction of bias can be predicted, it is helpful to state this. The direction might be -characterized either as being towards (or away from) the null, or as being in favour of one of the -interventions. - -NA / Favours -experimental / Favours -comparator / Towards -null /Away from null / -Unpredictable - -flowchart LR -Q31["3.1 Outcome data for all participants?"] -Q32["3.2 Evidence that result is not biased?"] -Q33["3.3 Missingness could depend on true value?"] -Q34["3.4 Likely that missingness depended on true value?"] - - L["Low risk"] - M["Some concerns"] - H["High risk"] - - %% Question 3.1 - Q31 -- "Y/PY" --> L - Q31 -- "N/PN/NI" --> Q32 - - %% Question 3.2 - Q32 -- "Y/PY" --> L - Q32 -- "N/PN" --> Q33 - - %% Question 3.3 - Q33 -- "N/PN" --> L - Q33 -- "Y/PY/NI" --> Q34 - - %% Question 3.4 - Q34 -- "N/PN" --> M - Q34 -- "Y/PY/NI" --> H diff --git a/packages/landing/src/components/checklist/ROB2Checklist/scoring/decision-diagrams/domain-4.md b/packages/landing/src/components/checklist/ROB2Checklist/scoring/decision-diagrams/domain-4.md deleted file mode 100644 index 853cc093..00000000 --- a/packages/landing/src/components/checklist/ROB2Checklist/scoring/decision-diagrams/domain-4.md +++ /dev/null @@ -1,137 +0,0 @@ -Domain 4: Risk of bias in measurement of the outcome -Signalling questions Elaboration Response options -4.1 Was the method of -measuring the outcome -inappropriate? - -This question aims to identify methods of outcome measurement (data collection) that are unsuitable for -the outcome they are intended to evaluate. The question does not aim to assess whether the choice of -outcome being evaluated was sensible (e.g. because it is a surrogate or proxy for the main outcome of -interest). In most circumstances, for pre-specified outcomes, the answer to this question will be ‘No’ or -‘Probably no’. -Answer ‘Yes’ or ‘Probably yes’ if the method of measuring the outcome is inappropriate, for example -because: -(1) it is unlikely to be sensitive to plausible intervention effects (e.g. important ranges of outcome -values fall outside levels that are detectable using the measurement method); or -(2) the measurement instrument has been demonstrated to have poor validity. - -Y/PY/PN/N/NI - -4.2 Could measurement -or ascertainment of the -outcome have differed -between intervention -groups? - -Comparable methods of outcome measurement (data collection) involve the same measurement -methods and thresholds, used at comparable time points. Differences between intervention groups may -arise because of ‘diagnostic detection bias’ in the context of passive collection of outcome data, or if an -intervention involves additional visits to a healthcare provider, leading to additional opportunities for -outcome events to be identified. - -Y/PY/PN/N/NI - -4.3 If N/PN/NI to 4.1 and -4.2: Were outcome -assessors aware of the -intervention received by -study participants? - -Answer ‘No’ if outcome assessors were blinded to intervention status. For participant-reported -outcomes, the outcome assessor is the study participant. - -NA/Y/PY/PN/N/NI - -4.4 If Y/PY/NI to 4.3: -Could assessment of the -outcome have been -influenced by knowledge -of intervention received? - -Knowledge of the assigned intervention could influence participant-reported outcomes (such as level of -pain), observer-reported outcomes involving some judgement, and intervention provider decision -outcomes. They are unlikely to influence observer-reported outcomes that do not involve judgement, for -example all-cause mortality. - -NA/Y/PY/PN/N/NI - -18 - -4.5 If Y/PY/NI to 4.4: Is it -likely that assessment of -the outcome was -influenced by knowledge -of intervention received? - -This question distinguishes between situations in which (i) knowledge of intervention status could have -influenced outcome assessment but there is no reason to believe that it did (assessed as ‘Some -concerns’) from those in which (ii) knowledge of intervention status was likely to influence outcome -assessment (assessed as ‘High’). When there are strong levels of belief in either beneficial or harmful -effects of the intervention, it is more likely that the outcome was influenced by knowledge of the -intervention received. Examples may include patient-reported symptoms in trials of homeopathy, or -assessments of recovery of function by a physiotherapist who delivered the intervention. - -NA/Y/PY/PN/N/NI - -Risk-of-bias judgement See algorithm. Low / High / Some -concerns - -Optional: What is the -predicted direction of -bias in measurement of -the outcome? - -If the likely direction of bias can be predicted, it is helpful to state this. The direction might be -characterized either as being towards (or away from) the null, or as being in favour of one of the -interventions. - -NA / Favours -experimental / Favours -comparator / Towards -null /Away from null / -Unpredictable - -flowchart LR -Q41["4.1 Method of measuring the outcome inappropriate?"] -Q42["4.2 Measurement or ascertainment of outcome differ between groups?"] - - Q43a["4.3 Outcome assessors aware of intervention received?"] - Q44a["4.4 Could assessment have been influenced by knowledge of intervention?"] - Q45a["4.5 Likely that assessment was influenced by knowledge of intervention?"] - - Q43b["4.3 Outcome assessors aware of intervention received?"] - Q44b["4.4 Could assessment have been influenced by knowledge of intervention?"] - Q45b["4.5 Likely that assessment was influenced by knowledge of intervention?"] - - L["Low risk"] - M["Some concerns"] - H["High risk"] - - %% 4.1 - Q41 -- "N/PN/NI" --> Q42 - Q41 -- "Y/PY" --> H - - %% 4.2 - Q42 -- "N/PN" --> Q43a - Q42 -- "NI" --> Q43b - Q42 -- "Y/PY" --> H - - %% Branch A (from N/PN) - Q43a -- "N/PN" --> L - Q43a -- "Y/PY/NI" --> Q44a - - Q44a -- "N/PN" --> L - Q44a -- "Y/PY/NI" --> Q45a - - Q45a -- "N/PN" --> M - Q45a -- "Y/PY/NI" --> H - - %% Branch B (from NI) - Q43b -- "N/PN" --> M - Q43b -- "Y/PY/NI" --> Q44b - - Q44b -- "N/PN" --> M - Q44b -- "Y/PY/NI" --> Q45b - - Q45b -- "N/PN" --> M - Q45b -- "Y/PY/NI" --> H diff --git a/packages/landing/src/components/checklist/ROB2Checklist/scoring/decision-diagrams/domain-5.md b/packages/landing/src/components/checklist/ROB2Checklist/scoring/decision-diagrams/domain-5.md deleted file mode 100644 index ff59697e..00000000 --- a/packages/landing/src/components/checklist/ROB2Checklist/scoring/decision-diagrams/domain-5.md +++ /dev/null @@ -1,152 +0,0 @@ -Domain 5: Risk of bias in selection of the reported result -Signalling questions Elaboration Response options -5.1 Were the data that -produced this result -analysed in accordance with -a pre-specified analysis plan -that was finalized before -unblinded outcome data -were available for analysis? - -If the researchers’ pre-specified intentions are available in sufficient detail, then planned outcome -measurements and analyses can be compared with those presented in the published report(s). To -avoid the possibility of selection of the reported result, finalization of the analysis intentions must -precede availability of unblinded outcome data to the trial investigators. -Changes to analysis plans that were made before unblinded outcome data were available, or that -were clearly unrelated to the results (e.g. due to a broken machine making data collection impossible) -do not raise concerns about bias in selection of the reported result. - -Y/PY/PN/N/NI - -Is the numerical result being -assessed likely to have been -selected, on the basis of the -results, from... -5.2. ... multiple eligible -outcome measurements -(e.g. scales, definitions, -time points) within the -outcome domain? - -A particular outcome domain (i.e. a true state or endpoint of interest) may be measured in multiple -ways. For example, the domain pain may be measured using multiple scales (e.g. a visual analogue - -scale and the McGill Pain Questionnaire), each at multiple time points (e.g. 3, 6 and 12 weeks post- -treatment). If multiple measurements were made, but only one or a subset is reported on the basis of - -the results (e.g. statistical significance), there is a high risk of bias in the fully reported result. -Attention should be restricted to outcome measurements that are eligible for consideration by the -RoB 2 tool user. For example, if only a result using a specific measurement scale is eligible for -inclusion in a meta-analysis (e.g. Hamilton Depression Rating Scale), and this is reported by the trial, -then there would not be an issue of selection even if this result was reported (on the basis of the -results) in preference to the result from a different measurement scale (e.g. Beck Depression -Inventory). -Answer ‘Yes’ or ‘Probably yes’ if: -There is clear evidence (usually through examination of a trial protocol or statistical analysis plan) -that a domain was measured in multiple eligible ways, but data for only one or a subset of -measures is fully reported (without justification), and the fully reported result is likely to have been -selected on the basis of the results. Selection on the basis of the results can arise from a desire for -findings to be newsworthy, sufficiently noteworthy to merit publication, or to confirm a prior -hypothesis. For example, trialists who have a preconception, or vested interest in showing, that an - -Y/PY/PN/N/NI - -21 - -experimental intervention is beneficial may be inclined to report outcome measurements -selectively that are favourable to the experimental intervention. -Answer ‘No’ or ‘Probably no’ if: -There is clear evidence (usually through examination of a trial protocol or statistical analysis plan) -that all eligible reported results for the outcome domain correspond to all intended outcome -measurements. -or -There is only one possible way in which the outcome domain can be measured (hence there is no -opportunity to select from multiple measures). -or -Outcome measurements are inconsistent across different reports on the same trial, but the -trialists have provided the reason for the inconsistency and it is not related to the nature of the -results. -Answer ‘No information’ if: -Analysis intentions are not available, or the analysis intentions are not reported in sufficient detail to -enable an assessment, and there is more than one way in which the outcome domain could have -been measured. - -5.3 ... multiple eligible -analyses of the data? - -A particular outcome measurement may be analysed in multiple ways. Examples include: unadjusted -and adjusted models; final value vs change from baseline vs analysis of covariance; transformations of -variables; different definitions of composite outcomes (e.g. ‘major adverse event’); conversion of -continuously scaled outcome to categorical data with different cut-points; different sets of covariates -for adjustment; and different strategies for dealing with missing data. Application of multiple -methods generates multiple effect estimates for a specific outcome measurement. If multiple -estimates are generated but only one or a subset is reported on the basis of the results (e.g. statistical -significance), there is a high risk of bias in the fully reported result. Attention should be restricted to -analyses that are eligible for consideration by the RoB 2 tool user. For example, if only the result from -an analysis of post-intervention values is eligible for inclusion in a meta-analysis (e.g. at 12 weeks -after randomization), and this is reported by the trial, then there would not be an issue of selection -even if this result was reported (on the basis of the results) in preference to the result from an -analysis of changes from baseline. -Answer ‘Yes’ or ‘Probably yes’ if: - -Y/PY/PN/N/NI - -22 - -There is clear evidence (usually through examination of a trial protocol or statistical analysis plan) -that a measurement was analysed in multiple eligible ways, but data for only one or a subset of -analyses is fully reported (without justification), and the fully reported result is likely to have been -selected on the basis of the results. Selection on the basis of the results arises from a desire for -findings to be newsworthy, sufficiently noteworthy to merit publication, or to confirm a prior -hypothesis. For example, trialists who have a preconception or vested interest in showing that an -experimental intervention is beneficial may be inclined to selectively report analyses that are -favourable to the experimental intervention. -Answer ‘No’ or ‘Probably no’ if: -There is clear evidence (usually through examination of a trial protocol or statistical analysis plan) -that all eligible reported results for the outcome measurement correspond to all intended -analyses. -or -There is only one possible way in which the outcome measurement can be analysed (hence there -is no opportunity to select from multiple analyses). -or -Analyses are inconsistent across different reports on the same trial, but the trialists have provided -the reason for the inconsistency and it is not related to the nature of the results. -Answer ‘No information’ if: -Analysis intentions are not available, or the analysis intentions are not reported in sufficient detail to -enable an assessment, and there is more than one way in which the outcome measurement could -have been analysed. - -Risk-of-bias judgement See algorithm. Low / High / Some -concerns - -Optional: What is the -predicted direction of bias -due to selection of the -reported result? - -If the likely direction of bias can be predicted, it is helpful to state this. The direction might be -characterized either as being towards (or away from) the null, or as being in favour of one of the -interventions. - -NA / Favours -experimental / Favours -comparator / Towards -null /Away from null / -Unpredictable - -flowchart LR -Q52["Result selected from…\n\n5.2 …multiple outcome measurements?\n\n5.3 …multiple analyses of the data?"] -Q51["5.1 Trial analysed in accordance with a pre-specified plan?"] - - L["Low risk"] - M["Some concerns"] - H["High risk"] - - %% From result selection questions - Q52 -- "Both N/PN" --> Q51 - Q52 -- "At least one NI,\nbut neither Y/PY" --> M - Q52 -- "Either Y/PY" --> H - - %% From pre-specified analysis plan - Q51 -- "Y/PY" --> L - Q51 -- "N/PN/NI" --> M diff --git a/packages/landing/src/components/checklist/ROB2Checklist/scoring/decision-diagrams/overall.md b/packages/landing/src/components/checklist/ROB2Checklist/scoring/decision-diagrams/overall.md deleted file mode 100644 index 7ea38d05..00000000 --- a/packages/landing/src/components/checklist/ROB2Checklist/scoring/decision-diagrams/overall.md +++ /dev/null @@ -1,23 +0,0 @@ -Overall risk of bias -Risk-of-bias judgement Low / High / Some -concerns - -Optional: What is the overall -predicted direction of bias for this -outcome? - -Favours experimental / -Favours comparator / -Towards null /Away from -null / Unpredictable / NA - -Overall risk-of-bias judgement Criteria -Low risk of bias The study is judged to be at low risk of bias for all domains for this result. -Some concerns The study is judged to raise some concerns in at least one domain for this result, but not to be at high risk of bias for any - -domain. - -High risk of bias The study is judged to be at high risk of bias in at least one domain for this result. - -Or -The study is judged to have diff --git a/packages/landing/src/components/checklist/ROB2Checklist/scoring/decision-diagrams/preliminary.md b/packages/landing/src/components/checklist/ROB2Checklist/scoring/decision-diagrams/preliminary.md deleted file mode 100644 index f2b18b65..00000000 --- a/packages/landing/src/components/checklist/ROB2Checklist/scoring/decision-diagrams/preliminary.md +++ /dev/null @@ -1,37 +0,0 @@ -Preliminary considerations -Study design - Individually-randomized parallel-group trial - Cluster-randomized parallel-group trial - Individually randomized cross-over (or other matched) trial -For the purposes of this assessment, the interventions being compared are defined as -Experimental: Comparator: - -Specify which outcome is being assessed for risk of bias -Specify the numerical result being assessed. In case of multiple alternative -analyses being presented, specify the numeric result (e.g. RR = 1.52 (95% CI -0.83 to 2.77) and/or a reference (e.g. to a table, figure or paragraph) that -uniquely defines the result being assessed. -Is the review team’s aim for this result...? - to assess the effect of assignment to intervention (the ‘intention-to-treat’ effect) - to assess the effect of adhering to intervention (the ‘per-protocol’ effect) -If the aim is to assess the effect of adhering to intervention, select the deviations from intended intervention that should be addressed (at least one must be -checked): - occurrence of non-protocol interventions - failures in implementing the intervention that could have affected the outcome - non-adherence to their assigned intervention by trial participants - -3 - -Which of the following sources were obtained to help inform the risk-of-bias assessment? (tick as many as apply) - Journal article(s) - Trial protocol - Statistical analysis plan (SAP) - Non-commercial trial registry record (e.g. ClinicalTrials.gov record) - Company-owned trial registry record (e.g. GSK Clinical Study Register record) - “Grey literature” (e.g. unpublished thesis) - Conference abstract(s) about the trial - Regulatory document (e.g. Clinical Study Report, Drug Approval Package) - Research ethics application - Grant database summary (e.g. NIH RePORTER or Research Councils UK Gateway to Research) - Personal communication with trialist - Personal communication with the sponsor diff --git a/packages/landing/src/components/checklist/ROBINSIChecklist/__tests__/robins-scoring.test.js b/packages/landing/src/components/checklist/ROBINSIChecklist/__tests__/robins-scoring.test.js deleted file mode 100644 index 327f1628..00000000 --- a/packages/landing/src/components/checklist/ROBINSIChecklist/__tests__/robins-scoring.test.js +++ /dev/null @@ -1,980 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { - scoreRobinsDomain, - getEffectiveDomainJudgement, - scoreAllDomains, - mapOverallJudgementToDisplay, - JUDGEMENTS, -} from '../scoring/robins-scoring.js'; - -// Helper to create answer objects -const ans = answer => ({ answer, comment: '' }); -const answers = obj => Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, ans(v)])); - -describe('scoreRobinsDomain', () => { - describe('Domain 1A (ITT - Confounding)', () => { - it('returns null for incomplete answers (missing Q1)', () => { - const result = scoreRobinsDomain('domain1a', {}); - expect(result.judgement).toBeNull(); - expect(result.isComplete).toBe(false); - }); - - it('returns null when Q1 answered but Q3 missing (Y/PY path)', () => { - const result = scoreRobinsDomain('domain1a', answers({ d1a_1: 'Y' })); - expect(result.judgement).toBeNull(); - expect(result.isComplete).toBe(false); - }); - - it('Path: Q1=Y/PY -> Q3a=N/PN/NI -> Q2a=Y/PY -> NC2=N/PN -> LOW_EX', () => { - const result = scoreRobinsDomain( - 'domain1a', - answers({ d1a_1: 'Y', d1a_2: 'Y', d1a_3: 'N', d1a_4: 'N' }), - ); - expect(result.judgement).toBe(JUDGEMENTS.LOW_EXCEPT_CONFOUNDING); - expect(result.ruleId).toBe('D1A.R1'); - }); - - it('Path: Q1=Y/PY -> Q3a=N/PN/NI -> Q2a=Y/PY -> NC2=Y/PY -> MOD', () => { - const result = scoreRobinsDomain( - 'domain1a', - answers({ d1a_1: 'PY', d1a_2: 'PY', d1a_3: 'PN', d1a_4: 'Y' }), - ); - expect(result.judgement).toBe(JUDGEMENTS.MODERATE); - expect(result.ruleId).toBe('D1A.R2'); - }); - - it('Path: Q1=Y/PY -> Q3a=N/PN/NI -> Q2a=WN -> NC2=N/PN -> LOW_EX', () => { - const result = scoreRobinsDomain( - 'domain1a', - answers({ d1a_1: 'Y', d1a_2: 'WN', d1a_3: 'N', d1a_4: 'N' }), - ); - expect(result.judgement).toBe(JUDGEMENTS.LOW_EXCEPT_CONFOUNDING); - expect(result.ruleId).toBe('D1A.R1'); - }); - - it('Path: Q1=Y/PY -> Q3a=N/PN/NI -> Q2a=SN/NI -> SER (terminal)', () => { - const result = scoreRobinsDomain( - 'domain1a', - answers({ d1a_1: 'Y', d1a_2: 'SN', d1a_3: 'N' }), - ); - expect(result.judgement).toBe(JUDGEMENTS.SERIOUS); - expect(result.ruleId).toBe('D1A.R3'); - }); - - it('Path: Q1=Y/PY -> Q3a=Y/PY -> NC3=N/PN -> SER', () => { - const result = scoreRobinsDomain( - 'domain1a', - answers({ d1a_1: 'PY', d1a_3: 'Y', d1a_4: 'PN' }), - ); - expect(result.judgement).toBe(JUDGEMENTS.SERIOUS); - expect(result.ruleId).toBe('D1A.R5'); - }); - - it('Path: Q1=Y/PY -> Q3a=Y/PY -> NC3=Y/PY -> CRIT', () => { - const result = scoreRobinsDomain('domain1a', answers({ d1a_1: 'Y', d1a_3: 'Y', d1a_4: 'Y' })); - expect(result.judgement).toBe(JUDGEMENTS.CRITICAL); - expect(result.ruleId).toBe('D1A.R4'); - }); - - it('Path: Q1=WN -> Q3b=N/PN/NI -> Q2b=Y/PY/WN -> NC2=N/PN -> LOW_EX', () => { - const result = scoreRobinsDomain( - 'domain1a', - answers({ d1a_1: 'WN', d1a_2: 'WN', d1a_3: 'N', d1a_4: 'N' }), - ); - expect(result.judgement).toBe(JUDGEMENTS.LOW_EXCEPT_CONFOUNDING); - expect(result.ruleId).toBe('D1A.R1'); - }); - - it('Path: Q1=WN -> Q3b=Y/PY -> NC4=N/PN -> SER', () => { - const result = scoreRobinsDomain( - 'domain1a', - answers({ d1a_1: 'WN', d1a_3: 'Y', d1a_4: 'N' }), - ); - expect(result.judgement).toBe(JUDGEMENTS.SERIOUS); - expect(result.ruleId).toBe('D1A.R6'); - }); - - it('Path: Q1=WN -> Q3b=Y/PY -> NC4=Y/PY -> CRIT', () => { - const result = scoreRobinsDomain( - 'domain1a', - answers({ d1a_1: 'WN', d1a_3: 'Y', d1a_4: 'Y' }), - ); - expect(result.judgement).toBe(JUDGEMENTS.CRITICAL); - expect(result.ruleId).toBe('D1A.R7'); - }); - - it('Path: Q1=SN/NI -> NC1=N/PN -> SER', () => { - const result = scoreRobinsDomain('domain1a', answers({ d1a_1: 'SN', d1a_4: 'PN' })); - expect(result.judgement).toBe(JUDGEMENTS.SERIOUS); - expect(result.ruleId).toBe('D1A.R8'); - }); - - it('Path: Q1=SN/NI -> NC1=Y/PY -> CRIT', () => { - const result = scoreRobinsDomain('domain1a', answers({ d1a_1: 'NI', d1a_4: 'Y' })); - expect(result.judgement).toBe(JUDGEMENTS.CRITICAL); - expect(result.ruleId).toBe('D1A.R9'); - }); - }); - - describe('Domain 1B (Per-Protocol - Confounding)', () => { - it('returns null for incomplete answers (missing Q1)', () => { - const result = scoreRobinsDomain('domain1b', {}); - expect(result.judgement).toBeNull(); - expect(result.isComplete).toBe(false); - }); - - it('Path: Q1=Y/PY -> Q2=Y/PY -> Q3a=Y/PY -> NC1=N/PN -> LOW', () => { - const result = scoreRobinsDomain( - 'domain1b', - answers({ - d1b_1: 'Y', - d1b_2: 'Y', - d1b_3: 'PY', - d1b_5: 'N', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.LOW); - expect(result.ruleId).toBe('D1B.R1'); - }); - - it('Path: Q1=Y/PY -> Q2=Y/PY -> Q3a=Y/PY -> NC1=Y/PY -> MOD', () => { - const result = scoreRobinsDomain( - 'domain1b', - answers({ - d1b_1: 'PY', - d1b_2: 'Y', - d1b_3: 'Y', - d1b_5: 'Y', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.MODERATE); - expect(result.ruleId).toBe('D1B.R2'); - }); - - it('Path: Q1=Y/PY -> Q2=Y/PY -> Q3a=WN -> NC2=N/PN -> LOW_EX', () => { - const result = scoreRobinsDomain( - 'domain1b', - answers({ - d1b_1: 'Y', - d1b_2: 'Y', - d1b_3: 'WN', - d1b_5: 'PN', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.LOW_EXCEPT_CONFOUNDING); - expect(result.ruleId).toBe('D1B.R3'); - }); - - it('Path: Q1=Y/PY -> Q2=WN -> Q3b=Y/PY/WN -> NC2=N/PN -> LOW_EX', () => { - const result = scoreRobinsDomain( - 'domain1b', - answers({ - d1b_1: 'PY', - d1b_2: 'WN', - d1b_3: 'WN', - d1b_5: 'N', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.LOW_EXCEPT_CONFOUNDING); - expect(result.ruleId).toBe('D1B.R3'); - }); - - it('Path: Q1=Y/PY -> Q2=SN/NI -> SER (terminal)', () => { - const result = scoreRobinsDomain( - 'domain1b', - answers({ - d1b_1: 'Y', - d1b_2: 'SN', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.SERIOUS); - expect(result.ruleId).toBe('D1B.R5'); - }); - - it('Path: Q1=N/PN/NI -> Q4=Y/PY -> CRIT (terminal)', () => { - const result = scoreRobinsDomain( - 'domain1b', - answers({ - d1b_1: 'N', - d1b_4: 'Y', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.CRITICAL); - expect(result.ruleId).toBe('D1B.R6'); - }); - - it('Path: Q1=N/PN/NI -> Q4=N/PN/NI -> NC3=N/PN -> SER', () => { - const result = scoreRobinsDomain( - 'domain1b', - answers({ - d1b_1: 'PN', - d1b_4: 'PN', - d1b_5: 'N', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.SERIOUS); - expect(result.ruleId).toBe('D1B.R8'); - }); - - it('Path: Q1=N/PN/NI -> Q4=N/PN/NI -> NC3=Y/PY -> CRIT', () => { - const result = scoreRobinsDomain( - 'domain1b', - answers({ - d1b_1: 'N', - d1b_4: 'NI', - d1b_5: 'Y', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.CRITICAL); - expect(result.ruleId).toBe('D1B.R7'); - }); - }); - - describe('Domain 2 (Classification of Interventions)', () => { - it('returns null for incomplete answers (missing Q1)', () => { - const result = scoreRobinsDomain('domain2', {}); - expect(result.judgement).toBeNull(); - expect(result.isComplete).toBe(false); - }); - - it('Path: A1=Y/PY -> C1=N/PN -> E1=N/PN -> LOW', () => { - const result = scoreRobinsDomain( - 'domain2', - answers({ - d2_1: 'Y', - d2_4: 'N', - d2_5: 'PN', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.LOW); - expect(result.ruleId).toBe('D2.R1'); - }); - - it('Path: A1=Y/PY -> C1=N/PN -> E1=Y/PY/NI -> MOD', () => { - const result = scoreRobinsDomain( - 'domain2', - answers({ - d2_1: 'PY', - d2_4: 'PN', - d2_5: 'Y', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.MODERATE); - expect(result.ruleId).toBe('D2.R2'); - }); - - it('Path: A1=Y/PY -> C1=WY/NI -> E2=N/PN -> MOD', () => { - const result = scoreRobinsDomain( - 'domain2', - answers({ - d2_1: 'Y', - d2_4: 'WY', - d2_5: 'N', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.MODERATE); - expect(result.ruleId).toBe('D2.R3'); - }); - - it('Path: A1=Y/PY -> C1=SY -> E3=N/PN -> SER', () => { - const result = scoreRobinsDomain( - 'domain2', - answers({ - d2_1: 'PY', - d2_4: 'SY', - d2_5: 'PN', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.SERIOUS); - expect(result.ruleId).toBe('D2.R4'); - }); - - it('Path: A1=Y/PY -> C1=SY -> E3=Y/PY/NI -> CRIT', () => { - const result = scoreRobinsDomain( - 'domain2', - answers({ - d2_1: 'Y', - d2_4: 'SY', - d2_5: 'Y', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.CRITICAL); - expect(result.ruleId).toBe('D2.R4'); - }); - - it('Path: A1=N/PN/NI -> A2=Y/PY -> C1=N/PN -> E1=N/PN -> LOW', () => { - const result = scoreRobinsDomain( - 'domain2', - answers({ - d2_1: 'N', - d2_2: 'Y', - d2_4: 'PN', - d2_5: 'N', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.LOW); - expect(result.ruleId).toBe('D2.R5'); - }); - - it('Path: A1=N/PN/NI -> A2=N/PN/NI -> A3=WY/NI -> C2=N/PN -> E2=N/PN -> MOD', () => { - const result = scoreRobinsDomain( - 'domain2', - answers({ - d2_1: 'PN', - d2_2: 'PN', - d2_3: 'WY', - d2_4: 'N', - d2_5: 'PN', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.MODERATE); - expect(result.ruleId).toBe('D2.R6'); - }); - - it('treats SY like WY for 2.3 (A3=SY takes the C2 path)', () => { - const result = scoreRobinsDomain( - 'domain2', - answers({ - d2_1: 'PN', - d2_2: 'PN', - d2_3: 'SY', - d2_4: 'N', - d2_5: 'PN', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.MODERATE); - expect(result.ruleId).toBe('D2.R6'); - }); - - it('Path: A1=N/PN/NI -> A2=N/PN/NI -> A3=N/PN -> C3=SY/WY/NI -> CRIT (terminal)', () => { - const result = scoreRobinsDomain( - 'domain2', - answers({ - d2_1: 'N', - d2_2: 'NI', - d2_3: 'N', - d2_4: 'WY', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.CRITICAL); - expect(result.ruleId).toBe('D2.R7'); - }); - }); - - describe('Domain 3 (Selection Bias - Multi-step)', () => { - it('returns null when Part A incomplete', () => { - const result = scoreRobinsDomain('domain3', answers({ d3_1: 'Y' })); - expect(result.judgement).toBeNull(); - expect(result.isComplete).toBe(false); - }); - - it('Path: All LOW -> LOW (terminal, no correction questions)', () => { - const result = scoreRobinsDomain( - 'domain3', - answers({ - d3_1: 'Y', - d3_2: 'N', - d3_3: 'N', - d3_4: 'N', - d3_5: 'N', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.LOW); - expect(result.ruleId).toBe('D3.R1'); - }); - - it('Path: At worst MODERATE -> MOD (terminal, no correction questions)', () => { - const result = scoreRobinsDomain( - 'domain3', - answers({ - d3_1: 'Y', - d3_2: 'Y', - d3_3: 'N', - d3_4: 'N', - d3_5: 'N', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.MODERATE); - expect(result.ruleId).toBe('D3.R2'); - }); - - it('Path: At least one SERIOUS -> C1=Y/PY -> MOD', () => { - const result = scoreRobinsDomain( - 'domain3', - answers({ - d3_1: 'SN', - d3_2: 'N', - d3_3: 'N', - d3_4: 'N', - d3_5: 'N', - d3_6: 'Y', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.MODERATE); - expect(result.ruleId).toBe('D3.R3'); - }); - - it('Path: At least one SERIOUS -> C1=N/PN/NI -> C2=Y/PY -> MOD', () => { - const result = scoreRobinsDomain( - 'domain3', - answers({ - d3_1: 'SN', - d3_2: 'N', - d3_3: 'N', - d3_4: 'N', - d3_5: 'N', - d3_6: 'N', - d3_7: 'Y', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.MODERATE); - expect(result.ruleId).toBe('D3.R3'); - }); - - it('Path: At least one SERIOUS -> C1=N/PN/NI -> C2=N/PN/NI -> C3=N/PN/NI -> SER', () => { - const result = scoreRobinsDomain( - 'domain3', - answers({ - d3_1: 'SN', - d3_2: 'N', - d3_3: 'N', - d3_4: 'N', - d3_5: 'N', - d3_6: 'PN', - d3_7: 'N', - d3_8: 'NI', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.SERIOUS); - expect(result.ruleId).toBe('D3.R4'); - }); - - it('Path: At least one SERIOUS -> C1=N/PN/NI -> C2=N/PN/NI -> C3=Y/PY -> CRIT', () => { - const result = scoreRobinsDomain( - 'domain3', - answers({ - d3_1: 'Y', - d3_2: 'N', - d3_3: 'Y', - d3_4: 'Y', - d3_5: 'Y', - d3_6: 'N', - d3_7: 'PN', - d3_8: 'Y', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.CRITICAL); - expect(result.ruleId).toBe('D3.R5'); - }); - - it('Path: Part A WN/NI -> MOD, Part B N/PN -> LOW -> MOD', () => { - const result = scoreRobinsDomain( - 'domain3', - answers({ - d3_1: 'WN', - d3_2: 'N', - d3_3: 'N', - d3_4: 'N', - d3_5: 'N', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.MODERATE); - expect(result.ruleId).toBe('D3.R2'); - }); - - it('Path: Part A SN -> SER, Part B N/PN -> LOW -> SER (needs correction)', () => { - const result = scoreRobinsDomain( - 'domain3', - answers({ - d3_1: 'SN', - d3_2: 'N', - d3_3: 'N', - d3_4: 'N', - d3_5: 'N', - d3_6: 'N', - d3_7: 'N', - d3_8: 'N', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.SERIOUS); - expect(result.ruleId).toBe('D3.R4'); - }); - }); - - describe('Domain 4 (Missing Data)', () => { - it('returns null when 4.1-4.3 incomplete', () => { - const result = scoreRobinsDomain('domain4', answers({ d4_1: 'Y' })); - expect(result.judgement).toBeNull(); - expect(result.isComplete).toBe(false); - }); - - it('Path: All Y/PY complete data -> LOW (terminal)', () => { - const result = scoreRobinsDomain( - 'domain4', - answers({ - d4_1: 'Y', - d4_2: 'PY', - d4_3: 'Y', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.LOW); - expect(result.ruleId).toBe('D4.R1'); - }); - - it('Path: Missing data -> B=Y/PY/NI -> C=N/PN -> LOW', () => { - const result = scoreRobinsDomain( - 'domain4', - answers({ - d4_1: 'N', - d4_2: 'Y', - d4_3: 'Y', - d4_4: 'Y', - d4_5: 'N', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.LOW); - expect(result.ruleId).toBe('D4.R2'); - }); - - it('treats NA like NI so scoring does not get stuck (4.4=NA behaves like 4.4=NI)', () => { - const result = scoreRobinsDomain( - 'domain4', - answers({ - d4_1: 'N', - d4_2: 'Y', - d4_3: 'Y', - d4_4: 'NA', - d4_5: 'N', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.LOW); - expect(result.ruleId).toBe('D4.R2'); - }); - - it('Path: Missing data -> B=Y/PY/NI -> C=Y/PY/NI -> E=Y/PY -> F1=Y/PY -> MOD', () => { - const result = scoreRobinsDomain( - 'domain4', - answers({ - d4_1: 'N', - d4_2: 'Y', - d4_3: 'Y', - d4_4: 'Y', - d4_5: 'Y', - d4_6: 'Y', - d4_11: 'Y', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.MODERATE); - expect(result.ruleId).toBe('D4.R3'); - }); - - it('Path: Missing data -> B=Y/PY/NI -> C=Y/PY/NI -> E=Y/PY -> F1=N/PN -> SER', () => { - const result = scoreRobinsDomain( - 'domain4', - answers({ - d4_1: 'N', - d4_2: 'Y', - d4_3: 'Y', - d4_4: 'NI', - d4_5: 'Y', - d4_6: 'PY', - d4_11: 'N', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.SERIOUS); - expect(result.ruleId).toBe('D4.R4'); - }); - - it('Path: Missing data -> B=N/PN -> D=Y/PY -> G=Y/PY -> I=Y/PY -> LOW', () => { - const result = scoreRobinsDomain( - 'domain4', - answers({ - d4_1: 'PN', - d4_2: 'Y', - d4_3: 'Y', - d4_4: 'N', - d4_7: 'Y', - d4_8: 'Y', - d4_9: 'Y', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.LOW); - expect(result.ruleId).toBe('D4.R5'); - }); - - it('Path: Missing data -> B=N/PN -> D=Y/PY -> G=Y/PY -> I=WN/NI -> F2=Y/PY -> MOD', () => { - const result = scoreRobinsDomain( - 'domain4', - answers({ - d4_1: 'N', - d4_2: 'Y', - d4_3: 'Y', - d4_4: 'N', - d4_7: 'Y', - d4_8: 'Y', - d4_9: 'WN', - d4_11: 'Y', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.MODERATE); - expect(result.ruleId).toBe('D4.R6'); - }); - - it('Path: Missing data -> B=N/PN -> D=N/PN/NI -> H=Y/PY -> LOW', () => { - const result = scoreRobinsDomain( - 'domain4', - answers({ - d4_1: 'N', - d4_2: 'N', - d4_3: 'N', - d4_4: 'N', - d4_7: 'N', - d4_10: 'Y', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.LOW); - expect(result.ruleId).toBe('D4.R2'); - }); - - it('Path: Missing data -> B=N/PN -> D=N/PN/NI -> H=SN -> F3=N/PN -> CRIT', () => { - const result = scoreRobinsDomain( - 'domain4', - answers({ - d4_1: 'N', - d4_2: 'N', - d4_3: 'N', - d4_4: 'N', - d4_7: 'NI', - d4_10: 'SN', - d4_11: 'N', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.CRITICAL); - expect(result.ruleId).toBe('D4.R8'); - }); - }); - - describe('Domain 5 (Measurement of Outcome)', () => { - it('returns null for incomplete answers (missing Q1)', () => { - const result = scoreRobinsDomain('domain5', {}); - expect(result.judgement).toBeNull(); - expect(result.isComplete).toBe(false); - }); - - it('Path: Q1=Y/PY -> SER (terminal, no Q2/Q3 needed)', () => { - const result = scoreRobinsDomain( - 'domain5', - answers({ - d5_1: 'Y', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.SERIOUS); - expect(result.ruleId).toBe('D5.R1'); - }); - - it('Path: Q1=N/PN -> Q2a=N/PN -> LOW (terminal, no Q3 needed)', () => { - const result = scoreRobinsDomain( - 'domain5', - answers({ - d5_1: 'N', - d5_2: 'PN', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.LOW); - expect(result.ruleId).toBe('D5.R2'); - }); - - it('Path: Q1=N/PN -> Q2a=Y/PY/NI -> Q3a=N/PN -> LOW', () => { - const result = scoreRobinsDomain( - 'domain5', - answers({ - d5_1: 'PN', - d5_2: 'Y', - d5_3: 'N', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.LOW); - expect(result.ruleId).toBe('D5.R3'); - }); - - it('Path: Q1=N/PN -> Q2a=Y/PY/NI -> Q3a=WY/NI -> MOD', () => { - const result = scoreRobinsDomain( - 'domain5', - answers({ - d5_1: 'N', - d5_2: 'PY', - d5_3: 'WY', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.MODERATE); - expect(result.ruleId).toBe('D5.R4'); - }); - - it('Path: Q1=N/PN -> Q2a=Y/PY/NI -> Q3a=SY -> SER', () => { - const result = scoreRobinsDomain( - 'domain5', - answers({ - d5_1: 'PN', - d5_2: 'Y', - d5_3: 'SY', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.SERIOUS); - expect(result.ruleId).toBe('D5.R5'); - }); - - it('Path: Q1=NI -> Q2b=N/PN -> MOD (terminal, no Q3 needed)', () => { - const result = scoreRobinsDomain( - 'domain5', - answers({ - d5_1: 'NI', - d5_2: 'N', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.MODERATE); - expect(result.ruleId).toBe('D5.R6'); - }); - - it('Path: Q1=NI -> Q2b=Y/PY/NI -> Q3b=WY/N/PN/NI -> MOD', () => { - const result = scoreRobinsDomain( - 'domain5', - answers({ - d5_1: 'NI', - d5_2: 'Y', - d5_3: 'WY', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.MODERATE); - expect(result.ruleId).toBe('D5.R7'); - }); - - it('Path: Q1=NI -> Q2b=Y/PY/NI -> Q3b=SY -> SER', () => { - const result = scoreRobinsDomain( - 'domain5', - answers({ - d5_1: 'NI', - d5_2: 'PY', - d5_3: 'SY', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.SERIOUS); - expect(result.ruleId).toBe('D5.R7'); - }); - }); - - describe('Domain 6 (Selection of Reported Result)', () => { - it('returns null for incomplete answers (missing Q1)', () => { - const result = scoreRobinsDomain('domain6', {}); - expect(result.judgement).toBeNull(); - expect(result.isComplete).toBe(false); - }); - - it('Path: Q1=Y/PY -> LOW (terminal, no selection questions needed)', () => { - const result = scoreRobinsDomain( - 'domain6', - answers({ - d6_1: 'Y', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.LOW); - expect(result.ruleId).toBe('D6.R1'); - }); - - it('Path: Q1=N/PN/NI -> SEL: All N/PN -> LOW', () => { - const result = scoreRobinsDomain( - 'domain6', - answers({ - d6_1: 'N', - d6_2: 'N', - d6_3: 'PN', - d6_4: 'N', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.LOW); - expect(result.ruleId).toBe('D6.R2'); - }); - - it('Path: Q1=N/PN/NI -> SEL: At least one NI, but none Y/PY -> MOD', () => { - const result = scoreRobinsDomain( - 'domain6', - answers({ - d6_1: 'PN', - d6_2: 'N', - d6_3: 'NI', - d6_4: 'N', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.MODERATE); - expect(result.ruleId).toBe('D6.R3'); - }); - - it('Path: Q1=N/PN/NI -> SEL: One Y/PY -> SER', () => { - const result = scoreRobinsDomain( - 'domain6', - answers({ - d6_1: 'N', - d6_2: 'Y', - d6_3: 'N', - d6_4: 'N', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.SERIOUS); - expect(result.ruleId).toBe('D6.R4'); - }); - - it('Path: Q1=N/PN/NI -> SEL: All NI -> SER', () => { - const result = scoreRobinsDomain( - 'domain6', - answers({ - d6_1: 'N', - d6_2: 'NI', - d6_3: 'NI', - d6_4: 'NI', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.SERIOUS); - expect(result.ruleId).toBe('D6.R4'); - }); - - it('Path: Q1=N/PN/NI -> SEL: Two or more Y/PY -> CRIT', () => { - const result = scoreRobinsDomain( - 'domain6', - answers({ - d6_1: 'NI', - d6_2: 'Y', - d6_3: 'PY', - d6_4: 'N', - }), - ); - expect(result.judgement).toBe(JUDGEMENTS.CRITICAL); - expect(result.ruleId).toBe('D6.R5'); - }); - }); -}); - -describe('getEffectiveDomainJudgement', () => { - it('returns auto judgement when source is auto', () => { - const domainState = { judgementSource: 'auto', judgement: null }; - const autoScore = { judgement: JUDGEMENTS.MODERATE }; - expect(getEffectiveDomainJudgement(domainState, autoScore)).toBe(JUDGEMENTS.MODERATE); - }); - - it('returns manual judgement when source is manual and judgement exists', () => { - const domainState = { judgementSource: 'manual', judgement: JUDGEMENTS.SERIOUS }; - const autoScore = { judgement: JUDGEMENTS.LOW }; - expect(getEffectiveDomainJudgement(domainState, autoScore)).toBe(JUDGEMENTS.SERIOUS); - }); - - it('falls back to auto when manual but no judgement set', () => { - const domainState = { judgementSource: 'manual', judgement: null }; - const autoScore = { judgement: JUDGEMENTS.LOW }; - expect(getEffectiveDomainJudgement(domainState, autoScore)).toBe(JUDGEMENTS.LOW); - }); -}); - -describe('scoreAllDomains', () => { - it('returns incomplete when not all domains are scored', () => { - const checklistState = { - sectionC: { isPerProtocol: false }, - domain1a: { answers: {} }, - domain2: { answers: {} }, - domain3: { answers: {} }, - domain4: { answers: {} }, - domain5: { answers: {} }, - domain6: { answers: {} }, - }; - const result = scoreAllDomains(checklistState); - expect(result.isComplete).toBe(false); - expect(result.overall).toBeNull(); - }); - - it('uses domain1a for ITT (isPerProtocol=false)', () => { - const checklistState = { - sectionC: { isPerProtocol: false }, - domain1a: { answers: answers({ d1a_1: 'Y', d1a_2: 'Y', d1a_3: 'N', d1a_4: 'N' }) }, - domain1b: { answers: {} }, - domain2: { answers: answers({ d2_1: 'Y', d2_4: 'N', d2_5: 'N' }) }, - domain3: { answers: answers({ d3_1: 'Y', d3_2: 'N', d3_3: 'N', d3_4: 'N', d3_5: 'N' }) }, - domain4: { answers: answers({ d4_1: 'Y', d4_2: 'Y', d4_3: 'Y' }) }, - domain5: { answers: answers({ d5_1: 'N', d5_2: 'N' }) }, - domain6: { answers: answers({ d6_1: 'Y' }) }, - }; - const result = scoreAllDomains(checklistState); - expect(result.isComplete).toBe(true); - expect(result.domains.domain1a).toBeDefined(); - expect(result.domains.domain1b).toBeUndefined(); - }); - - it('uses domain1b for per-protocol (isPerProtocol=true)', () => { - const checklistState = { - sectionC: { isPerProtocol: true }, - domain1a: { answers: {} }, - domain1b: { answers: answers({ d1b_1: 'Y', d1b_2: 'Y', d1b_3: 'Y', d1b_5: 'N' }) }, - domain2: { answers: answers({ d2_1: 'Y', d2_4: 'N', d2_5: 'N' }) }, - domain3: { answers: answers({ d3_1: 'Y', d3_2: 'N', d3_3: 'N', d3_4: 'N', d3_5: 'N' }) }, - domain4: { answers: answers({ d4_1: 'Y', d4_2: 'Y', d4_3: 'Y' }) }, - domain5: { answers: answers({ d5_1: 'N', d5_2: 'N' }) }, - domain6: { answers: answers({ d6_1: 'Y' }) }, - }; - const result = scoreAllDomains(checklistState); - expect(result.isComplete).toBe(true); - expect(result.domains.domain1a).toBeUndefined(); - expect(result.domains.domain1b).toBeDefined(); - }); - - it('calculates overall as max severity across domains', () => { - const checklistState = { - sectionC: { isPerProtocol: false }, - domain1a: { answers: answers({ d1a_1: 'Y', d1a_2: 'Y', d1a_3: 'N', d1a_4: 'N' }) }, // LOW_EX - domain2: { answers: answers({ d2_1: 'Y', d2_4: 'N', d2_5: 'N' }) }, // LOW - domain3: { answers: answers({ d3_1: 'Y', d3_2: 'N', d3_3: 'N', d3_4: 'N', d3_5: 'N' }) }, // LOW - domain4: { answers: answers({ d4_1: 'Y', d4_2: 'Y', d4_3: 'Y' }) }, // LOW - domain5: { answers: answers({ d5_1: 'N', d5_2: 'N' }) }, // LOW - domain6: { answers: answers({ d6_1: 'Y' }) }, // LOW - }; - const result = scoreAllDomains(checklistState); - expect(result.isComplete).toBe(true); - expect(result.overall).toBe(JUDGEMENTS.LOW); - }); - - it('calculates overall as CRITICAL when any domain is CRITICAL', () => { - const checklistState = { - sectionC: { isPerProtocol: false }, - domain1a: { answers: answers({ d1a_1: 'Y', d1a_2: 'Y', d1a_3: 'N', d1a_4: 'N' }) }, // LOW_EX - domain2: { answers: answers({ d2_1: 'Y', d2_4: 'N', d2_5: 'N' }) }, // LOW - domain3: { answers: answers({ d3_1: 'Y', d3_2: 'N', d3_3: 'N', d3_4: 'N', d3_5: 'N' }) }, // LOW - domain4: { answers: answers({ d4_1: 'Y', d4_2: 'Y', d4_3: 'Y' }) }, // LOW - domain5: { answers: answers({ d5_1: 'N', d5_2: 'N' }) }, // LOW - domain6: { answers: answers({ d6_1: 'N', d6_2: 'Y', d6_3: 'Y', d6_4: 'N' }) }, // CRITICAL - }; - const result = scoreAllDomains(checklistState); - expect(result.isComplete).toBe(true); - expect(result.overall).toBe(JUDGEMENTS.CRITICAL); - }); -}); - -describe('mapOverallJudgementToDisplay', () => { - it('maps Low to confounding display string (ROBINS-I overall cannot be plain Low)', () => { - expect(mapOverallJudgementToDisplay(JUDGEMENTS.LOW)).toBe( - 'Low risk of bias except for concerns about uncontrolled confounding', - ); - }); - - it('maps Low except confounding to display string', () => { - expect(mapOverallJudgementToDisplay(JUDGEMENTS.LOW_EXCEPT_CONFOUNDING)).toBe( - 'Low risk of bias except for concerns about uncontrolled confounding', - ); - }); - - it('maps Moderate to display string', () => { - expect(mapOverallJudgementToDisplay(JUDGEMENTS.MODERATE)).toBe('Moderate risk'); - }); - - it('maps Serious to display string', () => { - expect(mapOverallJudgementToDisplay(JUDGEMENTS.SERIOUS)).toBe('Serious risk'); - }); - - it('maps Critical to display string', () => { - expect(mapOverallJudgementToDisplay(JUDGEMENTS.CRITICAL)).toBe('Critical risk'); - }); -}); diff --git a/packages/landing/src/components/checklist/ROBINSIChecklist/scoring/decision-diagrams/domain-1-a.md b/packages/landing/src/components/checklist/ROBINSIChecklist/scoring/decision-diagrams/domain-1-a.md deleted file mode 100644 index df1091b3..00000000 --- a/packages/landing/src/components/checklist/ROBINSIChecklist/scoring/decision-diagrams/domain-1-a.md +++ /dev/null @@ -1,81 +0,0 @@ -```mermaid -flowchart LR - %% -------------------- - %% Core questions - %% -------------------- - Q1["1.1 Controlled for all the important confounding factors?"] - Q3a["1.3 Controlled for any post-intervention variables?"] - Q3b["1.3 Controlled for any post-intervention variables?"] - Q2a["1.2 Confounding factors measured validly and reliably?"] - Q2b["1.2 Confounding factors measured validly and reliably?"] - Q2c["1.2 Confounding factors measured validly and reliably?"] - - %% -------------------- - %% Negative controls - %% -------------------- - NC1["1.4 Negative controls etc suggest serious uncontrolled confounding?"] - NC2["1.4 Negative controls etc suggest serious uncontrolled confounding?"] - NC3["1.4 Negative controls etc suggest serious uncontrolled confounding?"] - NC4["1.4 Negative controls etc suggest serious uncontrolled confounding?"] - - %% -------------------- - %% Outcomes - %% -------------------- - LOW_EX["LOW RISK OF BIAS\n(except for concerns about uncontrolled confounding)"] - MOD["MODERATE RISK OF BIAS"] - SER["SERIOUS RISK OF BIAS"] - CRIT["CRITICAL RISK OF BIAS"] - - %% -------------------- - %% From 1.1 - %% -------------------- - Q1 -- "Y / PY" --> Q3a - Q1 -- "WN" --> Q3b - Q1 -- "SN / NI" --> NC1 - - %% -------------------- - %% From 1.3 (top) - %% -------------------- - Q3a -- "N / PN / NI" --> Q2a - Q3a -- "Y / PY" --> NC3 - - %% -------------------- - %% From 1.3 (middle) - %% -------------------- - Q3b -- "N / PN / NI" --> Q2b - Q3b -- "Y / PY" --> NC4 - - %% -------------------- - %% From 1.2 (top) - %% -------------------- - Q2a -- "Y / PY" --> NC2 - Q2a -- "WN" --> NC2 - Q2a -- "SN / NI" --> SER - - %% -------------------- - %% From 1.2 (middle) - %% -------------------- - Q2b -- "Y / PY / WN" --> NC2 - Q2b -- "SN / NI" --> SER - - %% -------------------- - %% From 1.2 (bottom) - %% -------------------- - Q2c -- "Y / PY" --> SER - Q2c -- "SN / WN / NI" --> CRIT - - %% -------------------- - %% Negative controls → outcomes - %% -------------------- - NC1 -- "N / PN" --> SER - NC1 -- "Y / PY" --> CRIT - - NC2 -- "N / PN" --> LOW_EX - NC2 -- "Y / PY" --> MOD - - NC3 -- "N / PN" --> SER - NC3 -- "Y / PY" --> CRIT - - NC4 -- "N / PN" --> SER - NC4 -- "Y / PY" --> CRIT -``` diff --git a/packages/landing/src/components/checklist/ROBINSIChecklist/scoring/decision-diagrams/domain-1-b.md b/packages/landing/src/components/checklist/ROBINSIChecklist/scoring/decision-diagrams/domain-1-b.md deleted file mode 100644 index 023258bc..00000000 --- a/packages/landing/src/components/checklist/ROBINSIChecklist/scoring/decision-diagrams/domain-1-b.md +++ /dev/null @@ -1,71 +0,0 @@ -```mermaid -flowchart LR - %% -------------------- - %% Core questions - %% -------------------- - Q1["1.1 Appropriate analysis method?"] - Q2["1.2 Controlled for all the important confounding factors?"] - Q3a["1.3 Confounding factors measured validly and reliably?"] - Q3b["1.3 Confounding factors measured validly and reliably?"] - Q4["1.4 Controlled for variables measured after start of intervention?"] - - %% -------------------- - %% Negative controls - %% -------------------- - NC1["1.5 Negative controls etc suggest serious uncontrolled confounding?"] - NC2["1.5 Negative controls etc suggest serious uncontrolled confounding?"] - NC3["1.5 Negative controls etc suggest serious uncontrolled confounding?"] - - %% -------------------- - %% Outcomes - %% -------------------- - LOW["LOW RISK OF BIAS"] - LOW_EX["LOW RISK OF BIAS\n(except for concerns about uncontrolled confounding)"] - MOD["MODERATE RISK OF BIAS"] - SER["SERIOUS RISK OF BIAS"] - CRIT["CRITICAL RISK OF BIAS"] - - %% -------------------- - %% Paths from 1.1 - %% -------------------- - Q1 -- "Y / PY" --> Q2 - Q1 -- "N / PN / NI" --> Q4 - - %% -------------------- - %% Paths from 1.2 - %% -------------------- - Q2 -- "Y / PY" --> Q3a - Q2 -- "WN" --> Q3b - Q2 -- "SN / NI" --> SER - - %% -------------------- - %% Paths from 1.3 (top) - %% -------------------- - Q3a -- "Y / PY" --> NC1 - Q3a -- "WN" --> NC2 - Q3a -- "SN / NI" --> SER - - %% -------------------- - %% Paths from 1.3 (middle) - %% -------------------- - Q3b -- "Y / PY / WN" --> NC2 - Q3b -- "SN / NI" --> SER - - %% -------------------- - %% Paths from 1.4 - %% -------------------- - Q4 -- "N / PN / NI" --> NC3 - Q4 -- "Y / PY" --> CRIT - - %% -------------------- - %% Negative controls to outcomes - %% -------------------- - NC1 -- "N / PN" --> LOW - NC1 -- "Y / PY" --> MOD - - NC2 -- "N / PN" --> LOW_EX - NC2 -- "Y / PY" --> SER - - NC3 -- "N / PN" --> SER - NC3 -- "Y / PY" --> CRIT -``` diff --git a/packages/landing/src/components/checklist/ROBINSIChecklist/scoring/decision-diagrams/domain-2.md b/packages/landing/src/components/checklist/ROBINSIChecklist/scoring/decision-diagrams/domain-2.md deleted file mode 100644 index 11d90f62..00000000 --- a/packages/landing/src/components/checklist/ROBINSIChecklist/scoring/decision-diagrams/domain-2.md +++ /dev/null @@ -1,72 +0,0 @@ -```mermaid -flowchart LR - %% -------------------- - %% Start - %% -------------------- - A1["2.1 Intervention distinguishable at start of follow-up?"] - - %% -------------------- - %% Early pathway - %% -------------------- - A2["2.2 Almost all outcome events after strategies distinguishable?"] - A3["2.3 Appropriate analysis?"] - - %% -------------------- - %% Classification influenced by outcome - %% -------------------- - C1["2.4 Classification of intervention influenced by outcome?"] - C2["2.4 Classification of intervention influenced by outcome?"] - C3["2.4 Classification of intervention influenced by outcome?"] - - %% -------------------- - %% Further errors - %% -------------------- - E1["2.5 Further classification errors likely?"] - E2["2.5 Further classification errors likely?"] - E3["2.5 Further classification errors likely?"] - - %% -------------------- - %% Outcomes - %% -------------------- - LOW["LOW RISK OF BIAS"] - MOD["MODERATE RISK OF BIAS"] - SER["SERIOUS RISK OF BIAS"] - CRIT["CRITICAL RISK OF BIAS"] - - %% -------------------- - %% Connections - %% -------------------- - A1 -- "Y / PY" --> C1 - A1 -- "N / PN / NI" --> A2 - - A2 -- "Y / PY" --> C1 - A2 -- "N / PN / NI" --> A3 - - A3 -- "SY / WY / NI" --> C2 - A3 -- "N / PN" --> C3 - - %% -------------------- - %% Classification → errors - %% -------------------- - C1 -- "N / PN" --> E1 - C1 -- "WY / NI" --> E2 - C1 -- "SY" --> E3 - - C2 -- "N / PN" --> E2 - C2 -- "SY" --> E3 - - C3 -- "N / PN" --> E3 - C3 -- "SY / WY / NI" --> CRIT - - %% -------------------- - %% Error nodes → outcomes - %% -------------------- - E1 -- "N / PN" --> LOW - E1 -- "Y / PY / NI" --> MOD - - E2 -- "N / PN" --> MOD - E2 -- "Y / PY / NI" --> SER - - E3 -- "N / PN" --> SER - E3 -- "Y / PY / NI" --> CRIT -``` diff --git a/packages/landing/src/components/checklist/ROBINSIChecklist/scoring/decision-diagrams/domain-3.md b/packages/landing/src/components/checklist/ROBINSIChecklist/scoring/decision-diagrams/domain-3.md deleted file mode 100644 index 40696864..00000000 --- a/packages/landing/src/components/checklist/ROBINSIChecklist/scoring/decision-diagrams/domain-3.md +++ /dev/null @@ -1,76 +0,0 @@ -```mermaid -flowchart LR - %% -------------------- - %% Section A - %% -------------------- - subgraph A["A. Follow-up and early outcomes"] - A1["3.1 Participants followed from start of intervention?"] - - A2["3.2 Early outcome events excluded?"] - - A1 -- "Y / PY" --> A2 - A1 -- "WN / NI" --> A_MOD["MODERATE"] - A1 -- "SN" --> A_SER["SERIOUS"] - - A2 -- "N / PN / NI" --> A_LOW["LOW"] - A2 -- "Y / PY" --> A_MOD - end - - %% -------------------- - %% Section B - %% -------------------- - subgraph B["B. Selection bias"] - B1["3.3 Selection based on characteristics after start?"] - B2["3.4 Selection variables associated with intervention?"] - B3["3.5 Selection variables influenced by outcome?"] - - B1 -- "N / PN" --> B_LOW1["LOW"] - B1 -- "Y / PY" --> B2 - B1 -- "NI" --> B_MOD1["MODERATE"] - - B2 -- "N / PN" --> B_LOW2["LOW"] - B2 -- "Y / PY" --> B3 - B2 -- "NI" --> B_MOD2["MODERATE"] - - B3 -- "N / PN / NI" --> B_MOD3["MODERATE"] - B3 -- "Y / PY" --> B_SER["SERIOUS"] - end - - %% -------------------- - %% Combine A and B - %% -------------------- - subgraph AB["Across A and B"] - AB_LOW["All LOW"] - AB_MOD["At worst MODERATE"] - AB_SER["At least one SERIOUS"] - end - - A_LOW --> AB_LOW - B_LOW1 --> AB_LOW - B_LOW2 --> AB_LOW - - A_MOD --> AB_MOD - B_MOD1 --> AB_MOD - B_MOD2 --> AB_MOD - B_MOD3 --> AB_MOD - - A_SER --> AB_SER - B_SER --> AB_SER - - %% -------------------- - %% Final adjustments - %% -------------------- - AB_LOW --> LOW_RISK["LOW RISK OF BIAS"] - AB_MOD --> MOD_RISK["MODERATE RISK OF BIAS"] - - AB_SER --> C1["3.6 Analysis corrected for selection biases?"] - - C1 -- "Y / PY" --> MOD_RISK - C1 -- "N / PN / NI" --> C2["3.7 Sensitivity analyses demonstrate minimal impact?"] - - C2 -- "Y / PY" --> MOD_RISK - C2 -- "N / PN / NI" --> C3["3.8 Selection biases severe?"] - - C3 -- "N / PN / NI" --> SER_RISK["SERIOUS RISK OF BIAS"] - C3 -- "Y / PY" --> CRIT_RISK["CRITICAL RISK OF BIAS"] -``` diff --git a/packages/landing/src/components/checklist/ROBINSIChecklist/scoring/decision-diagrams/domain-4.md b/packages/landing/src/components/checklist/ROBINSIChecklist/scoring/decision-diagrams/domain-4.md deleted file mode 100644 index 985ea70a..00000000 --- a/packages/landing/src/components/checklist/ROBINSIChecklist/scoring/decision-diagrams/domain-4.md +++ /dev/null @@ -1,45 +0,0 @@ -````mermaid -flowchart TD - A[4.1–4.3 Complete data for all participants?] - - A -->|All Y/PY| LOW1[LOW RISK OF BIAS] - A -->|Any N/PN/NI| B[4.4 Complete-case analysis?] - - %% Complete-case path - B -->|Y/PY/NI| C[4.5 Exclusion related to true outcome?] - B -->|N/PN| D[4.7 Analysis based on imputation?] - - C -->|N/PN| LOW2[LOW RISK OF BIAS] - C -->|Y/PY/NI| E[4.6 Outcome–missingness relationship explained by model?] - - E -->|Y/PY| F1[4.11 Evidence result not biased?] - E -->|WN/NI| F2[4.11 Evidence result not biased?] - E -->|SN| F3[4.11 Evidence result not biased?] - - %% Imputation path - D -->|Y/PY| G[4.8 MAR/MCAR reasonable?] - D -->|N/PN/NI| H[4.10 Alternative appropriate method?] - - G -->|Y/PY| I[4.9 Appropriate imputation?] - G -->|N/PN/NI| F2 - - I -->|Y/PY| LOW3[LOW RISK OF BIAS] - I -->|WN/NI| F2 - I -->|SN| F3 - - %% Alternative method path - H -->|Y/PY| LOW4[LOW RISK OF BIAS] - H -->|WN/NI| F2 - H -->|SN| F3 - - %% Final evidence checks - F1 -->|Y/PY| MOD1[MODERATE RISK OF BIAS] - F1 -->|N/PN| SER1[SERIOUS RISK OF BIAS] - - F2 -->|Y/PY| MOD2[MODERATE RISK OF BIAS] - F2 -->|N/PN| SER2[SERIOUS RISK OF BIAS] - - F3 -->|Y/PY| SER3[SERIOUS RISK OF BIAS] - F3 -->|N/PN| CRIT[CRITICAL RISK OF BIAS] - ``` -```` diff --git a/packages/landing/src/components/checklist/ROBINSIChecklist/scoring/decision-diagrams/domain-5.md b/packages/landing/src/components/checklist/ROBINSIChecklist/scoring/decision-diagrams/domain-5.md deleted file mode 100644 index 692487eb..00000000 --- a/packages/landing/src/components/checklist/ROBINSIChecklist/scoring/decision-diagrams/domain-5.md +++ /dev/null @@ -1,52 +0,0 @@ -```mermaid -flowchart LR - %% -------------------- - %% Core questions - %% -------------------- - Q1["5.1 Measurement of outcome differs by intervention?"] - - Q2a["5.2 Outcome assessors aware of intervention received?"] - Q2b["5.2 Outcome assessors aware of intervention received?"] - - Q3a["5.3 Assessment could be influenced by knowledge of intervention?"] - Q3b["5.3 Assessment could be influenced by knowledge of intervention?"] - - %% -------------------- - %% Outcomes - %% -------------------- - LOW["LOW RISK OF BIAS"] - MOD["MODERATE RISK OF BIAS"] - SER["SERIOUS RISK OF BIAS"] - - %% -------------------- - %% From 5.1 - %% -------------------- - Q1 -- "N / PN" --> Q2a - Q1 -- "NI" --> Q2b - Q1 -- "Y / PY" --> SER - - %% -------------------- - %% From 5.2 (top) - %% -------------------- - Q2a -- "N / PN" --> LOW - Q2a -- "Y / PY / NI" --> Q3a - - %% -------------------- - %% From 5.2 (middle) - %% -------------------- - Q2b -- "N / PN" --> MOD - Q2b -- "Y / PY / NI" --> Q3b - - %% -------------------- - %% From 5.3 (top) - %% -------------------- - Q3a -- "N / PN" --> LOW - Q3a -- "WY / NI" --> MOD - Q3a -- "SY" --> SER - - %% -------------------- - %% From 5.3 (middle) - %% -------------------- - Q3b -- "WY / N / PN / NI" --> MOD - Q3b -- "SY" --> SER -``` diff --git a/packages/landing/src/components/checklist/ROBINSIChecklist/scoring/decision-diagrams/domain-6.md b/packages/landing/src/components/checklist/ROBINSIChecklist/scoring/decision-diagrams/domain-6.md deleted file mode 100644 index 34bfae06..00000000 --- a/packages/landing/src/components/checklist/ROBINSIChecklist/scoring/decision-diagrams/domain-6.md +++ /dev/null @@ -1,38 +0,0 @@ -```mermaid -flowchart LR - %% -------------------- - %% Core questions - %% -------------------- - Q1["6.1 Result reported according to analysis plan?"] - - Q2["6.2 Multiple outcome measurements?"] - Q3["6.3 Multiple analyses of the data?"] - Q4["6.4 Multiple subgroups?"] - - %% -------------------- - %% Aggregated decision node - %% -------------------- - SEL["Result selected from:\n6.2 / 6.3 / 6.4"] - - %% -------------------- - %% Outcomes - %% -------------------- - LOW["LOW RISK OF BIAS"] - MOD["MODERATE RISK OF BIAS"] - SER["SERIOUS RISK OF BIAS"] - CRIT["CRITICAL RISK OF BIAS"] - - %% -------------------- - %% From 6.1 - %% -------------------- - Q1 -- "Y / PY" --> LOW - Q1 -- "N / PN / NI" --> SEL - - %% -------------------- - %% From selection set (6.2–6.4) - %% -------------------- - SEL -- "All N / PN" --> LOW - SEL -- "At least one NI,\nbut none Y / PY" --> MOD - SEL -- "One Y / PY,\nor all NI" --> SER - SEL -- "Two or more Y / PY" --> CRIT -``` diff --git a/packages/landing/src/components/ui/alert-dialog.tsx b/packages/landing/src/components/ui/alert-dialog.tsx deleted file mode 100644 index 659dc163..00000000 --- a/packages/landing/src/components/ui/alert-dialog.tsx +++ /dev/null @@ -1,228 +0,0 @@ -import * as React from 'react'; -import { AlertDialog as AlertDialogPrimitive } from 'radix-ui'; -import { TriangleAlertIcon, InfoIcon, AlertCircleIcon } from 'lucide-react'; - -import { cn } from '@/lib/utils'; -import { Button } from '@/components/ui/button'; - -function AlertDialog({ ...props }: React.ComponentProps) { - return ; -} - -function AlertDialogTrigger({ - ...props -}: React.ComponentProps) { - return ; -} - -function AlertDialogPortal({ ...props }: React.ComponentProps) { - return ; -} - -function AlertDialogOverlay({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function AlertDialogContent({ - className, - size = 'default', - ...props -}: React.ComponentProps & { - size?: 'default' | 'sm'; -}) { - return ( - - - - - ); -} - -function AlertDialogHeader({ className, ...props }: React.ComponentProps<'div'>) { - return ( -
- ); -} - -function AlertDialogFooter({ className, ...props }: React.ComponentProps<'div'>) { - return ( -
- ); -} - -function AlertDialogMedia({ className, ...props }: React.ComponentProps<'div'>) { - return ( -
- ); -} - -function AlertDialogTitle({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function AlertDialogDescription({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function AlertDialogAction({ - className, - variant = 'default', - size = 'default', - ...props -}: React.ComponentProps & - Pick, 'variant' | 'size'>) { - return ( - - ); -} - -function AlertDialogCancel({ - className, - variant = 'outline', - size = 'default', - ...props -}: React.ComponentProps & - Pick, 'variant' | 'size'>) { - return ( - - ); -} - -/** - * AlertDialogIcon - Variant-colored icon container for alert dialogs. - * Wraps an icon with colored background based on severity. - * - * @example - * - * - * - */ -function AlertDialogIcon({ - className, - variant = 'danger', - children, - ...props -}: React.ComponentProps<'div'> & { - variant?: 'danger' | 'warning' | 'info'; -}) { - const variantStyles = { - danger: 'bg-destructive/10 text-destructive', - warning: 'bg-warning-bg text-warning', - info: 'bg-info-bg text-info', - }; - - const defaultIcons = { - danger: TriangleAlertIcon, - warning: AlertCircleIcon, - info: InfoIcon, - }; - - const DefaultIcon = defaultIcons[variant]; - - return ( -
- {children || } -
- ); -} - -export { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogIcon, - AlertDialogMedia, - AlertDialogOverlay, - AlertDialogPortal, - AlertDialogTitle, - AlertDialogTrigger, -}; diff --git a/packages/landing/src/components/ui/avatar.tsx b/packages/landing/src/components/ui/avatar.tsx deleted file mode 100644 index 0dd28301..00000000 --- a/packages/landing/src/components/ui/avatar.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import * as React from 'react'; -import { Avatar as AvatarPrimitive } from 'radix-ui'; - -import { cn } from '@/lib/utils'; - -function Avatar({ - className, - size = 'default', - ...props -}: React.ComponentProps & { - size?: 'default' | 'sm' | 'lg'; -}) { - return ( - - ); -} - -function AvatarImage({ className, ...props }: React.ComponentProps) { - return ( - - ); -} - -function AvatarFallback({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function AvatarBadge({ className, ...props }: React.ComponentProps<'span'>) { - return ( - svg]:hidden', - 'group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2', - 'group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2', - className, - )} - {...props} - /> - ); -} - -function AvatarGroup({ className, ...props }: React.ComponentProps<'div'>) { - return ( -
- ); -} - -function AvatarGroupCount({ className, ...props }: React.ComponentProps<'div'>) { - return ( -
svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3', - className, - )} - {...props} - /> - ); -} - -/** - * Helper to generate initials from a name - */ -function getInitials(name?: string): string { - if (!name) return ''; - const parts = name.trim().split(' ').filter(Boolean); - if (parts.length === 0) return ''; - if (parts.length === 1) return (parts[0]?.[0] ?? '').toUpperCase(); - return ((parts[0]?.[0] ?? '') + (parts[parts.length - 1]?.[0] ?? '')).toUpperCase(); -} - -/** - * Convenience component for user avatars with auto-generated initials. - * - * @example - * - * - */ -function UserAvatar({ - src, - name, - alt, - className, - size, -}: { - src?: string; - name?: string; - alt?: string; - className?: string; - size?: 'default' | 'sm' | 'lg'; -}) { - return ( - - - {getInitials(name)} - - ); -} - -export { - Avatar, - AvatarImage, - AvatarFallback, - AvatarGroup, - AvatarGroupCount, - AvatarBadge, - UserAvatar, - getInitials, -}; diff --git a/packages/landing/src/components/ui/button.tsx b/packages/landing/src/components/ui/button.tsx deleted file mode 100644 index a8bbed26..00000000 --- a/packages/landing/src/components/ui/button.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import * as React from 'react'; -import { cva, type VariantProps } from 'class-variance-authority'; -import { Slot } from 'radix-ui'; - -import { cn } from '@/lib/utils'; - -const buttonVariants = cva( - "group/button inline-flex shrink-0 items-center justify-center rounded-md border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", - { - variants: { - variant: { - default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80', - outline: - 'border-border bg-card text-secondary-foreground hover:bg-muted aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50', - secondary: - 'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground', - ghost: - 'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50', - destructive: - 'bg-destructive text-destructive-foreground hover:bg-destructive/80 focus-visible:border-destructive/40 focus-visible:ring-destructive/20', - link: 'text-primary underline-offset-4 hover:underline', - }, - size: { - default: - 'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2', - xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", - sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", - lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3', - icon: 'size-8', - 'icon-xs': - "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3", - 'icon-sm': - 'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg', - 'icon-lg': 'size-9', - }, - }, - defaultVariants: { - variant: 'default', - size: 'default', - }, - }, -); - -function Button({ - className, - variant = 'default', - size = 'default', - asChild = false, - ...props -}: React.ComponentProps<'button'> & - VariantProps & { - asChild?: boolean; - }) { - const Comp = asChild ? Slot.Root : 'button'; - - return ( - - ); -} - -export { Button, buttonVariants }; diff --git a/packages/landing/src/components/ui/checkbox.tsx b/packages/landing/src/components/ui/checkbox.tsx deleted file mode 100644 index 3621d46a..00000000 --- a/packages/landing/src/components/ui/checkbox.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import * as React from 'react'; -import { Checkbox as CheckboxPrimitive } from 'radix-ui'; - -import { cn } from '@/lib/utils'; -import { CheckIcon } from 'lucide-react'; - -function Checkbox({ className, ...props }: React.ComponentProps) { - return ( - - - - - - ); -} - -export { Checkbox }; diff --git a/packages/landing/src/components/ui/collapsible.tsx b/packages/landing/src/components/ui/collapsible.tsx deleted file mode 100644 index 1808acc9..00000000 --- a/packages/landing/src/components/ui/collapsible.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Collapsible as CollapsiblePrimitive } from 'radix-ui'; -import { cn } from '@/lib/utils'; - -function Collapsible({ ...props }: React.ComponentProps) { - return ; -} - -function CollapsibleTrigger({ - ...props -}: React.ComponentProps) { - return ; -} - -function CollapsibleContent({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -export { Collapsible, CollapsibleTrigger, CollapsibleContent }; diff --git a/packages/landing/src/components/ui/dialog.tsx b/packages/landing/src/components/ui/dialog.tsx deleted file mode 100644 index 2b69f6e7..00000000 --- a/packages/landing/src/components/ui/dialog.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import * as React from 'react'; -import { Dialog as DialogPrimitive } from 'radix-ui'; - -import { cn } from '@/lib/utils'; -import { Button } from '@/components/ui/button'; -import { XIcon } from 'lucide-react'; - -function Dialog({ ...props }: React.ComponentProps) { - return ; -} - -function DialogTrigger({ ...props }: React.ComponentProps) { - return ; -} - -function DialogPortal({ ...props }: React.ComponentProps) { - return ; -} - -function DialogClose({ ...props }: React.ComponentProps) { - return ; -} - -function DialogOverlay({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function DialogContent({ - className, - children, - showCloseButton = true, - ...props -}: React.ComponentProps & { - showCloseButton?: boolean; -}) { - return ( - - - - {children} - {showCloseButton && ( - - - - )} - - - ); -} - -function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) { - return ( -
- ); -} - -function DialogFooter({ - className, - showCloseButton = false, - children, - ...props -}: React.ComponentProps<'div'> & { - showCloseButton?: boolean; -}) { - return ( -
- {children} - {showCloseButton && ( - - - - )} -
- ); -} - -function DialogTitle({ className, ...props }: React.ComponentProps) { - return ( - - ); -} - -function DialogDescription({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -export { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogOverlay, - DialogPortal, - DialogTitle, - DialogTrigger, -}; diff --git a/packages/landing/src/components/ui/editable.tsx b/packages/landing/src/components/ui/editable.tsx deleted file mode 100644 index e916df8a..00000000 --- a/packages/landing/src/components/ui/editable.tsx +++ /dev/null @@ -1,210 +0,0 @@ -/** - * Editable - Inline editable text component (@ark-ui/react) - * - * @example - * - * - * @example - * - * - * - * - * - * - */ - -import * as React from 'react'; -import { Editable as EditablePrimitive } from '@ark-ui/react/editable'; -import { CheckIcon, XIcon, PencilIcon } from 'lucide-react'; -import { cn } from '@/lib/utils'; - -const variants = { - default: { - area: 'px-2 py-1 rounded transition-colors hover:bg-secondary focus-within:ring-1 focus-within:ring-primary focus-within:border-primary border border-transparent', - input: 'outline-none bg-transparent', - preview: 'cursor-pointer', - }, - inline: { - area: 'border-b border-transparent focus-within:border-primary transition-colors', - input: 'outline-none bg-transparent', - preview: 'cursor-pointer hover:text-primary transition-colors', - }, - heading: { - area: 'rounded transition-colors hover:bg-muted focus-within:bg-muted', - input: 'outline-none bg-transparent', - preview: 'cursor-pointer', - }, - field: { - area: 'px-3 py-2 border border-border rounded-md transition-colors hover:border-border/80 focus-within:border-primary focus-within:ring-1 focus-within:ring-primary', - input: 'outline-none bg-transparent', - preview: 'cursor-pointer', - }, -} as const; - -type EditableVariant = keyof typeof variants; - -interface SimpleEditableProps { - value?: string; - defaultValue?: string; - placeholder?: string; - onChange?: (value: string) => void; - onSubmit?: (value: string) => void; - onCancel?: () => void; - disabled?: boolean; - readOnly?: boolean; - autoResize?: boolean; - activationMode?: 'focus' | 'dblclick' | 'click' | 'none'; - submitMode?: 'enter' | 'blur' | 'none' | 'both'; - selectOnFocus?: boolean; - maxLength?: number; - variant?: EditableVariant; - className?: string; - areaClassName?: string; - inputClassName?: string; - previewClassName?: string; - showControls?: boolean; - showEditIcon?: boolean; - label?: string; -} - -function SimpleEditable({ - value, - defaultValue, - placeholder = 'Click to edit...', - onChange, - onSubmit, - onCancel, - disabled, - readOnly, - autoResize = true, - activationMode = 'dblclick', - submitMode = 'both', - selectOnFocus = true, - maxLength, - variant = 'default', - className, - areaClassName, - inputClassName, - previewClassName, - showControls = false, - showEditIcon = false, - label, -}: SimpleEditableProps) { - const variantStyles = variants[variant] || variants.default; - const initialValue = value ?? defaultValue ?? ''; - - return ( - onChange?.(details.value)} - onValueCommit={details => onSubmit?.(details.value)} - onValueRevert={() => onCancel?.()} - className={cn('group inline-block', className)} - > - - {api => ( - <> - {label && ( - - {label} - - )} - -
- - - - - - {showControls && ( -
- {api.editing ? - <> - - - - - - - - : - - - } -
- )} - - {showEditIcon && !showControls && !api.editing && ( - - - - )} -
- - )} -
-
- ); -} - -function EditableTextarea({ className, ...props }: React.ComponentProps<'textarea'>) { - return ( - -