diff --git a/.cursor/mcp.json b/.cursor/mcp.json
index aacf47805..c22c455e0 100644
--- a/.cursor/mcp.json
+++ b/.cursor/mcp.json
@@ -5,6 +5,10 @@
"command": "node",
"args": ["/Users/jacobmaynard/Documents/Repos/corates/packages/mcp/server.js"],
"env": {}
+ },
+ "ark-ui": {
+ "command": "npx",
+ "args": ["-y", "@ark-ui/mcp"]
}
}
}
diff --git a/.cursor/rules/corates.mdc b/.cursor/rules/corates.mdc
index 492a16dfc..07bdaa2e9 100644
--- a/.cursor/rules/corates.mdc
+++ b/.cursor/rules/corates.mdc
@@ -15,7 +15,7 @@ The project is split into multiple packages under the `packages/` directory:
- `/web`: Frontend application built with SolidJS
- `/workers`: Backend services, API endpoints, and database migrations
- `/landing`: Marketing and landing site
-- `/ui`: Shared UI component library built with Zag.js
+- `/ui`: Shared UI component library built with Ark UI
- `/shared`: Shared TypeScript utilities and error definitions
- `/mcp`: MCP server for development tools and documentation
@@ -48,7 +48,7 @@ Do not worry about migrations (client side or backend) unless specifically instr
- **Zod**: Schema and input validation (backend)
- **Drizzle ORM**: ALL database interactions and migrations
- **Better-Auth**: Authentication and user management
-- **Zag.js**: UI components from `@corates/ui` package (NOT from local components)
+- **Ark UI**: UI components from `@corates/ui` package
- **solid-icons**: Icon library (e.g., `solid-icons/bi`, `solid-icons/fi`)
### Code Comments
@@ -81,14 +81,14 @@ retries += 1
### UI Components
-**Zag components are in `@corates/ui` package, NOT in local components.**
+**Ark UI components are in `@corates/ui` package, NOT in local components.**
```js
// CORRECT
import { Dialog, Select, Toast } from '@corates/ui';
// WRONG
-import { Dialog } from '@/components/zag/Dialog.jsx';
+import { Dialog } from '@/components/ark/Dialog.jsx';
```
See `ui-components.mdc` for detailed component usage patterns.
@@ -112,7 +112,7 @@ See `solidjs.mdc` for detailed reactivity patterns and examples.
## Documentation
-- **ALWAYS use Corates MCP tools** for Better-Auth, Drizzle, Icons, linting, and Zag documentation
+- **ALWAYS use Corates MCP tools or other MCP** for Better-Auth, Drizzle, Icons, linting, and Ark UI documentation
- Reference `style-guide.md` for UI styling guidelines
- Reference `docs/error-handling-guide.md` for error handling patterns
- See `TESTING.md` for testing guidelines (do NOT add tests unless asked)
diff --git a/.cursor/rules/ui-components.mdc b/.cursor/rules/ui-components.mdc
index 257e1c152..daa0605af 100644
--- a/.cursor/rules/ui-components.mdc
+++ b/.cursor/rules/ui-components.mdc
@@ -1,14 +1,14 @@
---
alwaysApply: false
-description: "UI component usage patterns including Zag.js components, solid-icons, and import aliases"
+description: "UI component usage patterns including Ark UI components, solid-icons, and import aliases"
globs: packages/web/**, packages/ui/**
---
# UI Components
-## Zag.js Components
+## Ark UI Components
-**Zag components are in `@corates/ui` package, NOT in local components.**
+**UI components are in `@corates/ui` package, built with Ark UI (and some remaining Zag.js components). NOT in local components.**
```js
// CORRECT - Import from @corates/ui
@@ -21,7 +21,7 @@ import { Dialog } from 'packages/web/src/components/zag/Dialog.jsx';
### Available Components
-See `packages/ui/src/zag/index.js` for all available components. Common ones:
+See `packages/ui/src/zag/index.js` for all available components. Most components have been migrated to Ark UI. Common ones:
- `Dialog`, `ConfirmDialog`, `useConfirmDialog`
- `Select`, `Combobox`
- `Toast`, `toaster`, `showToast`
diff --git a/README.md b/README.md
index 34008cd92..633de6d2d 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ CoRATES is a web application designed to streamline the entire quality and risk-
## Tech Stack
-- **Frontend**: SolidJS, SolidStart, Tailwind CSS, Vite, Zag
+- **Frontend**: SolidJS, SolidStart, Tailwind CSS, Vite, Ark UI
- **Backend**: Cloudflare Workers, Durable Objects
- **Database**: Cloudflare D1 (SQLite)
- **Storage**: Cloudflare R2 (PDF documents)
diff --git a/docs/architecture/diagrams.md b/docs/architecture/diagrams.md
index 5853b7149..a4b373d35 100644
--- a/docs/architecture/diagrams.md
+++ b/docs/architecture/diagrams.md
@@ -10,11 +10,12 @@ Mermaid diagrams to help new contributors understand the CoRATES application arc
## Diagrams
-| # | Diagram | Description |
-| --- | ----------------------------------------------------------- | -------------------------------------------- |
-| 1 | [Package Architecture](diagrams/01-package-architecture.md) | Monorepo structure and package relationships |
-| 2 | [System Architecture](diagrams/02-system-architecture.md) | Frontend, backend, and storage layers |
-| 3 | [Sync Flow](diagrams/03-sync-flow.md) | Local-first CRDT sync with Yjs |
-| 4 | [Data Model](diagrams/04-data-model.md) | Entity relationships and storage |
-| 5 | [Frontend Routes](diagrams/05-frontend-routes.md) | Application routing structure |
-| 6 | [API Routes](diagrams/06-api-routes.md) | Backend endpoints and middleware |
+| # | Diagram | Description |
+| --- | ----------------------------------------------------------- | -------------------------------------------------------------------- |
+| 1 | [Package Architecture](diagrams/01-package-architecture.md) | Monorepo structure and package relationships |
+| 2 | [System Architecture](diagrams/02-system-architecture.md) | Frontend, backend, and storage layers |
+| 3 | [Sync Flow](diagrams/03-sync-flow.md) | Local-first CRDT sync with Yjs |
+| 4 | [Data Model](diagrams/04-data-model.md) | Entity relationships and storage |
+| 5 | [Frontend Routes](diagrams/05-frontend-routes.md) | Application routing structure |
+| 6 | [API Routes](diagrams/06-api-routes.md) | Backend endpoints and middleware |
+| 7 | [API Actions](diagrams/07-api-actions.md) | All actions and failure points for projects, studies, and checklists |
diff --git a/docs/architecture/diagrams/01-package-architecture.md b/docs/architecture/diagrams/01-package-architecture.md
index bd6d06ea8..b4e67f144 100644
--- a/docs/architecture/diagrams/01-package-architecture.md
+++ b/docs/architecture/diagrams/01-package-architecture.md
@@ -36,6 +36,6 @@ graph TB
| `web` | Main SolidJS application | SolidJS, Vite, Tailwind |
| `workers` | Backend API and real-time sync | Hono, Cloudflare Workers |
| `landing` | Marketing site (includes web app) | SolidStart |
-| `ui` | Shared component library | SolidJS, Zag.js |
+| `ui` | Shared component library | SolidJS, Ark UI |
| `shared` | Shared error definitions and utilities | TypeScript |
| `mcp` | Development tooling (docs, linting) | Node.js |
diff --git a/docs/architecture/diagrams/02-system-architecture.md b/docs/architecture/diagrams/02-system-architecture.md
index 057772f94..edb755873 100644
--- a/docs/architecture/diagrams/02-system-architecture.md
+++ b/docs/architecture/diagrams/02-system-architecture.md
@@ -44,7 +44,7 @@ flowchart TB
### Frontend (SolidJS)
-- **UI Components**: Zag.js-based accessible components
+- **UI Components**: Ark UI-based accessible components
- **Stores**: Centralized state management (no prop drilling)
- **Yjs Client**: CRDT sync with local IndexedDB persistence for project content
- **Notification WebSocket**: Real-time connection to UserSession for user-level notifications (project invites, etc.)
diff --git a/docs/architecture/diagrams/07-api-actions.md b/docs/architecture/diagrams/07-api-actions.md
new file mode 100644
index 000000000..054f9e4ed
--- /dev/null
+++ b/docs/architecture/diagrams/07-api-actions.md
@@ -0,0 +1,725 @@
+# API Actions and Failure Points
+
+Comprehensive diagrams showing all actions that can be performed on projects, studies, and checklists, including all failure points and error conditions.
+
+## Overview
+
+This document visualizes the complete API surface for the three main entities in CoRATES:
+
+- **Projects**: Backend API routes + Y.js sync operations
+- **Studies**: Y.js operations + PDF management via backend API
+- **Checklists**: Y.js operations only
+
+Each diagram shows:
+
+- All CRUD operations
+- Permission and validation checks
+- External dependencies (D1, R2, Durable Objects)
+- All failure points with error codes
+- Network and sync failures
+
+---
+
+## Project Actions
+
+Projects are managed through both HTTP API endpoints and Y.js synchronization. The backend API handles CRUD operations and member management, while Y.js syncs metadata and member changes to Durable Objects.
+
+```mermaid
+flowchart TB
+ Start([User Action]) --> Auth{Authenticated?}
+ Auth -->|No| AuthError[AUTH_REQUIRED
401]
+ Auth -->|Yes| Action{Action Type}
+
+ Action -->|Create| CreateFlow
+ Action -->|Read| ReadFlow
+ Action -->|Update| UpdateFlow
+ Action -->|Delete| DeleteFlow
+ Action -->|Member| MemberFlow
+
+ subgraph CreateFlow["Create Project"]
+ CreateStart[POST /api/projects] --> Entitlement{Has project.create
entitlement?}
+ Entitlement -->|No| EntitlementError[AUTH_FORBIDDEN
403
missing_entitlement]
+ Entitlement -->|Yes| QuotaCheck{Under projects.max
quota?}
+ QuotaCheck -->|No| QuotaError[AUTH_FORBIDDEN
403
quota_exceeded]
+ QuotaCheck -->|Yes| ValidateInput{Valid name &
description?}
+ ValidateInput -->|No| ValidationError[VALIDATION_ERRORS
400]
+ ValidateInput -->|Yes| CreateDB[Insert into D1
projects + projectMembers]
+ CreateDB -->|DB Error| DBError[SYSTEM_DB_TRANSACTION_FAILED
500]
+ CreateDB -->|Success| SyncDO[Sync to Durable Object
meta + members]
+ SyncDO -->|Sync Failed| SyncError[Log error
Continue]
+ SyncDO -->|Success| CreateSuccess[201 Created
Return project]
+ end
+
+ subgraph ReadFlow["Read Project"]
+ ReadStart[GET /api/projects/:id] --> CheckMembership{User is
member?}
+ CheckMembership -->|No| NotFoundError[PROJECT_NOT_FOUND
404]
+ CheckMembership -->|Yes| QueryDB[Query D1
projects + projectMembers]
+ QueryDB -->|DB Error| ReadDBError[SYSTEM_DB_ERROR
500]
+ QueryDB -->|Success| ReadSuccess[200 OK
Return project]
+ end
+
+ subgraph UpdateFlow["Update Project"]
+ UpdateStart[PUT /api/projects/:id] --> ValidateUpdate{Valid name &
description?}
+ ValidateUpdate -->|No| UpdateValidationError[VALIDATION_ERRORS
400]
+ ValidateUpdate -->|Yes| CheckEditRole{User has edit
role? owner/collaborator}
+ CheckEditRole -->|No| UpdateForbidden[AUTH_FORBIDDEN
403
Only owners and collaborators]
+ CheckEditRole -->|Yes| UpdateDB[Update D1 projects]
+ UpdateDB -->|DB Error| UpdateDBError[SYSTEM_DB_ERROR
500]
+ UpdateDB -->|Success| UpdateSyncDO[Sync meta to DO]
+ UpdateSyncDO -->|Sync Failed| UpdateSyncError[Log error
Continue]
+ UpdateSyncDO -->|Success| UpdateSuccess[200 OK
Success response]
+ end
+
+ subgraph DeleteFlow["Delete Project"]
+ DeleteStart[DELETE /api/projects/:id] --> CheckOwner{User is
owner?}
+ CheckOwner -->|No| DeleteForbidden[AUTH_FORBIDDEN
403
Only owners can delete]
+ CheckOwner -->|Yes| GetMembers[Get all members
for notifications]
+ GetMembers -->|DB Error| DeleteDBError1[SYSTEM_DB_ERROR
500]
+ GetMembers -->|Success| DisconnectDO[Disconnect all users
from ProjectDoc DO]
+ DisconnectDO -->|Failed| DisconnectError[Log error
Continue]
+ DisconnectDO -->|Success| CleanupR2[Delete all PDFs
from R2 storage]
+ CleanupR2 -->|Failed| R2Error[Log error
Continue]
+ CleanupR2 -->|Success| DeleteDB[Delete from D1
projects cascade]
+ DeleteDB -->|DB Error| DeleteDBError2[SYSTEM_DB_ERROR
500]
+ DeleteDB -->|Success| NotifyMembers[Send notifications
to all members]
+ NotifyMembers -->|Some Failed| NotifyError[Log errors
Continue]
+ NotifyMembers -->|Success| DeleteSuccess[200 OK
Success response]
+ end
+
+ subgraph MemberFlow["Member Management"]
+ MemberAction{Member Action}
+ MemberAction -->|List| ListMembers[GET /api/projects/:id/members]
+ MemberAction -->|Add| AddMemberFlow
+ MemberAction -->|Update Role| UpdateRoleFlow
+ MemberAction -->|Remove| RemoveMemberFlow
+
+ ListMembers --> ListDB[Query D1
projectMembers + user]
+ ListDB -->|DB Error| ListDBError[SYSTEM_DB_ERROR
500]
+ ListDB -->|Success| ListSuccess[200 OK
Return members]
+
+ subgraph AddMemberFlow["Add Member"]
+ AddStart[POST /api/projects/:id/members] --> CheckOwnerAdd{User is
owner?}
+ CheckOwnerAdd -->|No| AddForbidden[AUTH_FORBIDDEN
403
Only owners can add]
+ CheckOwnerAdd -->|Yes| ValidateMember{Valid userId
or email?}
+ ValidateMember -->|No| MemberValidationError[VALIDATION_ERRORS
400]
+ ValidateMember -->|Yes| FindUser[Find user by
userId or email]
+ FindUser -->|Not Found| UserNotFound[USER_NOT_FOUND
404]
+ FindUser -->|Found| CheckExisting{Already
member?}
+ CheckExisting -->|Yes| MemberExists[PROJECT_MEMBER_ALREADY_EXISTS
409]
+ CheckExisting -->|No| InsertMember[Insert into D1
projectMembers]
+ InsertMember -->|DB Error| AddDBError[SYSTEM_DB_ERROR
500]
+ InsertMember -->|Success| NotifyUser[Send notification
via UserSession DO]
+ NotifyUser -->|Failed| NotifyUserError[Log error
Continue]
+ NotifyUser -->|Success| SyncMemberDO[Sync member to DO]
+ SyncMemberDO -->|Failed| SyncMemberError[Log error
Continue]
+ SyncMemberDO -->|Success| AddSuccess[201 Created
Return member]
+ end
+
+ subgraph UpdateRoleFlow["Update Member Role"]
+ UpdateRoleStart[PUT /api/projects/:id/members/:userId] --> CheckOwnerRole{User is
owner?}
+ CheckOwnerRole -->|No| UpdateRoleForbidden[AUTH_FORBIDDEN
403
Only owners can update]
+ CheckOwnerRole -->|Yes| ValidateRole{Valid role?}
+ ValidateRole -->|No| RoleValidationError[VALIDATION_ERRORS
400]
+ ValidateRole -->|Yes| CheckLastOwner{Removing last
owner?}
+ CheckLastOwner -->|Yes| LastOwnerError[PROJECT_LAST_OWNER
400
Assign another owner first]
+ CheckLastOwner -->|No| UpdateRoleDB[Update D1
projectMembers.role]
+ UpdateRoleDB -->|DB Error| UpdateRoleDBError[SYSTEM_DB_ERROR
500]
+ UpdateRoleDB -->|Success| SyncRoleDO[Sync role to DO]
+ SyncRoleDO -->|Failed| SyncRoleError[Log error
Continue]
+ SyncRoleDO -->|Success| UpdateRoleSuccess[200 OK
Success response]
+ end
+
+ subgraph RemoveMemberFlow["Remove Member"]
+ RemoveStart[DELETE /api/projects/:id/members/:userId] --> CheckRemoveAuth{User is owner
or self-removal?}
+ CheckRemoveAuth -->|No| RemoveForbidden[AUTH_FORBIDDEN
403
Only owners can remove]
+ CheckRemoveAuth -->|Yes| CheckTargetExists{Target member
exists?}
+ CheckTargetExists -->|No| RemoveNotFound[PROJECT_NOT_FOUND
404
Member not found]
+ CheckTargetExists -->|Yes| CheckRemoveLastOwner{Removing last
owner?}
+ CheckRemoveLastOwner -->|Yes| RemoveLastOwnerError[PROJECT_LAST_OWNER
400
Assign another owner first]
+ CheckRemoveLastOwner -->|No| RemoveDB[Delete from D1
projectMembers]
+ RemoveDB -->|DB Error| RemoveDBError[SYSTEM_DB_ERROR
500]
+ RemoveDB -->|Success| SyncRemoveDO[Sync removal to DO
forces disconnect]
+ SyncRemoveDO -->|Failed| SyncRemoveError[Log error
Continue]
+ SyncRemoveDO -->|Success| NotifyRemoved{Self-removal?}
+ NotifyRemoved -->|Yes| RemoveSuccess[200 OK
Success response]
+ NotifyRemoved -->|No| NotifyRemovedUser[Send notification
via UserSession DO]
+ NotifyRemovedUser -->|Failed| NotifyRemovedError[Log error
Continue]
+ NotifyRemovedUser -->|Success| RemoveSuccess
+ end
+ end
+
+ style AuthError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style EntitlementError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style QuotaError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style ValidationError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style DBError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style SyncError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style NotFoundError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style ReadDBError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style UpdateValidationError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style UpdateForbidden fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style UpdateDBError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style UpdateSyncError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style DeleteForbidden fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style DeleteDBError1 fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style DeleteDBError2 fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style DisconnectError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style R2Error fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style NotifyError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style AddForbidden fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style MemberValidationError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style UserNotFound fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style MemberExists fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style AddDBError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style NotifyUserError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style SyncMemberError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style UpdateRoleForbidden fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style RoleValidationError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style LastOwnerError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style UpdateRoleDBError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style SyncRoleError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style RemoveForbidden fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style RemoveNotFound fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style RemoveLastOwnerError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style RemoveDBError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style SyncRemoveError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style NotifyRemovedError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+```
+
+---
+
+## Study Actions
+
+Studies are managed entirely through Y.js operations (no direct backend API). PDFs are managed via backend API routes. Studies support metadata extraction, DOI lookups, and Google Drive imports.
+
+```mermaid
+flowchart TB
+ Start([User Action]) --> Connected{Connected to
project?}
+ Connected -->|No| ConnectionError[Not connected to project
Show toast error]
+ Connected -->|Yes| Synced{Synced with
Y.js?}
+ Synced -->|No| SyncError[Y.js not synced
Operation fails]
+ Synced -->|Yes| StudyAction{Study Action}
+
+ StudyAction -->|Create| CreateStudyFlow
+ StudyAction -->|Update| UpdateStudyFlow
+ StudyAction -->|Delete| DeleteStudyFlow
+ StudyAction -->|PDF| PDFFlow
+ StudyAction -->|Import| ImportFlow
+
+ subgraph CreateStudyFlow["Create Study"]
+ CreateStudy[createStudy name, description, metadata] --> CheckYDoc{Y.Doc
available?}
+ CheckYDoc -->|No| CreateYDocError[No Y.Doc
Return null]
+ CheckYDoc -->|Yes| GenerateId[Generate studyId
crypto.randomUUID]
+ GenerateId --> CreateYMap[Create Y.Map for study
Set name, description,
createdAt, updatedAt,
checklists: new Y.Map]
+ CreateYMap --> SetMetadata[Set optional metadata:
originalTitle, firstAuthor,
publicationYear, authors,
journal, doi, abstract, etc.]
+ SetMetadata --> AddToMap[Add to reviews Y.Map
studiesMap.set studyId]
+ AddToMap -->|Y.js Error| YjsError[Y.js operation failed
Return null]
+ AddToMap -->|Success| CreateSuccess[Return studyId]
+ end
+
+ subgraph UpdateStudyFlow["Update Study"]
+ UpdateStudy[updateStudy studyId, updates] --> CheckYDocUpdate{Y.Doc
available?}
+ CheckYDocUpdate -->|No| UpdateYDocError[No Y.Doc
Return]
+ CheckYDocUpdate -->|Yes| GetStudyMap[Get study from
reviews Y.Map]
+ GetStudyMap -->|Not Found| StudyNotFound[Study not found
Return]
+ GetStudyMap -->|Found| UpdateFields[Update fields:
name, description, metadata
Set updatedAt]
+ UpdateFields -->|Y.js Error| UpdateYjsError[Y.js operation failed
Show toast error]
+ UpdateFields -->|Success| UpdateSuccess[Update complete]
+ end
+
+ subgraph DeleteStudyFlow["Delete Study"]
+ DeleteStudy[deleteStudy studyId] --> CheckYDocDelete{Y.Doc
available?}
+ CheckYDocDelete -->|No| DeleteYDocError[No Y.Doc
Show toast error]
+ CheckYDocDelete -->|Yes| GetStudyData[Get study data
from projectStore]
+ GetStudyData -->|Not Found| DeleteNotFound[Study not found
Show toast error]
+ GetStudyData -->|Found| GetPDFs[Get all PDFs
for study]
+ GetPDFs --> DeletePDFs[Delete each PDF
from R2 storage]
+ DeletePDFs -->|Some Failed| PDFDeleteError[Log warnings
Continue]
+ DeletePDFs -->|Success| ClearCache[Clear PDF cache
from IndexedDB]
+ ClearCache -->|Failed| CacheError[Log warning
Continue]
+ ClearCache -->|Success| DeleteFromYjs[Delete from
reviews Y.Map]
+ DeleteFromYjs -->|Y.js Error| DeleteYjsError[Y.js operation failed
Show toast error]
+ DeleteFromYjs -->|Success| DeleteSuccess[Study deleted]
+ end
+
+ subgraph PDFFlow["PDF Operations"]
+ PDFAction{PDF Action}
+ PDFAction -->|Upload| UploadPDFFlow
+ PDFAction -->|Download| DownloadPDFFlow
+ PDFAction -->|Delete| DeletePDFFlow
+ PDFAction -->|List| ListPDFFlow
+
+ subgraph UploadPDFFlow["Upload PDF"]
+ UploadStart[POST /api/projects/:id/studies/:id/pdfs] --> CheckAuth{Authenticated?}
+ CheckAuth -->|No| UploadAuthError[AUTH_REQUIRED
401]
+ CheckAuth -->|Yes| CheckMembership{User is
member?}
+ CheckMembership -->|No| UploadAccessError[PROJECT_ACCESS_DENIED
403]
+ CheckMembership -->|Yes| CheckRole{User role is
viewer?}
+ CheckRole -->|Yes| UploadForbidden[AUTH_FORBIDDEN
403
Insufficient permissions]
+ CheckRole -->|No| CheckSize{File size
check}
+ CheckSize -->|Too Large| SizeError[FILE_TOO_LARGE
413
Exceeds limit]
+ CheckSize -->|OK| ValidateFile{Valid PDF
magic bytes?}
+ ValidateFile -->|No| InvalidTypeError[FILE_INVALID_TYPE
400
Not a valid PDF]
+ ValidateFile -->|Yes| ValidateFileName{Valid file
name?}
+ ValidateFileName -->|No| FileNameError[VALIDATION_FIELD_INVALID_FORMAT
400
Invalid file name]
+ ValidateFileName -->|Yes| CheckDuplicate{File already
exists?}
+ CheckDuplicate -->|Yes| DuplicateError[FILE_ALREADY_EXISTS
409]
+ CheckDuplicate -->|No| UploadR2[Upload to R2
PDF_BUCKET.put]
+ UploadR2 -->|R2 Error| R2UploadError[FILE_UPLOAD_FAILED
500]
+ UploadR2 -->|Success| ExtractMetadata[Extract PDF metadata:
title, DOI, etc.]
+ ExtractMetadata -->|Failed| MetadataError[Log warning
Continue]
+ ExtractMetadata -->|Success| FetchDOIMetadata{DOI found?}
+ FetchDOIMetadata -->|Yes| LookupDOI[Fetch from DOI API]
+ LookupDOI -->|Network Error| DOINetworkError[Network error
Log and continue]
+ LookupDOI -->|Success| AddToStudy[Add PDF metadata
to study via Y.js]
+ FetchDOIMetadata -->|No| AddToStudy
+ AddToStudy -->|Y.js Error| AddYjsError[Y.js operation failed
Rollback: delete PDF]
+ AddToStudy -->|Success| CachePDF[Cache PDF
in IndexedDB]
+ CachePDF -->|Failed| CachePDFError[Log warning
Continue]
+ CachePDF -->|Success| UploadSuccess[200 OK
Return PDF info]
+ end
+
+ subgraph DownloadPDFFlow["Download PDF"]
+ DownloadStart[GET /api/projects/:id/studies/:id/pdfs/:fileName] --> CheckAuthDownload{Authenticated?}
+ CheckAuthDownload -->|No| DownloadAuthError[AUTH_REQUIRED
401]
+ CheckAuthDownload -->|Yes| CheckMembershipDownload{User is
member?}
+ CheckMembershipDownload -->|No| DownloadAccessError[PROJECT_ACCESS_DENIED
403]
+ CheckMembershipDownload -->|Yes| ValidateFileNameDownload{Valid file
name?}
+ ValidateFileNameDownload -->|No| FileNameDownloadError[VALIDATION_FIELD_INVALID_FORMAT
400]
+ ValidateFileNameDownload -->|Yes| GetR2[Get from R2
PDF_BUCKET.get]
+ GetR2 -->|Not Found| NotFoundError[FILE_NOT_FOUND
404]
+ GetR2 -->|R2 Error| R2DownloadError[SYSTEM_INTERNAL_ERROR
500]
+ GetR2 -->|Success| DownloadSuccess[200 OK
Return PDF binary]
+ end
+
+ subgraph DeletePDFFlow["Delete PDF"]
+ DeletePDFStart[DELETE /api/projects/:id/studies/:id/pdfs/:fileName] --> CheckAuthDelete{Authenticated?}
+ CheckAuthDelete -->|No| DeleteAuthError[AUTH_REQUIRED
401]
+ CheckAuthDelete -->|Yes| CheckMembershipDelete{User is
member?}
+ CheckMembershipDelete -->|No| DeleteAccessError[PROJECT_ACCESS_DENIED
403]
+ CheckMembershipDelete -->|Yes| CheckRoleDelete{User role is
viewer?}
+ CheckRoleDelete -->|Yes| DeleteForbidden[AUTH_FORBIDDEN
403
Insufficient permissions]
+ CheckRoleDelete -->|No| ValidateFileNameDelete{Valid file
name?}
+ ValidateFileNameDelete -->|No| FileNameDeleteError[VALIDATION_FIELD_INVALID_FORMAT
400]
+ ValidateFileNameDelete -->|Yes| DeleteR2[Delete from R2
PDF_BUCKET.delete]
+ DeleteR2 -->|R2 Error| R2DeleteError[SYSTEM_INTERNAL_ERROR
500]
+ DeleteR2 -->|Success| DeletePDFSuccess[200 OK
Success response]
+ end
+
+ subgraph ListPDFFlow["List PDFs"]
+ ListPDFStart[GET /api/projects/:id/studies/:id/pdfs] --> CheckAuthList{Authenticated?}
+ CheckAuthList -->|No| ListAuthError[AUTH_REQUIRED
401]
+ CheckAuthList -->|Yes| CheckMembershipList{User is
member?}
+ CheckMembershipList -->|No| ListAccessError[PROJECT_ACCESS_DENIED
403]
+ CheckMembershipList -->|Yes| ListR2[List from R2
PDF_BUCKET.list]
+ ListR2 -->|R2 Error| R2ListError[SYSTEM_INTERNAL_ERROR
500]
+ ListR2 -->|Success| ListSuccess[200 OK
Return PDF list]
+ end
+ end
+
+ subgraph ImportFlow["Import Operations"]
+ ImportAction{Import Type}
+ ImportAction -->|Google Drive| GoogleDriveFlow
+ ImportAction -->|DOI Lookup| DOIFlow
+ ImportAction -->|Reference File| ReferenceFileFlow
+
+ subgraph GoogleDriveFlow["Google Drive Import"]
+ GDriveStart[importFromGoogleDrive fileId] --> CheckAuthGDrive{Authenticated?}
+ CheckAuthGDrive -->|No| GDriveAuthError[AUTH_REQUIRED
401]
+ CheckAuthGDrive -->|Yes| FetchGDrive[Fetch file from
Google Drive API]
+ FetchGDrive -->|Network Error| GDriveNetworkError[Network error
Show toast error]
+ FetchGDrive -->|API Error| GDriveAPIError[Google Drive API error
Show toast error]
+ FetchGDrive -->|Success| UploadGDrive[Upload to R2
via PDF upload flow]
+ UploadGDrive -->|Failed| GDriveUploadError[Upload failed
Show toast error]
+ UploadGDrive -->|Success| ExtractGDriveMetadata[Extract metadata
from PDF]
+ ExtractGDriveMetadata -->|Failed| GDriveMetadataError[Log warning
Continue]
+ ExtractGDriveMetadata -->|Success| UpdateStudyGDrive[Update study
with metadata]
+ UpdateStudyGDrive -->|Y.js Error| GDriveYjsError[Y.js operation failed
Show toast error]
+ UpdateStudyGDrive -->|Success| GDriveSuccess[Import complete]
+ end
+
+ subgraph DOIFlow["DOI Lookup"]
+ DOIStart[fetchFromDOI doi] --> ValidateDOI{Valid DOI
format?}
+ ValidateDOI -->|No| DOIValidationError[Invalid DOI
Return null]
+ ValidateDOI -->|Yes| FetchDOIAPI[Fetch from DOI
API external]
+ FetchDOIAPI -->|Network Error| DOINetworkError[Network error
Log warning
Return null]
+ FetchDOIAPI -->|API Error| DOIAPIError[DOI API error
Log warning
Return null]
+ FetchDOIAPI -->|Success| ParseDOIResponse[Parse response:
firstAuthor, publicationYear,
authors, journal, abstract]
+ ParseDOIResponse -->|Parse Error| DOIParseError[Parse error
Log warning
Return null]
+ ParseDOIResponse -->|Success| DOISuccess[Return metadata]
+ end
+
+ subgraph ReferenceFileFlow["Reference File Import"]
+ RefFileStart[importReferences references] --> CheckConnectionRef{Connected to
project?}
+ CheckConnectionRef -->|No| RefConnectionError[Not connected
Show toast error
Return 0]
+ CheckConnectionRef -->|Yes| LoopRefs[For each reference]
+ LoopRefs --> CreateRefStudy[Create study from
reference data]
+ CreateRefStudy -->|Y.js Error| RefYjsError[Y.js operation failed
Log error
Continue]
+ CreateRefStudy -->|Success| IncrementCount[Increment success count]
+ IncrementCount --> MoreRefs{More
references?}
+ MoreRefs -->|Yes| LoopRefs
+ MoreRefs -->|No| RefSuccess[Show toast
Return count]
+ end
+ end
+
+ style ConnectionError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style SyncError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style CreateYDocError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style YjsError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style UpdateYDocError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style StudyNotFound fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style UpdateYjsError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style DeleteYDocError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style DeleteNotFound fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style PDFDeleteError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style CacheError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style DeleteYjsError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style UploadAuthError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style UploadAccessError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style UploadForbidden fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style SizeError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style InvalidTypeError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style FileNameError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style DuplicateError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style R2UploadError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style MetadataError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style DOINetworkError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style AddYjsError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style CachePDFError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style DownloadAuthError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style DownloadAccessError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style FileNameDownloadError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style NotFoundError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style R2DownloadError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style DeleteAuthError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style DeleteAccessError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style DeleteForbidden fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style FileNameDeleteError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style R2DeleteError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style ListAuthError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style ListAccessError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style R2ListError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style GDriveAuthError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style GDriveNetworkError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style GDriveAPIError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style GDriveUploadError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style GDriveMetadataError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style GDriveYjsError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style DOIValidationError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style DOINetworkError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style DOIAPIError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style DOIParseError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style RefConnectionError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style RefYjsError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+```
+
+---
+
+## Checklist Actions
+
+Checklists are managed entirely through Y.js operations (no backend API). Checklists support multiple types (AMSTAR2, ROBINS-I) with different answer structures. Answers are stored as nested Y.Maps for concurrent editing support.
+
+```mermaid
+flowchart TB
+ Start([User Action]) --> Connected{Connected to
project?}
+ Connected -->|No| ConnectionError[Not connected to project
Show toast error]
+ Connected -->|Yes| Synced{Synced with
Y.js?}
+ Synced -->|No| SyncError[Y.js not synced
Operation fails]
+ Synced -->|Yes| ChecklistAction{Checklist Action}
+
+ ChecklistAction -->|Create| CreateChecklistFlow
+ ChecklistAction -->|Update| UpdateChecklistFlow
+ ChecklistAction -->|Delete| DeleteChecklistFlow
+ ChecklistAction -->|Update Answer| UpdateAnswerFlow
+ ChecklistAction -->|Get Data| GetDataFlow
+ ChecklistAction -->|Get Note| GetNoteFlow
+
+ subgraph CreateChecklistFlow["Create Checklist"]
+ CreateChecklist[createChecklist studyId, type, assigneeId] --> CheckYDoc{Y.Doc
available?}
+ CheckYDoc -->|No| CreateYDocError[No Y.Doc
Show toast error
Return false]
+ CheckYDoc -->|Yes| GetStudy[Get study from
reviews Y.Map]
+ GetStudy -->|Not Found| StudyNotFound[Study not found
Return null]
+ GetStudy -->|Found| ValidateType{Valid checklist
type? AMSTAR2/ROBINS-I}
+ ValidateType -->|No| InvalidTypeError[Invalid checklist type
Return null]
+ ValidateType -->|Yes| GetChecklistsMap[Get checklists Y.Map
from study]
+ GetChecklistsMap -->|Not Exists| CreateChecklistsMap[Create new Y.Map
for checklists]
+ CreateChecklistsMap -->|Y.js Error| CreateMapError[Y.js operation failed
Return null]
+ CreateChecklistsMap -->|Success| GenerateChecklistId[Generate checklistId
crypto.randomUUID]
+ GetChecklistsMap -->|Exists| GenerateChecklistId
+ GenerateChecklistId --> GetTemplate[Get checklist template
from CHECKLIST_REGISTRY]
+ GetTemplate -->|Type Not Found| TemplateError[Checklist type not in registry
Return null]
+ GetTemplate -->|Found| ExtractAnswers[Extract default answers
structure from template]
+ ExtractAnswers -->|Extract Error| ExtractError[Failed to extract answers
Return null]
+ ExtractAnswers -->|Success| CreateChecklistYMap[Create checklist Y.Map:
type, title, assignedTo,
status: pending,
isReconciled: false,
createdAt, updatedAt]
+ CreateChecklistYMap --> CreateAnswersMap[Create answers Y.Map
Structure depends on type]
+ CreateAnswersMap -->|AMSTAR2| CreateAMSTAR2Answers[Create nested Y.Maps
for each question q1-q16
with answers, critical, note]
+ CreateAMSTAR2Answers -->|Y.js Error| AMSTAR2Error[Y.js operation failed
Return null]
+ CreateAMSTAR2Answers -->|Success| AddToChecklistsMap[Add to checklists Y.Map
checklistsMap.set checklistId]
+ CreateAnswersMap -->|ROBINS-I| CreateROBINSAnswers[Create nested Y.Maps
for domains, sections
with judgement, direction,
answers nested structure]
+ CreateROBINSAnswers -->|Y.js Error| ROBINSError[Y.js operation failed
Return null]
+ CreateROBINSAnswers -->|Success| AddToChecklistsMap
+ CreateAnswersMap -->|Other| CreateOtherAnswers[Store data directly
as JSON]
+ CreateOtherAnswers -->|Y.js Error| OtherError[Y.js operation failed
Return null]
+ CreateOtherAnswers -->|Success| AddToChecklistsMap
+ AddToChecklistsMap -->|Y.js Error| AddMapError[Y.js operation failed
Return null]
+ AddToChecklistsMap -->|Success| UpdateStudyTimestamp[Update study
updatedAt timestamp]
+ UpdateStudyTimestamp -->|Y.js Error| TimestampError[Y.js operation failed
Log error
Continue]
+ UpdateStudyTimestamp -->|Success| CreateSuccess[Return checklistId]
+ end
+
+ subgraph UpdateChecklistFlow["Update Checklist"]
+ UpdateChecklist[updateChecklist studyId, checklistId, updates] --> CheckYDocUpdate{Y.Doc
available?}
+ CheckYDocUpdate -->|No| UpdateYDocError[No Y.Doc
Show toast error]
+ CheckYDocUpdate -->|Yes| GetStudyUpdate[Get study from
reviews Y.Map]
+ GetStudyUpdate -->|Not Found| UpdateStudyNotFound[Study not found
Return]
+ GetStudyUpdate -->|Found| GetChecklistsMapUpdate[Get checklists Y.Map]
+ GetChecklistsMapUpdate -->|Not Exists| UpdateChecklistsMapError[Checklists map not found
Return]
+ GetChecklistsMapUpdate -->|Exists| GetChecklist[Get checklist Y.Map]
+ GetChecklist -->|Not Found| UpdateChecklistNotFound[Checklist not found
Return]
+ GetChecklist -->|Found| UpdateFields[Update fields:
title, assignedTo,
status, isReconciled
Set updatedAt]
+ UpdateFields -->|Y.js Error| UpdateYjsError[Y.js operation failed
Show toast error]
+ UpdateFields -->|Success| UpdateSuccess[Update complete]
+ end
+
+ subgraph DeleteChecklistFlow["Delete Checklist"]
+ DeleteChecklist[deleteChecklist studyId, checklistId] --> CheckYDocDelete{Y.Doc
available?}
+ CheckYDocDelete -->|No| DeleteYDocError[No Y.Doc
Show toast error]
+ CheckYDocDelete -->|Yes| GetStudyDelete[Get study from
reviews Y.Map]
+ GetStudyDelete -->|Not Found| DeleteStudyNotFound[Study not found
Return]
+ GetStudyDelete -->|Found| GetChecklistsMapDelete[Get checklists Y.Map]
+ GetChecklistsMapDelete -->|Not Exists| DeleteChecklistsMapError[Checklists map not found
Return]
+ GetChecklistsMapDelete -->|Exists| DeleteFromMap[Delete from
checklists Y.Map]
+ DeleteFromMap -->|Y.js Error| DeleteYjsError[Y.js operation failed
Show toast error]
+ DeleteFromMap -->|Success| UpdateStudyTimestampDelete[Update study
updatedAt timestamp]
+ UpdateStudyTimestampDelete -->|Y.js Error| DeleteTimestampError[Y.js operation failed
Log error
Continue]
+ UpdateStudyTimestampDelete -->|Success| DeleteSuccess[Checklist deleted]
+ end
+
+ subgraph UpdateAnswerFlow["Update Checklist Answer"]
+ UpdateAnswer[updateChecklistAnswer studyId, checklistId, key, data] --> CheckYDocAnswer{Y.Doc
available?}
+ CheckYDocAnswer -->|No| AnswerYDocError[No Y.Doc
Return]
+ CheckYDocAnswer -->|Yes| GetStudyAnswer[Get study from
reviews Y.Map]
+ GetStudyAnswer -->|Not Found| AnswerStudyNotFound[Study not found
Return]
+ GetStudyAnswer -->|Found| GetChecklistsMapAnswer[Get checklists Y.Map]
+ GetChecklistsMapAnswer -->|Not Exists| AnswerChecklistsMapError[Checklists map not found
Return]
+ GetChecklistsMapAnswer -->|Exists| GetChecklistAnswer[Get checklist Y.Map]
+ GetChecklistAnswer -->|Not Found| AnswerChecklistNotFound[Checklist not found
Return]
+ GetChecklistAnswer -->|Found| GetAnswersMap[Get answers Y.Map
from checklist]
+ GetAnswersMap -->|Not Exists| CreateAnswersMapAnswer[Create new
answers Y.Map]
+ CreateAnswersMapAnswer -->|Y.js Error| CreateAnswersError[Y.js operation failed
Return]
+ CreateAnswersMapAnswer -->|Success| GetChecklistType[Get checklist type]
+ GetAnswersMap -->|Exists| GetChecklistType
+ GetChecklistType -->|AMSTAR2| UpdateAMSTAR2Answer[Update question Y.Map:
answers, critical
Preserve note Y.Text]
+ UpdateAMSTAR2Answer -->|Y.js Error| UpdateAMSTAR2Error[Y.js operation failed
Return]
+ UpdateAMSTAR2Answer -->|Success| CheckStatus[Check status]
+ GetChecklistType -->|ROBINS-I| UpdateROBINSAnswer[Update section Y.Map:
judgement, direction,
nested answers structure]
+ UpdateROBINSAnswer -->|Y.js Error| UpdateROBINSError[Y.js operation failed
Return]
+ UpdateROBINSAnswer -->|Success| CheckStatus
+ GetChecklistType -->|Other| UpdateOtherAnswer[Store data directly]
+ UpdateOtherAnswer -->|Y.js Error| UpdateOtherError[Y.js operation failed
Return]
+ UpdateOtherAnswer -->|Success| CheckStatus
+ CheckStatus -->|status === pending| SetInProgress[Set status to
in-progress]
+ SetInProgress -->|Y.js Error| StatusError[Y.js operation failed
Log error
Continue]
+ SetInProgress -->|Success| SetUpdatedAt[Set updatedAt
timestamp]
+ CheckStatus -->|status !== pending| SetUpdatedAt
+ SetUpdatedAt -->|Y.js Error| UpdatedAtError[Y.js operation failed
Log error
Continue]
+ SetUpdatedAt -->|Success| AnswerSuccess[Answer updated]
+ end
+
+ subgraph GetDataFlow["Get Checklist Data"]
+ GetData[getChecklistData studyId, checklistId] --> CheckYDocData{Y.Doc
available?}
+ CheckYDocData -->|No| DataYDocError[No Y.Doc
Return null]
+ CheckYDocData -->|Yes| GetStudyData[Get study from
reviews Y.Map]
+ GetStudyData -->|Not Found| DataStudyNotFound[Study not found
Return null]
+ GetStudyData -->|Found| GetChecklistsMapData[Get checklists Y.Map]
+ GetChecklistsMapData -->|Not Exists| DataChecklistsMapError[Checklists map not found
Return null]
+ GetChecklistsMapData -->|Exists| GetChecklistData[Get checklist Y.Map]
+ GetChecklistData -->|Not Found| DataChecklistNotFound[Checklist not found
Return null]
+ GetChecklistData -->|Found| ConvertToJSON[Convert Y.Map to
plain object]
+ ConvertToJSON -->|Conversion Error| ConvertError[Conversion failed
Return null]
+ ConvertToJSON -->|Success| GetAnswers[Get answers Y.Map]
+ GetAnswers -->|Not Exists| DataAnswersNotFound[Answers map not found
Return data without answers]
+ GetAnswers -->|Exists| ReconstructAnswers[Reconstruct nested structure
from Y.Maps based on type]
+ ReconstructAnswers -->|AMSTAR2| ReconstructAMSTAR2[Convert question Y.Maps
to plain objects]
+ ReconstructAMSTAR2 -->|Error| ReconstructAMSTAR2Error[Reconstruction failed
Return partial data]
+ ReconstructAMSTAR2 -->|Success| MergeData[Merge with checklist data]
+ ReconstructAnswers -->|ROBINS-I| ReconstructROBINS[Convert domain/section Y.Maps
to nested structure]
+ ReconstructROBINS -->|Error| ReconstructROBINSError[Reconstruction failed
Return partial data]
+ ReconstructROBINS -->|Success| MergeData
+ ReconstructAnswers -->|Other| ReconstructOther[Convert directly]
+ ReconstructOther -->|Error| ReconstructOtherError[Reconstruction failed
Return partial data]
+ ReconstructOther -->|Success| MergeData
+ MergeData -->|Success| DataSuccess[Return checklist data]
+ end
+
+ subgraph GetNoteFlow["Get Question Note"]
+ GetNote[getQuestionNote studyId, checklistId, questionKey] --> CheckYDocNote{Y.Doc
available?}
+ CheckYDocNote -->|No| NoteYDocError[No Y.Doc
Return null]
+ CheckYDocNote -->|Yes| GetStudyNote[Get study from
reviews Y.Map]
+ GetStudyNote -->|Not Found| NoteStudyNotFound[Study not found
Return null]
+ GetStudyNote -->|Found| GetChecklistsMapNote[Get checklists Y.Map]
+ GetChecklistsMapNote -->|Not Exists| NoteChecklistsMapError[Checklists map not found
Return null]
+ GetChecklistsMapNote -->|Exists| GetChecklistNote[Get checklist Y.Map]
+ GetChecklistNote -->|Not Found| NoteChecklistNotFound[Checklist not found
Return null]
+ GetChecklistNote -->|Found| GetAnswersMapNote[Get answers Y.Map]
+ GetAnswersMapNote -->|Not Exists| NoteAnswersNotFound[Answers map not found
Return null]
+ GetAnswersMapNote -->|Exists| GetQuestionMap[Get question Y.Map
by questionKey]
+ GetQuestionMap -->|Not Found| NoteQuestionNotFound[Question not found
Return null]
+ GetQuestionMap -->|Found| GetNoteYText[Get note Y.Text]
+ GetNoteYText -->|Not Exists| CreateNoteYText[Create new Y.Text
for backward compatibility]
+ CreateNoteYText -->|Not Synced| NoteNotSyncedError[Not synced
Return null]
+ CreateNoteYText -->|Synced| NoteSuccess[Return Y.Text]
+ GetNoteYText -->|Exists| NoteSuccess
+ end
+
+ style ConnectionError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style SyncError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style CreateYDocError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style StudyNotFound fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style InvalidTypeError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style CreateMapError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style TemplateError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style ExtractError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style AMSTAR2Error fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style ROBINSError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style OtherError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style AddMapError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style TimestampError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style UpdateYDocError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style UpdateStudyNotFound fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style UpdateChecklistsMapError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style UpdateChecklistNotFound fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style UpdateYjsError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style DeleteYDocError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style DeleteStudyNotFound fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style DeleteChecklistsMapError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style DeleteYjsError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style DeleteTimestampError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style AnswerYDocError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style AnswerStudyNotFound fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style AnswerChecklistsMapError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style AnswerChecklistNotFound fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style CreateAnswersError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style UpdateAMSTAR2Error fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style UpdateROBINSError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style UpdateOtherError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style StatusError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style UpdatedAtError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style DataYDocError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style DataStudyNotFound fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style DataChecklistsMapError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style DataChecklistNotFound fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style ConvertError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style DataAnswersNotFound fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style ReconstructAMSTAR2Error fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style ReconstructROBINSError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style ReconstructOtherError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style NoteYDocError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style NoteStudyNotFound fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style NoteChecklistsMapError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style NoteChecklistNotFound fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style NoteAnswersNotFound fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style NoteQuestionNotFound fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+ style NoteNotSyncedError fill:#dc3545,stroke:#991f2e,stroke-width:2px,color:#ffffff
+```
+
+---
+
+## Failure Points Legend
+
+### Error Code Categories
+
+#### Authentication Errors (AUTH_ERRORS)
+
+- **AUTH_REQUIRED** (401): User not authenticated
+- **AUTH_INVALID** (401): Invalid credentials
+- **AUTH_EXPIRED** (401): Session expired
+- **AUTH_FORBIDDEN** (403): Insufficient permissions or missing entitlement
+
+#### Validation Errors (VALIDATION_ERRORS)
+
+- **FIELD_REQUIRED** (400): Required field missing
+- **FIELD_INVALID_FORMAT** (400): Field format invalid
+- **FIELD_TOO_LONG** (400): Field exceeds maximum length
+- **FIELD_TOO_SHORT** (400): Field below minimum length
+- **MULTI_FIELD** (400): Multiple validation errors
+- **FAILED** (400): General validation failure
+- **INVALID_INPUT** (400): Invalid input data
+
+#### Project Errors (PROJECT_ERRORS)
+
+- **NOT_FOUND** (404): Project, member, or resource not found
+- **ACCESS_DENIED** (403): User does not have access to project
+- **MEMBER_ALREADY_EXISTS** (409): User is already a member
+- **LAST_OWNER** (400): Cannot remove last owner
+- **INVALID_ROLE** (400): Invalid role specified
+
+#### File Errors (FILE_ERRORS)
+
+- **TOO_LARGE** (413): File exceeds size limit
+- **INVALID_TYPE** (400): Invalid file type (not PDF)
+- **NOT_FOUND** (404): File not found in R2
+- **UPLOAD_FAILED** (500): File upload to R2 failed
+- **ALREADY_EXISTS** (409): File with same name already exists
+
+#### User Errors (USER_ERRORS)
+
+- **NOT_FOUND** (404): User not found
+- **EMAIL_NOT_VERIFIED** (403): Email address not verified
+
+#### System Errors (SYSTEM_ERRORS)
+
+- **DB_ERROR** (500): Database operation failed
+- **DB_TRANSACTION_FAILED** (500): Database transaction failed
+- **EMAIL_SEND_FAILED** (500): Failed to send email
+- **EMAIL_INVALID** (400): Invalid email address
+- **RATE_LIMITED** (429): Too many requests
+- **INTERNAL_ERROR** (500): Internal server error
+- **SERVICE_UNAVAILABLE** (503): Service temporarily unavailable
+
+### HTTP Status Codes
+
+- **200 OK**: Successful operation
+- **201 Created**: Resource created successfully
+- **400 Bad Request**: Validation error or business logic error
+- **401 Unauthorized**: Authentication required or invalid
+- **403 Forbidden**: Insufficient permissions
+- **404 Not Found**: Resource not found
+- **409 Conflict**: Resource conflict (e.g., duplicate)
+- **413 Payload Too Large**: File size exceeds limit
+- **429 Too Many Requests**: Rate limit exceeded
+- **500 Internal Server Error**: Server error
+- **503 Service Unavailable**: Service temporarily unavailable
+
+### Y.js Sync Failures
+
+Y.js operations can fail in several ways:
+
+- **Not Connected**: No active Y.js connection to project
+- **Not Synced**: Y.js document not yet synced with server
+- **Y.Doc Missing**: Y.Doc not available (null/undefined)
+- **Y.js Operation Error**: Y.js map/text operation throws error
+- **Sync Conflict**: Concurrent edits cause sync issues
+
+### Network Failures
+
+External API calls can fail:
+
+- **Network Error**: Connection timeout, DNS failure, etc.
+- **API Error**: External API returns error response
+- **Parse Error**: Failed to parse API response
+- **Timeout**: Request exceeded timeout limit
+
+### Permission Levels
+
+- **Owner**: Full access, can delete project, manage all members
+- **Collaborator**: Can edit content, cannot delete project or manage members
+- **Member**: Can edit content, cannot delete project or manage members
+- **Viewer**: Read-only access, cannot edit or upload
+
+### External Dependencies
+
+- **D1 Database**: Cloudflare D1 SQL database for persistent storage
+- **R2 Storage**: Cloudflare R2 object storage for PDFs
+- **Durable Objects**: ProjectDoc and UserSession for real-time sync
+- **External APIs**: DOI lookup, Google Drive API
diff --git a/docs/architecture/index.html b/docs/architecture/index.html
index 344df682b..8b154d823 100644
--- a/docs/architecture/index.html
+++ b/docs/architecture/index.html
@@ -12,6 +12,17 @@
startOnLoad: false,
theme: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'default',
securityLevel: 'loose',
+ flowchart: {
+ useMaxWidth: false,
+ htmlLabels: true,
+ curve: 'basis',
+ },
+ gantt: {
+ useMaxWidth: false,
+ },
+ sequence: {
+ useMaxWidth: false,
+ },
});
const diagrams = [
@@ -21,6 +32,7 @@
{ id: 'data', file: '04-data-model.md', title: 'Data Model' },
{ id: 'routes', file: '05-frontend-routes.md', title: 'Frontend Routes' },
{ id: 'api', file: '06-api-routes.md', title: 'API Routes' },
+ { id: 'actions', file: '07-api-actions.md', title: 'API Actions' },
];
async function loadDiagrams() {
@@ -59,6 +71,13 @@
// Render all mermaid diagrams
await mermaid.run({ querySelector: '.language-mermaid' });
+
+ // Scale up diagrams after rendering
+ document.querySelectorAll('.diagram-container svg').forEach(svg => {
+ svg.style.minWidth = '1200px';
+ svg.style.width = '100%';
+ svg.style.height = 'auto';
+ });
}
// Custom renderer to handle mermaid code blocks
@@ -96,13 +115,17 @@
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- max-width: 1200px;
- margin: 0 auto;
+ max-width: 100%;
+ margin: 0;
padding: 2rem;
background: var(--bg);
color: var(--text);
line-height: 1.6;
}
+ .content-wrapper {
+ max-width: 1800px;
+ margin: 0 auto;
+ }
h1 {
border-bottom: 2px solid var(--accent);
padding-bottom: 0.5rem;
@@ -111,20 +134,55 @@
h2 {
margin-top: 2rem;
color: var(--accent);
+ font-size: 1.8rem;
+ }
+ section {
+ width: 100%;
+ max-width: 100%;
}
.diagram-container {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 8px;
- padding: 1.5rem;
- margin: 1rem 0 2rem;
+ padding: 2rem;
+ margin: 2rem 0 3rem;
overflow-x: auto;
+ overflow-y: visible;
+ min-height: 400px;
+ width: 100%;
+ position: relative;
}
.language-mermaid {
- display: flex;
- justify-content: center;
+ display: block;
background: transparent;
margin: 0;
+ width: 100%;
+ min-width: 100%;
+ }
+ .language-mermaid svg {
+ max-width: none !important;
+ width: 100% !important;
+ height: auto !important;
+ min-width: 1200px;
+ font-size: 14px;
+ }
+ .diagram-container svg .nodeLabel,
+ .diagram-container svg .edgeLabel {
+ font-size: 14px !important;
+ }
+ .diagram-container::-webkit-scrollbar {
+ height: 12px;
+ }
+ .diagram-container::-webkit-scrollbar-track {
+ background: var(--card-bg);
+ border-radius: 6px;
+ }
+ .diagram-container::-webkit-scrollbar-thumb {
+ background: var(--border);
+ border-radius: 6px;
+ }
+ .diagram-container::-webkit-scrollbar-thumb:hover {
+ background: var(--accent);
}
p {
color: var(--text);
@@ -197,6 +255,8 @@