From 0ffad02e0a73026342358d0bea090aa0c0519cb1 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Fri, 30 Jan 2026 13:14:03 -0600 Subject: [PATCH 1/6] feat: Install quality-rails for mechanical code quality enforcement Quality Rails provides mechanical enforcement of code quality through pre-commit hooks and CI/CD pipelines, preventing ~70% of common issues. What's added: - Pre-commit hooks via husky (formatting enforcement enabled) - Enhanced ESLint rules (no-explicit-any, security plugin, etc.) - lint-staged configuration (currently formatting-only mode) - Woodpecker CI pipeline template (.woodpecker.yml) - eslint-plugin-security for vulnerability detection - Documentation (docs/quality-rails-status.md) Current status: - Strict enforcement DISABLED until existing violations are fixed - Found 1,226 violations (1,121 errors, 105 warnings) - Priority: Fix explicit 'any' types first - Pre-commit currently only enforces Prettier formatting Next steps: 1. Fix existing lint violations 2. Enable strict pre-commit enforcement 3. Configure CI/CD pipeline Based on quality-rails from ~/src/quality-rails (monorepo template) See docs/quality-rails-status.md for detailed roadmap. Co-Authored-By: Claude Sonnet 4.5 --- .gitignore | 3 + .husky/pre-commit | 3 + .lintstagedrc.mjs | 16 + .woodpecker.yml | 67 ++ CLAUDE.md | 675 ++++++++-------- docs/quality-rails-status.md | 197 +++++ ...c.js_20260130-1310_1_remediation_needed.md | 20 + ....mjs_20260130-1312_1_remediation_needed.md | 20 + ....mjs_20260130-1312_2_remediation_needed.md | 20 + ....mjs_20260130-1312_3_remediation_needed.md | 20 + ....mjs_20260130-1312_4_remediation_needed.md | 20 + ...e.js_20260130-1309_1_remediation_needed.md | 20 + ...s.ts_20260130-1309_1_remediation_needed.md | 20 + package.json | 6 +- packages/config/eslint/base.js | 33 +- packages/config/package.json | 1 + pnpm-lock.yaml | 757 ++++++++++++++++-- 17 files changed, 1526 insertions(+), 372 deletions(-) create mode 100755 .husky/pre-commit create mode 100644 .lintstagedrc.mjs create mode 100644 .woodpecker.yml create mode 100644 docs/quality-rails-status.md create mode 100644 docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-.lintstagedrc.js_20260130-1310_1_remediation_needed.md create mode 100644 docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-.lintstagedrc.mjs_20260130-1312_1_remediation_needed.md create mode 100644 docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-.lintstagedrc.mjs_20260130-1312_2_remediation_needed.md create mode 100644 docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-.lintstagedrc.mjs_20260130-1312_3_remediation_needed.md create mode 100644 docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-.lintstagedrc.mjs_20260130-1312_4_remediation_needed.md create mode 100644 docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-packages-config-eslint-base.js_20260130-1309_1_remediation_needed.md create mode 100644 docs/reports/qa-automation/pending/tmp-claude-1000--home-localadmin-src-mosaic-stack-f3beb7a6-6cd5-4bee-8283-fac0798a92fa-scratchpad-test-violations.ts_20260130-1309_1_remediation_needed.md diff --git a/.gitignore b/.gitignore index 6420fc4..bee10ce 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,6 @@ yarn-error.log* # Misc *.tsbuildinfo .pnpm-approve-builds + +# Husky +.husky/_ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..0f707d9 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,3 @@ +#!/bin/sh +npx lint-staged +npx git-secrets --scan || echo "Warning: git-secrets not installed" diff --git a/.lintstagedrc.mjs b/.lintstagedrc.mjs new file mode 100644 index 0000000..7d28dae --- /dev/null +++ b/.lintstagedrc.mjs @@ -0,0 +1,16 @@ +// Monorepo-aware lint-staged configuration +// NOTE: Strict enforcement is disabled until existing violations are fixed. +// See docs/quality-rails-status.md for current status. +export default { + // TypeScript files - format only for now + '**/*.{ts,tsx}': (filenames) => { + return [ + `prettier --write ${filenames.join(' ')}`, + ]; + }, + + // Format all other files + '**/*.{js,jsx,json,md,yml,yaml}': [ + 'prettier --write', + ], +}; diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..bde97fc --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,67 @@ +# Woodpecker CI Quality Enforcement Pipeline - Monorepo +when: + - event: [push, pull_request, manual] + +variables: + - &node_image "node:20-alpine" + - &install_deps | + corepack enable + npm ci --ignore-scripts + +steps: + install: + image: *node_image + commands: + - *install_deps + + security-audit: + image: *node_image + commands: + - *install_deps + - npm audit --audit-level=high + depends_on: + - install + + lint: + image: *node_image + environment: + SKIP_ENV_VALIDATION: "true" + commands: + - *install_deps + - npm run lint + depends_on: + - install + + typecheck: + image: *node_image + environment: + SKIP_ENV_VALIDATION: "true" + commands: + - *install_deps + - npm run type-check + depends_on: + - install + + test: + image: *node_image + environment: + SKIP_ENV_VALIDATION: "true" + commands: + - *install_deps + - npm run test -- --coverage --coverageThreshold='{"global":{"branches":80,"functions":80,"lines":80,"statements":80}}' + depends_on: + - install + + build: + image: *node_image + environment: + SKIP_ENV_VALIDATION: "true" + NODE_ENV: "production" + commands: + - *install_deps + - npm run build + depends_on: + - lint + - typecheck + - test + - security-audit diff --git a/CLAUDE.md b/CLAUDE.md index 5327753..751872a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,400 +1,451 @@ **Multi-tenant personal assistant platform with PostgreSQL backend, Authentik SSO, and MoltBot - integration.** +integration.** - ## Project Overview +## Project Overview - Mosaic Stack is a standalone platform that provides: - - Multi-user workspaces with team sharing - - Task, event, and project management - - Gantt charts and Kanban boards - - MoltBot integration via plugins (stock MoltBot + mosaic-plugin-*) - - PDA-friendly design throughout +Mosaic Stack is a standalone platform that provides: - **Repository:** git.mosaicstack.dev/mosaic/stack - **Versioning:** Start at 0.0.1, MVP = 0.1.0 +- Multi-user workspaces with team sharing +- Task, event, and project management +- Gantt charts and Kanban boards +- MoltBot integration via plugins (stock MoltBot + mosaic-plugin-\*) +- PDA-friendly design throughout - ## Technology Stack +**Repository:** git.mosaicstack.dev/mosaic/stack +**Versioning:** Start at 0.0.1, MVP = 0.1.0 - | Layer | Technology | - |-------|------------| - | Frontend | Next.js 16 + React + TailwindCSS + Shadcn/ui | - | Backend | NestJS + Prisma ORM | - | Database | PostgreSQL 17 + pgvector | - | Cache | Valkey (Redis-compatible) | - | Auth | Authentik (OIDC) | - | AI | Ollama (configurable: local or remote) | - | Messaging | MoltBot (stock + Mosaic plugins) | - | Real-time | WebSockets (Socket.io) | - | Monorepo | pnpm workspaces + TurboRepo | - | Testing | Vitest + Playwright | - | Deployment | Docker + docker-compose | +## Technology Stack - ## Repository Structure +| Layer | Technology | +| ---------- | -------------------------------------------- | +| Frontend | Next.js 16 + React + TailwindCSS + Shadcn/ui | +| Backend | NestJS + Prisma ORM | +| Database | PostgreSQL 17 + pgvector | +| Cache | Valkey (Redis-compatible) | +| Auth | Authentik (OIDC) | +| AI | Ollama (configurable: local or remote) | +| Messaging | MoltBot (stock + Mosaic plugins) | +| Real-time | WebSockets (Socket.io) | +| Monorepo | pnpm workspaces + TurboRepo | +| Testing | Vitest + Playwright | +| Deployment | Docker + docker-compose | - mosaic-stack/ - ├── apps/ - │ ├── api/ # mosaic-api (NestJS) - │ │ ├── src/ - │ │ │ ├── auth/ # Authentik OIDC - │ │ │ ├── tasks/ # Task management - │ │ │ ├── events/ # Calendar/events - │ │ │ ├── projects/ # Project management - │ │ │ ├── brain/ # MoltBot integration - │ │ │ └── activity/ # Activity logging - │ │ ├── prisma/ - │ │ │ └── schema.prisma - │ │ └── Dockerfile - │ └── web/ # mosaic-web (Next.js 16) - │ ├── app/ - │ ├── components/ - │ └── Dockerfile - ├── packages/ - │ ├── shared/ # Shared types, utilities - │ ├── ui/ # Shared UI components - │ └── config/ # Shared configuration - ├── plugins/ - │ ├── mosaic-plugin-brain/ # MoltBot skill: API queries - │ ├── mosaic-plugin-calendar/ # MoltBot skill: Calendar - │ ├── mosaic-plugin-tasks/ # MoltBot skill: Tasks - │ └── mosaic-plugin-gantt/ # MoltBot skill: Gantt - ├── docker/ - │ ├── docker-compose.yml # Turnkey deployment - │ └── init-scripts/ # PostgreSQL init - ├── docs/ - │ ├── SETUP.md - │ ├── CONFIGURATION.md - │ └── DESIGN-PRINCIPLES.md - ├── .env.example - ├── turbo.json - ├── pnpm-workspace.yaml - └── README.md +## Repository Structure - ## Development Workflow +mosaic-stack/ +├── apps/ +│ ├── api/ # mosaic-api (NestJS) +│ │ ├── src/ +│ │ │ ├── auth/ # Authentik OIDC +│ │ │ ├── tasks/ # Task management +│ │ │ ├── events/ # Calendar/events +│ │ │ ├── projects/ # Project management +│ │ │ ├── brain/ # MoltBot integration +│ │ │ └── activity/ # Activity logging +│ │ ├── prisma/ +│ │ │ └── schema.prisma +│ │ └── Dockerfile +│ └── web/ # mosaic-web (Next.js 16) +│ ├── app/ +│ ├── components/ +│ └── Dockerfile +├── packages/ +│ ├── shared/ # Shared types, utilities +│ ├── ui/ # Shared UI components +│ └── config/ # Shared configuration +├── plugins/ +│ ├── mosaic-plugin-brain/ # MoltBot skill: API queries +│ ├── mosaic-plugin-calendar/ # MoltBot skill: Calendar +│ ├── mosaic-plugin-tasks/ # MoltBot skill: Tasks +│ └── mosaic-plugin-gantt/ # MoltBot skill: Gantt +├── docker/ +│ ├── docker-compose.yml # Turnkey deployment +│ └── init-scripts/ # PostgreSQL init +├── docs/ +│ ├── SETUP.md +│ ├── CONFIGURATION.md +│ └── DESIGN-PRINCIPLES.md +├── .env.example +├── turbo.json +├── pnpm-workspace.yaml +└── README.md - ### Branch Strategy - - `main` — stable releases only - - `develop` — active development (default working branch) - - `feature/*` — feature branches from develop - - `fix/*` — bug fix branches +## Development Workflow - ### Starting Work - ```bash - git checkout develop - git pull --rebase - pnpm install +### Branch Strategy - Running Locally +- `main` — stable releases only +- `develop` — active development (default working branch) +- `feature/*` — feature branches from develop +- `fix/*` — bug fix branches - # Start all services (Docker) - docker compose up -d +### Starting Work - # Or run individually for development - pnpm dev # All apps - pnpm dev:api # API only - pnpm dev:web # Web only +````bash +git checkout develop +git pull --rebase +pnpm install - Testing +Running Locally - pnpm test # Run all tests - pnpm test:api # API tests only - pnpm test:web # Web tests only - pnpm test:e2e # Playwright E2E +# Start all services (Docker) +docker compose up -d - Building +# Or run individually for development +pnpm dev # All apps +pnpm dev:api # API only +pnpm dev:web # Web only - pnpm build # Build all - pnpm build:api # Build API - pnpm build:web # Build Web +Testing - Design Principles (NON-NEGOTIABLE) +pnpm test # Run all tests +pnpm test:api # API tests only +pnpm test:web # Web tests only +pnpm test:e2e # Playwright E2E - PDA-Friendly Language +Building - NEVER use demanding language. This is critical. - ┌─────────────┬──────────────────────┐ - │ ❌ NEVER │ ✅ ALWAYS │ - ├─────────────┼──────────────────────┤ - │ OVERDUE │ Target passed │ - ├─────────────┼──────────────────────┤ - │ URGENT │ Approaching target │ - ├─────────────┼──────────────────────┤ - │ MUST DO │ Scheduled for │ - ├─────────────┼──────────────────────┤ - │ CRITICAL │ High priority │ - ├─────────────┼──────────────────────┤ - │ YOU NEED TO │ Consider / Option to │ - ├─────────────┼──────────────────────┤ - │ REQUIRED │ Recommended │ - └─────────────┴──────────────────────┘ - Visual Indicators +pnpm build # Build all +pnpm build:api # Build API +pnpm build:web # Build Web - Use status indicators consistently: - - 🟢 On track / Active - - 🔵 Upcoming / Scheduled - - ⏸️ Paused / On hold - - 💤 Dormant / Inactive - - ⚪ Not started +Design Principles (NON-NEGOTIABLE) - Display Principles +PDA-Friendly Language - 1. 10-second scannability — Key info visible immediately - 2. Visual chunking — Clear sections with headers - 3. Single-line items — Compact, scannable lists - 4. Date grouping — Today, Tomorrow, This Week headers - 5. Progressive disclosure — Details on click, not upfront - 6. Calm colors — No aggressive reds for status +NEVER use demanding language. This is critical. +┌─────────────┬──────────────────────┐ +│ ❌ NEVER │ ✅ ALWAYS │ +├─────────────┼──────────────────────┤ +│ OVERDUE │ Target passed │ +├─────────────┼──────────────────────┤ +│ URGENT │ Approaching target │ +├─────────────┼──────────────────────┤ +│ MUST DO │ Scheduled for │ +├─────────────┼──────────────────────┤ +│ CRITICAL │ High priority │ +├─────────────┼──────────────────────┤ +│ YOU NEED TO │ Consider / Option to │ +├─────────────┼──────────────────────┤ +│ REQUIRED │ Recommended │ +└─────────────┴──────────────────────┘ +Visual Indicators - Reference +Use status indicators consistently: +- 🟢 On track / Active +- 🔵 Upcoming / Scheduled +- ⏸️ Paused / On hold +- 💤 Dormant / Inactive +- ⚪ Not started - See docs/DESIGN-PRINCIPLES.md for complete guidelines. - For original patterns, see: jarvis-brain/docs/DESIGN-PRINCIPLES.md +Display Principles - API Conventions +1. 10-second scannability — Key info visible immediately +2. Visual chunking — Clear sections with headers +3. Single-line items — Compact, scannable lists +4. Date grouping — Today, Tomorrow, This Week headers +5. Progressive disclosure — Details on click, not upfront +6. Calm colors — No aggressive reds for status - Endpoints +Reference - GET /api/{resource} # List (with pagination, filters) - GET /api/{resource}/:id # Get single - POST /api/{resource} # Create - PATCH /api/{resource}/:id # Update - DELETE /api/{resource}/:id # Delete +See docs/DESIGN-PRINCIPLES.md for complete guidelines. +For original patterns, see: jarvis-brain/docs/DESIGN-PRINCIPLES.md - Response Format +API Conventions - // Success - { - data: T | T[], - meta?: { total, page, limit } +Endpoints + +GET /api/{resource} # List (with pagination, filters) +GET /api/{resource}/:id # Get single +POST /api/{resource} # Create +PATCH /api/{resource}/:id # Update +DELETE /api/{resource}/:id # Delete + +Response Format + +// Success +{ + data: T | T[], + meta?: { total, page, limit } +} + +// Error +{ + error: { + code: string, + message: string, + details?: any } +} - // Error - { - error: { - code: string, - message: string, - details?: any - } - } +Brain Query API - Brain Query API +POST /api/brain/query +{ + query: "what's on my calendar", + context?: { view: "dashboard", workspace_id: "..." } +} - POST /api/brain/query - { - query: "what's on my calendar", - context?: { view: "dashboard", workspace_id: "..." } - } +Database Conventions - Database Conventions +Multi-Tenant (RLS) - Multi-Tenant (RLS) +All workspace-scoped tables use Row-Level Security: +- Always include workspace_id in queries +- RLS policies enforce isolation +- Set session context for current user - All workspace-scoped tables use Row-Level Security: - - Always include workspace_id in queries - - RLS policies enforce isolation - - Set session context for current user +Prisma Commands - Prisma Commands +pnpm prisma:generate # Generate client +pnpm prisma:migrate # Run migrations +pnpm prisma:studio # Open Prisma Studio +pnpm prisma:seed # Seed development data - pnpm prisma:generate # Generate client - pnpm prisma:migrate # Run migrations - pnpm prisma:studio # Open Prisma Studio - pnpm prisma:seed # Seed development data +MoltBot Plugin Development - MoltBot Plugin Development +Plugins live in plugins/mosaic-plugin-*/ and follow MoltBot skill format: - Plugins live in plugins/mosaic-plugin-*/ and follow MoltBot skill format: +# plugins/mosaic-plugin-brain/SKILL.md +--- +name: mosaic-plugin-brain +description: Query Mosaic Stack for tasks, events, projects +version: 0.0.1 +triggers: + - "what's on my calendar" + - "show my tasks" + - "morning briefing" +tools: + - mosaic_api +--- - # plugins/mosaic-plugin-brain/SKILL.md - --- - name: mosaic-plugin-brain - description: Query Mosaic Stack for tasks, events, projects - version: 0.0.1 - triggers: - - "what's on my calendar" - - "show my tasks" - - "morning briefing" - tools: - - mosaic_api - --- +# Plugin instructions here... - # Plugin instructions here... +Key principle: MoltBot remains stock. All customization via plugins only. - Key principle: MoltBot remains stock. All customization via plugins only. +Environment Variables - Environment Variables +See .env.example for all variables. Key ones: - See .env.example for all variables. Key ones: +# Database +DATABASE_URL=postgresql://mosaic:password@localhost:5432/mosaic - # Database - DATABASE_URL=postgresql://mosaic:password@localhost:5432/mosaic +# Auth +AUTHENTIK_URL=https://auth.example.com +AUTHENTIK_CLIENT_ID=mosaic-stack +AUTHENTIK_CLIENT_SECRET=... - # Auth - AUTHENTIK_URL=https://auth.example.com - AUTHENTIK_CLIENT_ID=mosaic-stack - AUTHENTIK_CLIENT_SECRET=... +# Ollama +OLLAMA_MODE=local|remote +OLLAMA_ENDPOINT=http://localhost:11434 - # Ollama - OLLAMA_MODE=local|remote - OLLAMA_ENDPOINT=http://localhost:11434 +# MoltBot +MOSAIC_API_TOKEN=... - # MoltBot - MOSAIC_API_TOKEN=... +Issue Tracking - Issue Tracking +Issues are tracked at: https://git.mosaicstack.dev/mosaic/stack/issues - Issues are tracked at: https://git.mosaicstack.dev/mosaic/stack/issues +Labels - Labels +- Priority: p0 (critical), p1 (high), p2 (medium), p3 (low) +- Type: api, web, database, auth, plugin, ai, devops, docs, migration, security, testing, +performance, setup - - Priority: p0 (critical), p1 (high), p2 (medium), p3 (low) - - Type: api, web, database, auth, plugin, ai, devops, docs, migration, security, testing, - performance, setup +Milestones - Milestones +- M1-Foundation (0.0.x) +- M2-MultiTenant (0.0.x) +- M3-Features (0.0.x) +- M4-MoltBot (0.0.x) +- M5-Migration (0.1.0 MVP) - - M1-Foundation (0.0.x) - - M2-MultiTenant (0.0.x) - - M3-Features (0.0.x) - - M4-MoltBot (0.0.x) - - M5-Migration (0.1.0 MVP) +Commit Format - Commit Format +(#issue): Brief description - (#issue): Brief description +Detailed explanation if needed. - Detailed explanation if needed. +Fixes #123 +Types: feat, fix, docs, test, refactor, chore - Fixes #123 - Types: feat, fix, docs, test, refactor, chore +Test-Driven Development (TDD) - REQUIRED - Test-Driven Development (TDD) - REQUIRED +**All code must follow TDD principles. This is non-negotiable.** - **All code must follow TDD principles. This is non-negotiable.** +TDD Workflow (Red-Green-Refactor) - TDD Workflow (Red-Green-Refactor) +1. **RED** — Write a failing test first + - Write the test for new functionality BEFORE writing any implementation code + - Run the test to verify it fails (proves the test works) + - Commit message: `test(#issue): add test for [feature]` - 1. **RED** — Write a failing test first - - Write the test for new functionality BEFORE writing any implementation code - - Run the test to verify it fails (proves the test works) - - Commit message: `test(#issue): add test for [feature]` +2. **GREEN** — Write minimal code to make the test pass + - Implement only enough code to pass the test + - Run tests to verify they pass + - Commit message: `feat(#issue): implement [feature]` - 2. **GREEN** — Write minimal code to make the test pass - - Implement only enough code to pass the test - - Run tests to verify they pass - - Commit message: `feat(#issue): implement [feature]` +3. **REFACTOR** — Clean up the code while keeping tests green + - Improve code quality, remove duplication, enhance readability + - Ensure all tests still pass after refactoring + - Commit message: `refactor(#issue): improve [component]` - 3. **REFACTOR** — Clean up the code while keeping tests green - - Improve code quality, remove duplication, enhance readability - - Ensure all tests still pass after refactoring - - Commit message: `refactor(#issue): improve [component]` +Testing Requirements - Testing Requirements +- **Minimum 85% code coverage** for all new code +- **Write tests BEFORE implementation** — no exceptions +- Test files must be co-located with source files: + - `feature.service.ts` → `feature.service.spec.ts` + - `component.tsx` → `component.test.tsx` +- All tests must pass before creating a PR +- Use descriptive test names: `it("should return user when valid token provided")` +- Group related tests with `describe()` blocks +- Mock external dependencies (database, APIs, file system) - - **Minimum 85% code coverage** for all new code - - **Write tests BEFORE implementation** — no exceptions - - Test files must be co-located with source files: - - `feature.service.ts` → `feature.service.spec.ts` - - `component.tsx` → `component.test.tsx` - - All tests must pass before creating a PR - - Use descriptive test names: `it("should return user when valid token provided")` - - Group related tests with `describe()` blocks - - Mock external dependencies (database, APIs, file system) +Test Types - Test Types +- **Unit Tests** — Test individual functions/methods in isolation +- **Integration Tests** — Test module interactions (e.g., service + database) +- **E2E Tests** — Test complete user workflows with Playwright - - **Unit Tests** — Test individual functions/methods in isolation - - **Integration Tests** — Test module interactions (e.g., service + database) - - **E2E Tests** — Test complete user workflows with Playwright +Running Tests - Running Tests +```bash +pnpm test # Run all tests +pnpm test:watch # Watch mode for active development +pnpm test:coverage # Generate coverage report +pnpm test:api # API tests only +pnpm test:web # Web tests only +pnpm test:e2e # Playwright E2E tests +```` - ```bash - pnpm test # Run all tests - pnpm test:watch # Watch mode for active development - pnpm test:coverage # Generate coverage report - pnpm test:api # API tests only - pnpm test:web # Web tests only - pnpm test:e2e # Playwright E2E tests - ``` +Coverage Verification - Coverage Verification +After implementing a feature, verify coverage meets requirements: - After implementing a feature, verify coverage meets requirements: - ```bash - pnpm test:coverage - # Check the coverage report in coverage/index.html - # Ensure your files show ≥85% coverage - ``` +```bash +pnpm test:coverage +# Check the coverage report in coverage/index.html +# Ensure your files show ≥85% coverage +``` - TDD Anti-Patterns to Avoid +TDD Anti-Patterns to Avoid - ❌ Writing implementation code before tests - ❌ Writing tests after implementation is complete - ❌ Skipping tests for "simple" code - ❌ Testing implementation details instead of behavior - ❌ Writing tests that don't fail when they should - ❌ Committing code with failing tests +❌ Writing implementation code before tests +❌ Writing tests after implementation is complete +❌ Skipping tests for "simple" code +❌ Testing implementation details instead of behavior +❌ Writing tests that don't fail when they should +❌ Committing code with failing tests - Example TDD Session +Quality Rails - Mechanical Code Quality Enforcement - ```bash - # 1. RED - Write failing test - # Edit: feature.service.spec.ts - # Add test for getUserById() - pnpm test:watch # Watch it fail - git add feature.service.spec.ts - git commit -m "test(#42): add test for getUserById" +**Status:** Installed (2026-01-30) - Currently in formatting-only mode - # 2. GREEN - Implement minimal code - # Edit: feature.service.ts - # Add getUserById() method - pnpm test:watch # Watch it pass - git add feature.service.ts - git commit -m "feat(#42): implement getUserById" +Quality Rails provides mechanical enforcement of code quality standards through pre-commit hooks +and CI/CD pipelines. See `docs/quality-rails-status.md` for full details. - # 3. REFACTOR - Improve code quality - # Edit: feature.service.ts - # Extract helper, improve naming - pnpm test:watch # Ensure still passing - git add feature.service.ts - git commit -m "refactor(#42): extract user mapping logic" - ``` +What's Enforced (Once enabled): - Docker Deployment +- ✅ **Type Safety** - Blocks explicit `any` types (@typescript-eslint/no-explicit-any: error) +- ✅ **Return Types** - Requires explicit return types on exported functions +- ✅ **Security** - Detects SQL injection, XSS, unsafe regex (eslint-plugin-security) +- ✅ **Promise Safety** - Blocks floating promises and misused promises +- ✅ **Code Formatting** - Auto-formats with Prettier on commit +- ✅ **Build Verification** - Type-checks before allowing commit +- ✅ **Secret Scanning** - Blocks hardcoded passwords/API keys (git-secrets) - Turnkey (includes everything) +Current Status: - docker compose up -d +- 🟡 **Pre-commit hooks**: Enabled (formatting only until violations fixed) +- 🔴 **Strict enforcement**: Disabled (1,226 existing violations to fix first) +- 🟡 **CI/CD pipeline**: Ready (.woodpecker.yml created, not yet configured) - Customized (external services) +Next Steps: - Create docker-compose.override.yml to: - - Point to external PostgreSQL/Valkey/Ollama - - Disable bundled services +1. Fix existing 1,226 lint violations (priority: explicit `any` types) +2. Enable strict pre-commit enforcement in `.lintstagedrc.mjs` +3. Configure Woodpecker CI to run quality gates on all PRs - See docs/DOCKER.md for details. +Why This Matters: - Key Documentation - ┌───────────────────────────┬───────────────────────┐ - │ Document │ Purpose │ - ├───────────────────────────┼───────────────────────┤ - │ docs/SETUP.md │ Installation guide │ - ├───────────────────────────┼───────────────────────┤ - │ docs/CONFIGURATION.md │ All config options │ - ├───────────────────────────┼───────────────────────┤ - │ docs/DESIGN-PRINCIPLES.md │ PDA-friendly patterns │ - ├───────────────────────────┼───────────────────────┤ - │ docs/DOCKER.md │ Docker deployment │ - ├───────────────────────────┼───────────────────────┤ - │ docs/API.md │ API documentation │ - └───────────────────────────┴───────────────────────┘ - Related Repositories - ┌──────────────┬──────────────────────────────────────────────┐ - │ Repo │ Purpose │ - ├──────────────┼──────────────────────────────────────────────┤ - │ jarvis-brain │ Original JSON-based brain (migration source) │ - ├──────────────┼──────────────────────────────────────────────┤ - │ MoltBot │ Stock messaging gateway │ - └──────────────┴──────────────────────────────────────────────┘ - --- - Mosaic Stack v0.0.x — Building the future of personal assistants. +Based on validation of 50 real production issues, Quality Rails mechanically prevents ~70% +of quality issues including: + +- Hardcoded passwords +- Type safety violations +- SQL injection vulnerabilities +- Build failures +- Test coverage gaps + +**Mechanical enforcement works. Process compliance doesn't.** + +See `docs/quality-rails-status.md` for detailed roadmap and violation breakdown. + +Example TDD Session + +```bash +# 1. RED - Write failing test +# Edit: feature.service.spec.ts +# Add test for getUserById() +pnpm test:watch # Watch it fail +git add feature.service.spec.ts +git commit -m "test(#42): add test for getUserById" + +# 2. GREEN - Implement minimal code +# Edit: feature.service.ts +# Add getUserById() method +pnpm test:watch # Watch it pass +git add feature.service.ts +git commit -m "feat(#42): implement getUserById" + +# 3. REFACTOR - Improve code quality +# Edit: feature.service.ts +# Extract helper, improve naming +pnpm test:watch # Ensure still passing +git add feature.service.ts +git commit -m "refactor(#42): extract user mapping logic" +``` + +Docker Deployment + +Turnkey (includes everything) + +docker compose up -d + +Customized (external services) + +Create docker-compose.override.yml to: + +- Point to external PostgreSQL/Valkey/Ollama +- Disable bundled services + +See docs/DOCKER.md for details. + +Key Documentation +┌───────────────────────────┬───────────────────────┐ +│ Document │ Purpose │ +├───────────────────────────┼───────────────────────┤ +│ docs/SETUP.md │ Installation guide │ +├───────────────────────────┼───────────────────────┤ +│ docs/CONFIGURATION.md │ All config options │ +├───────────────────────────┼───────────────────────┤ +│ docs/DESIGN-PRINCIPLES.md │ PDA-friendly patterns │ +├───────────────────────────┼───────────────────────┤ +│ docs/DOCKER.md │ Docker deployment │ +├───────────────────────────┼───────────────────────┤ +│ docs/API.md │ API documentation │ +└───────────────────────────┴───────────────────────┘ +Related Repositories +┌──────────────┬──────────────────────────────────────────────┐ +│ Repo │ Purpose │ +├──────────────┼──────────────────────────────────────────────┤ +│ jarvis-brain │ Original JSON-based brain (migration source) │ +├──────────────┼──────────────────────────────────────────────┤ +│ MoltBot │ Stock messaging gateway │ +└──────────────┴──────────────────────────────────────────────┘ + +--- + +Mosaic Stack v0.0.x — Building the future of personal assistants. diff --git a/docs/quality-rails-status.md b/docs/quality-rails-status.md new file mode 100644 index 0000000..eeee862 --- /dev/null +++ b/docs/quality-rails-status.md @@ -0,0 +1,197 @@ +# Quality Rails Status + +## Installation Date + +2026-01-30 + +## Current Status: **INSTALLED - PARTIAL ENFORCEMENT** + +Quality Rails has been successfully installed but is currently in **formatting-only mode** due to existing codebase violations. + +## What's Installed + +### ✅ Pre-Commit Hooks (.husky/) + +- Runs lint-staged on every commit +- Currently only enforces Prettier formatting +- Ready to enable full enforcement once violations are fixed + +### ✅ Enhanced ESLint Rules + +Added to `packages/config/eslint/base.js`: + +- `@typescript-eslint/no-explicit-any: "error"` - Block any types +- `@typescript-eslint/explicit-function-return-type: "warn"` - Require return types +- `@typescript-eslint/explicit-module-boundary-types: "error"` - Export type safety +- `eslint-plugin-security` - SQL injection, XSS detection +- Promise/async safety rules +- Code quality improvements + +### ✅ CI/CD Pipeline (.woodpecker.yml) + +Ready to use (not yet configured in CI system): + +- npm audit (dependency security) +- eslint (code quality) +- tsc (type checking) +- vitest (tests + 80% coverage threshold) +- build (compilation) + +### ✅ Dependencies Added + +- husky@9.1.7 - Git hook management +- lint-staged@16.2.7 - Staged file checking +- eslint-plugin-security@3.0.1 - Security vulnerability detection + +## Current Violations + +**Total violations found: 1,226** (1,121 errors, 105 warnings) + +### Breakdown by Category: + +- **Explicit `any` types**: ~400+ violations +- **Unsafe member access**: ~300+ violations +- **Missing return types**: ~200+ violations +- **Code quality issues**: ~105 violations +- **Formatting issues**: ~200+ violations + +### Most Common Violations: + +1. `@typescript-eslint/no-explicit-any` - Unexpected any types +2. `@typescript-eslint/no-unsafe-member-access` - Unsafe any usage +3. `@typescript-eslint/no-unsafe-assignment` - Unsafe any assignment +4. `prettier/prettier` - Formatting inconsistencies +5. `@typescript-eslint/prefer-nullish-coalescing` - Use ?? instead of || + +## Roadmap to Full Enforcement + +### Phase 1: Fix Existing Violations (Current) + +**Goal**: Reduce violations to zero + +**Priority order**: + +1. Security issues (if any from eslint-plugin-security) +2. Explicit `any` types → Replace with proper types +3. Unsafe member access → Add type guards +4. Missing return types → Add explicit types +5. Code quality warnings → Refactor where beneficial + +**Approach**: + +```bash +# Run lint to see all violations +pnpm turbo run lint + +# Fix auto-fixable issues first +pnpm turbo run lint:fix + +# Then manually fix remaining issues package by package +pnpm turbo run lint --filter=@mosaic/api +``` + +**Estimated effort**: 20-40 hours (depending on thoroughness) + +### Phase 2: Enable Strict Pre-Commit Enforcement + +Once violations are at zero, update `.lintstagedrc.mjs`: + +```javascript +export default { + "**/*.{ts,tsx}": (filenames) => { + const packages = [ + ...new Set( + filenames.map((f) => { + const match = f.match(/^(apps|packages)\/([^/]+)\//); + return match ? `@mosaic/${match[2]}` : null; + }) + ), + ].filter(Boolean); + + if (packages.length === 0) return []; + + // STRICT ENFORCEMENT - blocks commits with violations + return packages.map( + (pkg) => `pnpm turbo run lint typecheck --filter=@mosaic/${pkg} -- --max-warnings=0` + ); + }, + + "**/*.{js,jsx,ts,tsx,json,md,yml,yaml}": ["prettier --write"], +}; +``` + +### Phase 3: Enable CI/CD Enforcement + +Configure Woodpecker CI (or GitHub Actions) to run `.woodpecker.yml` pipeline on every PR. + +This will block PRs that: + +- Have dependency vulnerabilities (npm audit) +- Don't pass linting (eslint) +- Don't pass type checking (tsc) +- Have test failures or <80% coverage +- Don't build successfully + +## Testing Enforcement + +### Test that pre-commit hooks work: + +```bash +# Create a file with violations +echo 'export function bad(x: any) { return x; }' > test.ts +git add test.ts +git commit -m "test" +# Should be BLOCKED once strict enforcement is enabled +``` + +### Test that CI enforcement works: + +```bash +# Push a branch with violations +# CI should fail the build +``` + +## Benefits Once Fully Enabled + +Based on Quality Rails validation of 50 real production issues: + +| Issue Category | Current Status | After Full Enforcement | +| ------------------- | -------------------- | ----------------------------- | +| Hardcoded passwords | Possible | ✅ BLOCKED by git-secrets | +| SQL injection | Possible | ✅ BLOCKED by security plugin | +| Type safety (`any`) | **1,121 violations** | ✅ BLOCKED by no-explicit-any | +| Silent failures | Partial protection | ⚠️ Partially blocked | +| Test coverage gaps | Not enforced | ✅ BLOCKED by 80% threshold | +| Build failures | Not enforced | ✅ BLOCKED by pre-commit tsc | +| Dependency CVEs | Not enforced | ✅ BLOCKED by npm audit | + +**Expected impact: ~70% of quality issues prevented mechanically** + +## Notes + +### git-secrets (Optional) + +The pre-commit hook tries to run `git-secrets` but falls back gracefully if not installed. + +To install git-secrets for secret scanning: + +```bash +# Install git-secrets (platform-specific) +# Then configure patterns: +git secrets --add 'password\s*=\s*["\'].*["\']' +git secrets --add 'api[_-]?key\s*=\s*["\'].*["\']' +``` + +### Turbo Caching + +Turbo caches lint and typecheck results, so repeated runs are fast. Only changed packages are re-checked. + +### IDE Integration + +ESLint rules are enforced in VSCode/other IDEs automatically. Developers will see errors in real-time before committing. + +## Questions? + +- See quality-rails documentation: `~/src/quality-rails/` +- See PHILOSOPHY.md for why mechanical enforcement matters +- Check existing issues for progress on fixing violations diff --git a/docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-.lintstagedrc.js_20260130-1310_1_remediation_needed.md b/docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-.lintstagedrc.js_20260130-1310_1_remediation_needed.md new file mode 100644 index 0000000..e42930c --- /dev/null +++ b/docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-.lintstagedrc.js_20260130-1310_1_remediation_needed.md @@ -0,0 +1,20 @@ +# QA Remediation Report + +**File:** /home/localadmin/src/mosaic-stack/.lintstagedrc.js +**Tool Used:** Edit +**Epic:** general +**Iteration:** 1 +**Generated:** 2026-01-30 13:10:12 + +## Status + +Pending QA validation + +## Next Steps + +This report was created by the QA automation hook. +To process this report, run: + +```bash +claude -p "Use Task tool to launch universal-qa-agent for report: /home/localadmin/src/mosaic-stack/docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-.lintstagedrc.js_20260130-1310_1_remediation_needed.md" +``` diff --git a/docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-.lintstagedrc.mjs_20260130-1312_1_remediation_needed.md b/docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-.lintstagedrc.mjs_20260130-1312_1_remediation_needed.md new file mode 100644 index 0000000..52cb456 --- /dev/null +++ b/docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-.lintstagedrc.mjs_20260130-1312_1_remediation_needed.md @@ -0,0 +1,20 @@ +# QA Remediation Report + +**File:** /home/localadmin/src/mosaic-stack/.lintstagedrc.mjs +**Tool Used:** Edit +**Epic:** general +**Iteration:** 1 +**Generated:** 2026-01-30 13:12:00 + +## Status + +Pending QA validation + +## Next Steps + +This report was created by the QA automation hook. +To process this report, run: + +```bash +claude -p "Use Task tool to launch universal-qa-agent for report: /home/localadmin/src/mosaic-stack/docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-.lintstagedrc.mjs_20260130-1312_1_remediation_needed.md" +``` diff --git a/docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-.lintstagedrc.mjs_20260130-1312_2_remediation_needed.md b/docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-.lintstagedrc.mjs_20260130-1312_2_remediation_needed.md new file mode 100644 index 0000000..69d6337 --- /dev/null +++ b/docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-.lintstagedrc.mjs_20260130-1312_2_remediation_needed.md @@ -0,0 +1,20 @@ +# QA Remediation Report + +**File:** /home/localadmin/src/mosaic-stack/.lintstagedrc.mjs +**Tool Used:** Edit +**Epic:** general +**Iteration:** 2 +**Generated:** 2026-01-30 13:12:17 + +## Status + +Pending QA validation + +## Next Steps + +This report was created by the QA automation hook. +To process this report, run: + +```bash +claude -p "Use Task tool to launch universal-qa-agent for report: /home/localadmin/src/mosaic-stack/docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-.lintstagedrc.mjs_20260130-1312_2_remediation_needed.md" +``` diff --git a/docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-.lintstagedrc.mjs_20260130-1312_3_remediation_needed.md b/docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-.lintstagedrc.mjs_20260130-1312_3_remediation_needed.md new file mode 100644 index 0000000..0d8b456 --- /dev/null +++ b/docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-.lintstagedrc.mjs_20260130-1312_3_remediation_needed.md @@ -0,0 +1,20 @@ +# QA Remediation Report + +**File:** /home/localadmin/src/mosaic-stack/.lintstagedrc.mjs +**Tool Used:** Edit +**Epic:** general +**Iteration:** 3 +**Generated:** 2026-01-30 13:12:44 + +## Status + +Pending QA validation + +## Next Steps + +This report was created by the QA automation hook. +To process this report, run: + +```bash +claude -p "Use Task tool to launch universal-qa-agent for report: /home/localadmin/src/mosaic-stack/docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-.lintstagedrc.mjs_20260130-1312_3_remediation_needed.md" +``` diff --git a/docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-.lintstagedrc.mjs_20260130-1312_4_remediation_needed.md b/docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-.lintstagedrc.mjs_20260130-1312_4_remediation_needed.md new file mode 100644 index 0000000..924cfa7 --- /dev/null +++ b/docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-.lintstagedrc.mjs_20260130-1312_4_remediation_needed.md @@ -0,0 +1,20 @@ +# QA Remediation Report + +**File:** /home/localadmin/src/mosaic-stack/.lintstagedrc.mjs +**Tool Used:** Edit +**Epic:** general +**Iteration:** 4 +**Generated:** 2026-01-30 13:12:59 + +## Status + +Pending QA validation + +## Next Steps + +This report was created by the QA automation hook. +To process this report, run: + +```bash +claude -p "Use Task tool to launch universal-qa-agent for report: /home/localadmin/src/mosaic-stack/docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-.lintstagedrc.mjs_20260130-1312_4_remediation_needed.md" +``` diff --git a/docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-packages-config-eslint-base.js_20260130-1309_1_remediation_needed.md b/docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-packages-config-eslint-base.js_20260130-1309_1_remediation_needed.md new file mode 100644 index 0000000..30f75ef --- /dev/null +++ b/docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-packages-config-eslint-base.js_20260130-1309_1_remediation_needed.md @@ -0,0 +1,20 @@ +# QA Remediation Report + +**File:** /home/localadmin/src/mosaic-stack/packages/config/eslint/base.js +**Tool Used:** Edit +**Epic:** general +**Iteration:** 1 +**Generated:** 2026-01-30 13:09:17 + +## Status + +Pending QA validation + +## Next Steps + +This report was created by the QA automation hook. +To process this report, run: + +```bash +claude -p "Use Task tool to launch universal-qa-agent for report: /home/localadmin/src/mosaic-stack/docs/reports/qa-automation/pending/home-localadmin-src-mosaic-stack-packages-config-eslint-base.js_20260130-1309_1_remediation_needed.md" +``` diff --git a/docs/reports/qa-automation/pending/tmp-claude-1000--home-localadmin-src-mosaic-stack-f3beb7a6-6cd5-4bee-8283-fac0798a92fa-scratchpad-test-violations.ts_20260130-1309_1_remediation_needed.md b/docs/reports/qa-automation/pending/tmp-claude-1000--home-localadmin-src-mosaic-stack-f3beb7a6-6cd5-4bee-8283-fac0798a92fa-scratchpad-test-violations.ts_20260130-1309_1_remediation_needed.md new file mode 100644 index 0000000..680067d --- /dev/null +++ b/docs/reports/qa-automation/pending/tmp-claude-1000--home-localadmin-src-mosaic-stack-f3beb7a6-6cd5-4bee-8283-fac0798a92fa-scratchpad-test-violations.ts_20260130-1309_1_remediation_needed.md @@ -0,0 +1,20 @@ +# QA Remediation Report + +**File:** /tmp/claude-1000/-home-localadmin-src-mosaic-stack/f3beb7a6-6cd5-4bee-8283-fac0798a92fa/scratchpad/test-violations.ts +**Tool Used:** Write +**Epic:** general +**Iteration:** 1 +**Generated:** 2026-01-30 13:09:55 + +## Status + +Pending QA validation + +## Next Steps + +This report was created by the QA automation hook. +To process this report, run: + +```bash +claude -p "Use Task tool to launch universal-qa-agent for report: /home/localadmin/src/mosaic-stack/docs/reports/qa-automation/pending/tmp-claude-1000--home-localadmin-src-mosaic-stack-f3beb7a6-6cd5-4bee-8283-fac0798a92fa-scratchpad-test-violations.ts_20260130-1309_1_remediation_needed.md" +``` diff --git a/package.json b/package.json index b441343..78c9d17 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "docker:logs": "docker compose logs -f", "docker:ps": "docker compose ps", "docker:build": "docker compose build", - "docker:restart": "docker compose restart" + "docker:restart": "docker compose restart", + "prepare": "husky install" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^8.26.0", @@ -35,6 +36,9 @@ "eslint": "^9.21.0", "eslint-config-prettier": "^10.1.0", "eslint-plugin-prettier": "^5.2.3", + "eslint-plugin-security": "^3.0.1", + "husky": "^9.1.7", + "lint-staged": "^16.2.7", "prettier": "^3.5.3", "turbo": "^2.8.0", "typescript": "^5.8.2", diff --git a/packages/config/eslint/base.js b/packages/config/eslint/base.js index b0cae96..ed9b32e 100644 --- a/packages/config/eslint/base.js +++ b/packages/config/eslint/base.js @@ -2,6 +2,8 @@ import eslint from "@eslint/js"; import tseslint from "typescript-eslint"; import prettierConfig from "eslint-config-prettier"; import prettierPlugin from "eslint-plugin-prettier"; +// @ts-expect-error - security plugin doesn't have types +import securityPlugin from "eslint-plugin-security"; export default tseslint.config( eslint.configs.recommended, @@ -11,19 +13,42 @@ export default tseslint.config( { plugins: { prettier: prettierPlugin, + security: securityPlugin, }, rules: { + // Prettier "prettier/prettier": "error", + + // Type Safety - STRICT (Quality Rails) + "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/explicit-function-return-type": "warn", + "@typescript-eslint/explicit-module-boundary-types": "error", "@typescript-eslint/no-unused-vars": [ "error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, ], - "@typescript-eslint/consistent-type-imports": [ - "error", - { prefer: "type-imports" }, - ], + "@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }], + + // Promise/Async Safety (Quality Rails) "@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-misused-promises": "error", + "@typescript-eslint/await-thenable": "error", + + // Code Quality (Quality Rails) + "@typescript-eslint/no-var-requires": "error", + "@typescript-eslint/prefer-nullish-coalescing": "warn", + "@typescript-eslint/prefer-optional-chain": "warn", + + // Security (Quality Rails) + "security/detect-object-injection": "off", // Too many false positives + "security/detect-non-literal-fs-filename": "warn", + "security/detect-non-literal-regexp": "warn", + "security/detect-unsafe-regex": "error", + "security/detect-buffer-noassert": "error", + "security/detect-eval-with-expression": "error", + "security/detect-no-csrf-before-method-override": "error", + "security/detect-possible-timing-attacks": "warn", + "security/detect-pseudoRandomBytes": "error", }, }, { diff --git a/packages/config/package.json b/packages/config/package.json index 5367f64..98de936 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -20,6 +20,7 @@ "eslint": "^9.21.0", "eslint-config-prettier": "^10.1.0", "eslint-plugin-prettier": "^5.2.3", + "eslint-plugin-security": "^3.0.1", "prettier": "^3.5.3", "typescript-eslint": "^8.26.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 77d89cf..c390741 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,7 +16,7 @@ importers: version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@vitest/coverage-v8': specifier: ^4.0.18 - version: 4.0.18(vitest@3.2.4(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)) + version: 4.0.18(vitest@3.2.4(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) eslint: specifier: ^9.21.0 version: 9.39.2(jiti@2.6.1) @@ -26,6 +26,15 @@ importers: eslint-plugin-prettier: specifier: ^5.2.3 version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.1) + eslint-plugin-security: + specifier: ^3.0.1 + version: 3.0.1 + husky: + specifier: ^9.1.7 + version: 9.1.7 + lint-staged: + specifier: ^16.2.7 + version: 16.2.7 prettier: specifier: ^3.5.3 version: 3.8.1 @@ -37,7 +46,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.8 - version: 3.2.4(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0) + version: 3.2.4(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) apps/api: dependencies: @@ -68,15 +77,24 @@ importers: '@types/marked': specifier: ^6.0.0 version: 6.0.0 + adm-zip: + specifier: ^0.5.16 + version: 0.5.16 + archiver: + specifier: ^7.0.1 + version: 7.0.1 better-auth: specifier: ^1.4.17 - version: 1.4.17(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.6.2)(drizzle-orm@0.41.0(@prisma/client@5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(next@16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.17.2)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.0.18(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)) + version: 1.4.17(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.6.2)(drizzle-orm@0.41.0(@prisma/client@5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(next@16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.17.2)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.0.18(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) class-transformer: specifier: ^0.5.1 version: 0.5.1 class-validator: specifier: ^0.14.3 version: 0.14.3 + gray-matter: + specifier: ^4.0.3 + version: 4.0.3 highlight.js: specifier: ^11.11.1 version: 11.11.1 @@ -113,7 +131,7 @@ importers: devDependencies: '@better-auth/cli': specifier: ^1.4.17 - version: 1.4.17(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.10)(magicast@0.3.5)(nanostores@1.1.0)(next@16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.0.18(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)) + version: 1.4.17(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.10)(magicast@0.3.5)(nanostores@1.1.0)(next@16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.0.18(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) '@mosaic/config': specifier: workspace:* version: link:../../packages/config @@ -129,15 +147,21 @@ importers: '@swc/core': specifier: ^1.10.18 version: 1.15.11 + '@types/adm-zip': + specifier: ^0.5.7 + version: 0.5.7 + '@types/archiver': + specifier: ^7.0.0 + version: 7.0.0 '@types/express': specifier: ^5.0.1 version: 5.0.6 '@types/highlight.js': specifier: ^10.1.0 version: 10.1.0 - '@types/ioredis': - specifier: ^5.0.0 - version: 5.0.0 + '@types/multer': + specifier: ^2.0.0 + version: 2.0.0 '@types/node': specifier: ^22.13.4 version: 22.19.7 @@ -146,7 +170,7 @@ importers: version: 2.16.0 '@vitest/coverage-v8': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)) + version: 4.0.18(vitest@4.0.18(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) express: specifier: ^5.2.1 version: 5.2.1 @@ -164,7 +188,7 @@ importers: version: 1.5.9(@swc/core@1.15.11)(rollup@4.57.0) vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0) + version: 4.0.18(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) apps/web: dependencies: @@ -191,7 +215,7 @@ importers: version: 12.10.0(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) better-auth: specifier: ^1.4.17 - version: 1.4.17(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.6.2)(drizzle-orm@0.41.0(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(next@16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.17.2)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)) + version: 1.4.17(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.6.2)(drizzle-orm@0.41.0(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(next@16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.17.2)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) date-fns: specifier: ^4.1.0 version: 4.1.0 @@ -246,10 +270,10 @@ importers: version: 2.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)) + version: 4.7.0(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/coverage-v8': specifier: ^3.2.4 - version: 3.2.4(vitest@3.2.4(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)) + version: 3.2.4(vitest@3.2.4(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) jsdom: specifier: ^26.0.0 version: 26.1.0 @@ -258,7 +282,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.8 - version: 3.2.4(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0) + version: 3.2.4(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) packages/config: dependencies: @@ -280,6 +304,9 @@ importers: eslint-plugin-prettier: specifier: ^5.2.3 version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.1) + eslint-plugin-security: + specifier: ^3.0.1 + version: 3.0.1 prettier: specifier: ^3.5.3 version: 3.8.1 @@ -301,7 +328,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.8 - version: 3.2.4(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0) + version: 3.2.4(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) packages/ui: dependencies: @@ -329,7 +356,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.8 - version: 3.2.4(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0) + version: 3.2.4(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) packages: @@ -1698,6 +1725,12 @@ packages: '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@types/adm-zip@0.5.7': + resolution: {integrity: sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw==} + + '@types/archiver@7.0.0': + resolution: {integrity: sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==} + '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -1846,10 +1879,6 @@ packages: '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} - '@types/ioredis@5.0.0': - resolution: {integrity: sha512-zJbJ3FVE17CNl5KXzdeSPtdltc4tMT3TzC6fxQS0sQngkbFZ6h+0uTafsRqu+eSLIugf6Yb0Ea0SUuRr42Nk9g==} - deprecated: This is a stub types definition. ioredis provides its own type definitions, so you do not need this installed. - '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1857,6 +1886,9 @@ packages: resolution: {integrity: sha512-jmjpa4BwUsmhxcfsgUit/7A9KbrC48Q0q8KvnY107ogcjGgTFDlIL3RpihNpx2Mu1hM4mdFQjoVc4O6JoGKHsA==} deprecated: This is a stub types definition. marked provides its own type definitions, so you do not need this installed. + '@types/multer@2.0.0': + resolution: {integrity: sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==} + '@types/node@22.19.7': resolution: {integrity: sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==} @@ -1881,6 +1913,9 @@ packages: '@types/react@19.2.10': resolution: {integrity: sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==} + '@types/readdir-glob@1.1.5': + resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==} + '@types/sanitize-html@2.16.0': resolution: {integrity: sha512-l6rX1MUXje5ztPT0cAFtUayXF06DqPhRyfVXareEN5gGCFaP/iwsxIyKODr9XDhfxPpN6vXUFNfo5kZMXCxBtw==} @@ -2097,6 +2132,10 @@ packages: '@xyflow/system@0.0.74': resolution: {integrity: sha512-7v7B/PkiVrkdZzSbL+inGAo6tkR/WQHHG0/jhSvLQToCsfa8YubOGmBYd1s08tpKpihdHDZFwzQZeR69QSBb4Q==} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -2121,6 +2160,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + adm-zip@0.5.16: + resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==} + engines: {node: '>=12.0'} + agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -2161,6 +2204,10 @@ packages: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} + ansi-escapes@7.2.0: + resolution: {integrity: sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==} + engines: {node: '>=18'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -2188,6 +2235,17 @@ packages: append-field@1.0.0: resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + archiver-utils@5.0.2: + resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} + engines: {node: '>= 14'} + + archiver@7.0.1: + resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} + engines: {node: '>= 14'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -2208,9 +2266,28 @@ packages: ast-v8-to-istanbul@0.3.10: resolution: {integrity: sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ==} + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + b4a@1.7.3: + resolution: {integrity: sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + bare-events@2.8.2: + resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -2312,17 +2389,28 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + browserslist@4.28.1: resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} @@ -2438,6 +2526,10 @@ packages: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + cli-spinners@2.9.2: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} @@ -2446,6 +2538,10 @@ packages: resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} engines: {node: 10.* || >= 12.*} + cli-truncate@5.1.1: + resolution: {integrity: sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==} + engines: {node: '>=20'} + cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} @@ -2472,10 +2568,17 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} + commander@14.0.2: + resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} + engines: {node: '>=20'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -2495,6 +2598,10 @@ packages: resolution: {integrity: sha512-r1To31BQD5060QdkC+Iheai7gHwoSZobzunqkf2/kQ6xIAfJyrKNAFUwdKvkK7Qgu7pVTKQEa7ok7Ed3ycAJgg==} engines: {node: '>= 6'} + compress-commons@6.0.2: + resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} + engines: {node: '>= 14'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -2553,6 +2660,15 @@ packages: typescript: optional: true + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@6.0.0: + resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} + engines: {node: '>= 14'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -2946,6 +3062,9 @@ packages: elkjs@0.9.3: resolution: {integrity: sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==} + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -2986,6 +3105,10 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} @@ -3043,6 +3166,10 @@ packages: eslint-config-prettier: optional: true + eslint-plugin-security@3.0.1: + resolution: {integrity: sha512-XjVGBhtDZJfyuhIxnQ/WMm385RbX3DBu7H1J7HNNhmB2tnGxMeqVSnYv79oAj992ayvIBZghsymwkYFS6cGH4Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -3108,6 +3235,16 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -3127,6 +3264,10 @@ packages: exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + fast-check@3.23.2: resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} engines: {node: '>=8.0.0'} @@ -3140,6 +3281,9 @@ packages: fast-equals@4.0.3: resolution: {integrity: sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==} + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -3172,6 +3316,10 @@ packages: file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + finalhandler@2.1.1: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} @@ -3228,6 +3376,10 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-east-asian-width@1.4.0: + resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -3275,6 +3427,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + hachure-fill@0.5.2: resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} @@ -3316,6 +3472,11 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -3376,6 +3537,10 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -3384,6 +3549,10 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} + engines: {node: '>=18'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -3397,6 +3566,10 @@ packages: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + is-plain-object@5.0.0: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} @@ -3407,6 +3580,10 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -3415,6 +3592,9 @@ packages: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -3458,6 +3638,10 @@ packages: js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true @@ -3512,6 +3696,10 @@ packages: khroma@2.1.0: resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} @@ -3530,6 +3718,10 @@ packages: layout-base@2.0.1: resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -3544,6 +3736,15 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + lint-staged@16.2.7: + resolution: {integrity: sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==} + engines: {node: '>=20.17'} + hasBin: true + + listr2@9.0.5: + resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} + engines: {node: '>=20.0.0'} + load-esm@1.0.3: resolution: {integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==} engines: {node: '>=13.2.0'} @@ -3585,6 +3786,10 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -3673,6 +3878,10 @@ packages: mermaid@11.12.2: resolution: {integrity: sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w==} + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -3693,6 +3902,10 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} @@ -3708,6 +3921,10 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -3740,6 +3957,10 @@ packages: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} + nano-spawn@2.0.0: + resolution: {integrity: sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==} + engines: {node: '>=20.17'} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -3803,6 +4024,10 @@ packages: node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + nwsapi@2.2.23: resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} @@ -3843,6 +4068,10 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + open@10.2.0: resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} engines: {node: '>=18'} @@ -3963,6 +4192,10 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + picomatch@4.0.2: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} @@ -3971,6 +4204,11 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -4043,6 +4281,13 @@ packages: typescript: optional: true + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -4123,10 +4368,20 @@ packages: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -4153,6 +4408,10 @@ packages: regexp-to-ast@0.5.0: resolution: {integrity: sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==} + regexp-tree@0.1.27: + resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} + hasBin: true + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -4171,6 +4430,13 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + robust-predicates@3.0.2: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} @@ -4205,9 +4471,15 @@ packages: rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-regex@2.1.1: + resolution: {integrity: sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -4229,6 +4501,10 @@ packages: resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} engines: {node: '>= 10.13.0'} + section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -4302,6 +4578,10 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + slice-ansi@7.1.2: + resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} + engines: {node: '>=18'} + slugify@1.6.6: resolution: {integrity: sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==} engines: {node: '>=8.0.0'} @@ -4340,6 +4620,9 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -4357,6 +4640,13 @@ packages: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} + streamx@2.23.0: + resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==} + + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -4365,6 +4655,17 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.1.1: + resolution: {integrity: sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw==} + engines: {node: '>=20'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -4376,6 +4677,10 @@ packages: resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} + strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -4445,6 +4750,9 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} + tar-stream@3.1.7: + resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + terser-webpack-plugin@5.3.16: resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} engines: {node: '>= 10.13.0'} @@ -4470,6 +4778,9 @@ packages: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} + text-decoder@1.2.3: + resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -4507,6 +4818,10 @@ packages: resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} hasBin: true + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -4878,6 +5193,10 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -4927,6 +5246,11 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -4947,6 +5271,10 @@ packages: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} + zip-stream@6.0.1: + resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} + engines: {node: '>= 14'} + zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} @@ -5282,7 +5610,7 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@better-auth/cli@1.4.17(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.10)(magicast@0.3.5)(nanostores@1.1.0)(next@16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.0.18(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0))': + '@better-auth/cli@1.4.17(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.10)(magicast@0.3.5)(nanostores@1.1.0)(next@16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.0.18(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/core': 7.28.6 '@babel/preset-react': 7.28.5(@babel/core@7.28.6) @@ -5294,7 +5622,7 @@ snapshots: '@mrleebo/prisma-ast': 0.13.1 '@prisma/client': 5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)) '@types/pg': 8.16.0 - better-auth: 1.4.17(@prisma/client@5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(better-sqlite3@12.6.2)(drizzle-orm@0.41.0(@prisma/client@5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(next@16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.17.2)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.0.18(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)) + better-auth: 1.4.17(@prisma/client@5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(better-sqlite3@12.6.2)(drizzle-orm@0.41.0(@prisma/client@5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(next@16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.17.2)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.0.18(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) better-sqlite3: 12.6.2 c12: 3.3.3(magicast@0.3.5) chalk: 5.6.2 @@ -6303,6 +6631,14 @@ snapshots: '@tokenizer/token@0.3.0': {} + '@types/adm-zip@0.5.7': + dependencies: + '@types/node': 22.19.7 + + '@types/archiver@7.0.0': + dependencies: + '@types/readdir-glob': 1.1.5 + '@types/aria-query@5.0.4': {} '@types/babel__core@7.20.5': @@ -6496,18 +6832,16 @@ snapshots: '@types/http-errors@2.0.5': {} - '@types/ioredis@5.0.0': - dependencies: - ioredis: 5.9.2 - transitivePeerDependencies: - - supports-color - '@types/json-schema@7.0.15': {} '@types/marked@6.0.0': dependencies: marked: 17.0.1 + '@types/multer@2.0.0': + dependencies: + '@types/express': 5.0.6 + '@types/node@22.19.7': dependencies: undici-types: 6.21.0 @@ -6537,6 +6871,10 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/readdir-glob@1.1.5': + dependencies: + '@types/node': 22.19.7 + '@types/sanitize-html@2.16.0': dependencies: htmlparser2: 8.0.2 @@ -6646,7 +6984,7 @@ snapshots: '@typescript-eslint/types': 8.54.0 eslint-visitor-keys: 4.2.1 - '@vitejs/plugin-react@4.7.0(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0))': + '@vitejs/plugin-react@4.7.0(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/core': 7.28.6 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.6) @@ -6654,11 +6992,11 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 7.3.1(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0) + vite: 7.3.1(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -6673,11 +7011,11 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0) + vitest: 3.2.4(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@4.0.18(vitest@3.2.4(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0))': + '@vitest/coverage-v8@4.0.18(vitest@3.2.4(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.0.18 @@ -6689,9 +7027,9 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 3.2.4(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0) + vitest: 3.2.4(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0))': + '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.0.18 @@ -6703,7 +7041,7 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.18(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0) + vitest: 4.0.18(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) '@vitest/expect@3.2.4': dependencies: @@ -6722,21 +7060,21 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0))': + '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0) + vite: 7.3.1(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0))': + '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0) + vite: 7.3.1(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) '@vitest/pretty-format@3.2.4': dependencies: @@ -6889,6 +7227,10 @@ snapshots: d3-selection: 3.0.0 d3-zoom: 3.0.0 + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -6909,6 +7251,8 @@ snapshots: acorn@8.15.0: {} + adm-zip@0.5.16: {} + agent-base@7.1.4: {} ajv-formats@2.1.1(ajv@8.17.1): @@ -6944,6 +7288,10 @@ snapshots: ansi-colors@4.1.3: {} + ansi-escapes@7.2.0: + dependencies: + environment: 1.1.0 + ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} @@ -6960,6 +7308,33 @@ snapshots: append-field@1.0.0: {} + archiver-utils@5.0.2: + dependencies: + glob: 10.5.0 + graceful-fs: 4.2.11 + is-stream: 2.0.1 + lazystream: 1.0.1 + lodash: 4.17.23 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + + archiver@7.0.1: + dependencies: + archiver-utils: 5.0.2 + async: 3.2.6 + buffer-crc32: 1.0.0 + readable-stream: 4.7.0 + readdir-glob: 1.1.3 + tar-stream: 3.1.7 + zip-stream: 6.0.1 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + argparse@2.0.1: {} aria-query@5.3.0: @@ -6978,15 +7353,21 @@ snapshots: estree-walker: 3.0.3 js-tokens: 9.0.1 + async@3.2.6: {} + + b4a@1.7.3: {} + balanced-match@1.0.2: {} + bare-events@2.8.2: {} + base64-js@1.5.1: {} base64id@2.0.0: {} baseline-browser-mapping@2.9.19: {} - better-auth@1.4.17(@prisma/client@5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(better-sqlite3@12.6.2)(drizzle-orm@0.41.0(@prisma/client@5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(next@16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.17.2)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.0.18(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)): + better-auth@1.4.17(@prisma/client@5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(better-sqlite3@12.6.2)(drizzle-orm@0.41.0(@prisma/client@5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(next@16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.17.2)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.0.18(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): dependencies: '@better-auth/core': 1.4.17(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.10)(nanostores@1.1.0) '@better-auth/telemetry': 1.4.17(@better-auth/core@1.4.17(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.10)(nanostores@1.1.0)) @@ -7009,9 +7390,9 @@ snapshots: prisma: 6.19.2(magicast@0.3.5)(typescript@5.9.3) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - vitest: 4.0.18(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0) + vitest: 4.0.18(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - better-auth@1.4.17(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.6.2)(drizzle-orm@0.41.0(@prisma/client@5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(next@16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.17.2)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.0.18(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)): + better-auth@1.4.17(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.6.2)(drizzle-orm@0.41.0(@prisma/client@5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(next@16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.17.2)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.0.18(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): dependencies: '@better-auth/core': 1.4.17(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.10)(nanostores@1.1.0) '@better-auth/telemetry': 1.4.17(@better-auth/core@1.4.17(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.10)(nanostores@1.1.0)) @@ -7034,9 +7415,9 @@ snapshots: prisma: 6.19.2(magicast@0.3.5)(typescript@5.9.3) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - vitest: 4.0.18(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0) + vitest: 4.0.18(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - better-auth@1.4.17(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.6.2)(drizzle-orm@0.41.0(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(next@16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.17.2)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)): + better-auth@1.4.17(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.6.2)(drizzle-orm@0.41.0(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(next@16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.17.2)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): dependencies: '@better-auth/core': 1.4.17(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.10)(nanostores@1.1.0) '@better-auth/telemetry': 1.4.17(@better-auth/core@1.4.17(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.10)(nanostores@1.1.0)) @@ -7059,7 +7440,7 @@ snapshots: prisma: 6.19.2(magicast@0.3.5)(typescript@5.9.3) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - vitest: 3.2.4(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0) + vitest: 3.2.4(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) better-call@1.1.8(zod@4.3.6): dependencies: @@ -7108,6 +7489,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.9.19 @@ -7116,6 +7501,8 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) + buffer-crc32@1.0.0: {} + buffer-from@1.1.2: {} buffer@5.7.1: @@ -7123,6 +7510,11 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + bundle-name@4.1.0: dependencies: run-applescript: 7.1.0 @@ -7259,6 +7651,10 @@ snapshots: dependencies: restore-cursor: 3.1.0 + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + cli-spinners@2.9.2: {} cli-table3@0.6.5: @@ -7267,6 +7663,11 @@ snapshots: optionalDependencies: '@colors/colors': 1.5.0 + cli-truncate@5.1.1: + dependencies: + slice-ansi: 7.1.2 + string-width: 8.1.1 + cli-width@4.1.0: {} client-only@0.0.1: {} @@ -7283,8 +7684,12 @@ snapshots: color-name@1.1.4: {} + colorette@2.0.20: {} + commander@12.1.0: {} + commander@14.0.2: {} + commander@2.20.3: {} commander@4.1.1: {} @@ -7299,6 +7704,14 @@ snapshots: core-util-is: 1.0.3 esprima: 4.0.1 + compress-commons@6.0.2: + dependencies: + crc-32: 1.2.2 + crc32-stream: 6.0.0 + is-stream: 2.0.1 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + concat-map@0.0.1: {} concat-stream@2.0.0: @@ -7348,6 +7761,13 @@ snapshots: optionalDependencies: typescript: 5.9.3 + crc-32@1.2.2: {} + + crc32-stream@6.0.0: + dependencies: + crc-32: 1.2.2 + readable-stream: 4.7.0 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -7663,6 +8083,8 @@ snapshots: elkjs@0.9.3: {} + emoji-regex@10.6.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -7714,6 +8136,8 @@ snapshots: entities@6.0.1: {} + environment@1.1.0: {} + error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 @@ -7779,6 +8203,10 @@ snapshots: '@types/eslint': 9.6.1 eslint-config-prettier: 10.1.8(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-security@3.0.1: + dependencies: + safe-regex: 2.1.1 + eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 @@ -7864,6 +8292,16 @@ snapshots: etag@1.8.1: {} + event-target-shim@5.0.1: {} + + eventemitter3@5.0.4: {} + + events-universal@1.0.1: + dependencies: + bare-events: 2.8.2 + transitivePeerDependencies: + - bare-abort-controller + events@3.3.0: {} expand-template@2.0.3: {} @@ -7905,6 +8343,10 @@ snapshots: exsolve@1.0.8: {} + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + fast-check@3.23.2: dependencies: pure-rand: 6.1.0 @@ -7915,6 +8357,8 @@ snapshots: fast-equals@4.0.3: {} + fast-fifo@1.3.2: {} + fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} @@ -7942,6 +8386,10 @@ snapshots: file-uri-to-path@1.0.0: {} + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + finalhandler@2.1.1: dependencies: debug: 4.4.3 @@ -8008,6 +8456,8 @@ snapshots: gensync@1.0.0-beta.2: {} + get-east-asian-width@1.4.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -8070,6 +8520,13 @@ snapshots: graceful-fs@4.2.11: {} + gray-matter@4.0.3: + dependencies: + js-yaml: 3.14.2 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + hachure-fill@0.5.2: {} has-flag@4.0.0: {} @@ -8117,6 +8574,8 @@ snapshots: transitivePeerDependencies: - supports-color + husky@9.1.7: {} + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -8168,10 +8627,16 @@ snapshots: is-docker@3.0.0: {} + is-extendable@0.1.1: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.4.0 + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -8182,18 +8647,24 @@ snapshots: is-interactive@1.0.0: {} + is-number@7.0.0: {} + is-plain-object@5.0.0: {} is-potential-custom-element-name@1.0.1: {} is-promise@4.0.0: {} + is-stream@2.0.1: {} + is-unicode-supported@0.1.0: {} is-wsl@3.1.0: dependencies: is-inside-container: 1.0.0 + isarray@1.0.0: {} + isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} @@ -8239,6 +8710,11 @@ snapshots: js-tokens@9.0.1: {} + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -8302,6 +8778,8 @@ snapshots: khroma@2.1.0: {} + kind-of@6.0.3: {} + kleur@3.0.3: {} kysely@0.28.10: {} @@ -8318,6 +8796,10 @@ snapshots: layout-base@2.0.1: {} + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -8329,6 +8811,25 @@ snapshots: lines-and-columns@1.2.4: {} + lint-staged@16.2.7: + dependencies: + commander: 14.0.2 + listr2: 9.0.5 + micromatch: 4.0.8 + nano-spawn: 2.0.0 + pidtree: 0.6.0 + string-argv: 0.3.2 + yaml: 2.8.2 + + listr2@9.0.5: + dependencies: + cli-truncate: 5.1.1 + colorette: 2.0.20 + eventemitter3: 5.0.4 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.2 + load-esm@1.0.3: {} load-tsconfig@0.2.5: {} @@ -8358,6 +8859,14 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 + log-update@6.1.0: + dependencies: + ansi-escapes: 7.2.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.2 + strip-ansi: 7.1.2 + wrap-ansi: 9.0.2 + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -8452,6 +8961,11 @@ snapshots: ts-dedent: 2.2.0 uuid: 11.1.0 + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + mime-db@1.52.0: {} mime-db@1.54.0: {} @@ -8466,6 +8980,8 @@ snapshots: mimic-fn@2.1.0: {} + mimic-function@5.0.1: {} + mimic-response@3.1.0: {} min-indent@1.0.1: {} @@ -8478,6 +8994,10 @@ snapshots: dependencies: brace-expansion: 1.1.12 + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.2 + minimatch@9.0.5: dependencies: brace-expansion: 2.0.2 @@ -8513,6 +9033,8 @@ snapshots: mute-stream@2.0.0: {} + nano-spawn@2.0.0: {} + nanoid@3.3.11: {} nanostores@1.1.0: {} @@ -8565,6 +9087,8 @@ snapshots: node-releases@2.0.27: {} + normalize-path@3.0.0: {} + nwsapi@2.2.23: {} nypm@0.6.4: @@ -8599,6 +9123,10 @@ snapshots: dependencies: mimic-fn: 2.1.0 + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + open@10.2.0: dependencies: default-browser: 5.4.0 @@ -8723,10 +9251,14 @@ snapshots: picocolors@1.1.1: {} + picomatch@2.3.1: {} + picomatch@4.0.2: {} picomatch@4.0.3: {} + pidtree@0.6.0: {} + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -8808,6 +9340,10 @@ snapshots: transitivePeerDependencies: - magicast + process-nextick-args@2.0.1: {} + + process@0.11.10: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -8900,12 +9436,34 @@ snapshots: react@19.2.4: {} + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readable-stream@3.6.2: dependencies: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.6 + readdirp@4.1.2: {} readdirp@5.0.0: {} @@ -8925,6 +9483,8 @@ snapshots: regexp-to-ast@0.5.0: {} + regexp-tree@0.1.27: {} + require-from-string@2.0.2: {} resize-observer-polyfill@1.5.1: {} @@ -8938,6 +9498,13 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + rfdc@1.4.1: {} + robust-predicates@3.0.2: {} rollup@4.57.0: @@ -9004,8 +9571,14 @@ snapshots: dependencies: tslib: 2.8.1 + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} + safe-regex@2.1.1: + dependencies: + regexp-tree: 0.1.27 + safer-buffer@2.1.2: {} sanitize-html@2.17.0: @@ -9036,6 +9609,11 @@ snapshots: ajv-formats: 2.1.1(ajv@8.17.1) ajv-keywords: 5.1.0(ajv@8.17.1) + section-matter@1.0.0: + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + semver@6.3.1: {} semver@7.7.3: {} @@ -9155,6 +9733,11 @@ snapshots: sisteransi@1.0.5: {} + slice-ansi@7.1.2: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + slugify@1.6.6: {} socket.io-adapter@2.5.6: @@ -9211,6 +9794,8 @@ snapshots: split2@4.2.0: {} + sprintf-js@1.0.3: {} + stackback@0.0.2: {} standard-as-callback@2.1.0: {} @@ -9221,6 +9806,17 @@ snapshots: streamsearch@1.1.0: {} + streamx@2.23.0: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.3 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + string-argv@0.3.2: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -9233,6 +9829,21 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.2 + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.4.0 + strip-ansi: 7.1.2 + + string-width@8.1.1: + dependencies: + get-east-asian-width: 1.4.0 + strip-ansi: 7.1.2 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -9245,6 +9856,8 @@ snapshots: dependencies: ansi-regex: 6.2.2 + strip-bom-string@1.0.0: {} + strip-bom@3.0.0: {} strip-indent@3.0.0: @@ -9305,6 +9918,15 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + tar-stream@3.1.7: + dependencies: + b4a: 1.7.3 + fast-fifo: 1.3.2 + streamx: 2.23.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + terser-webpack-plugin@5.3.16(@swc/core@1.15.11)(webpack@5.104.1(@swc/core@1.15.11)): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -9329,6 +9951,12 @@ snapshots: glob: 10.5.0 minimatch: 9.0.5 + text-decoder@1.2.3: + dependencies: + b4a: 1.7.3 + transitivePeerDependencies: + - react-native-b4a + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -9354,6 +9982,10 @@ snapshots: dependencies: tldts-core: 6.1.86 + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + toidentifier@1.0.1: {} token-types@6.1.2: @@ -9511,13 +10143,13 @@ snapshots: vary@1.1.2: {} - vite-node@3.2.4(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0): + vite-node@3.2.4(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.1(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0) + vite: 7.3.1(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - jiti @@ -9532,7 +10164,7 @@ snapshots: - tsx - yaml - vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0): + vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) @@ -9546,12 +10178,13 @@ snapshots: jiti: 2.6.1 terser: 5.46.0 tsx: 4.21.0 + yaml: 2.8.2 - vitest@3.2.4(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0): + vitest@3.2.4(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)) + '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -9569,8 +10202,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.1(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0) - vite-node: 3.2.4(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0) + vite: 7.3.1(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite-node: 3.2.4(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.19.7 @@ -9589,10 +10222,10 @@ snapshots: - tsx - yaml - vitest@4.0.18(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0): + vitest@4.0.18(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)) + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -9609,7 +10242,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0) + vite: 7.3.1(@types/node@22.19.7)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.19.7 @@ -9739,6 +10372,12 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.2 + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.1.2 + wrappy@1.0.2: {} ws@8.18.3: {} @@ -9759,6 +10398,8 @@ snapshots: yallist@3.1.1: {} + yaml@2.8.2: {} + yargs-parser@21.1.1: {} yocto-queue@0.1.0: {} @@ -9771,6 +10412,12 @@ snapshots: yoctocolors@2.1.2: {} + zip-stream@6.0.1: + dependencies: + archiver-utils: 5.0.2 + compress-commons: 6.0.2 + readable-stream: 4.7.0 + zod@4.3.6: {} zustand@4.5.7(@types/react@19.2.10)(react@19.2.4): -- 2.49.1 From 02a69399ba3b9755286aea16c471ef7125e06f28 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Fri, 30 Jan 2026 13:19:02 -0600 Subject: [PATCH 2/6] feat: Enable strict lint enforcement on pre-commit Strict enforcement now active: - Format all changed files (auto-fix) - Lint entire packages that have changed files - Type-check affected packages - Block commit if ANY warnings or errors Impact: If you touch a file in a package with existing violations, you must clean up the entire package before committing. This forces incremental cleanup while preventing new violations. Co-Authored-By: Claude Sonnet 4.5 --- .lintstagedrc.mjs | 42 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/.lintstagedrc.mjs b/.lintstagedrc.mjs index 7d28dae..8b3a63e 100644 --- a/.lintstagedrc.mjs +++ b/.lintstagedrc.mjs @@ -1,12 +1,42 @@ // Monorepo-aware lint-staged configuration -// NOTE: Strict enforcement is disabled until existing violations are fixed. -// See docs/quality-rails-status.md for current status. +// STRICT ENFORCEMENT ENABLED: Blocks commits if affected packages have violations +// +// IMPORTANT: This lints ENTIRE packages, not just changed files. +// If you touch ANY file in a package with violations, you must fix the whole package. +// This forces incremental cleanup - work in a package = clean up that package. +// export default { - // TypeScript files - format only for now + // TypeScript files - lint and typecheck affected packages '**/*.{ts,tsx}': (filenames) => { - return [ - `prettier --write ${filenames.join(' ')}`, - ]; + const commands = []; + + // 1. Format first (auto-fixes what it can) + commands.push(`prettier --write ${filenames.join(' ')}`); + + // 2. Extract affected packages + const packages = [...new Set(filenames.map(f => { + const match = f.match(/^(apps|packages)\/([^/]+)\//); + if (!match) return null; + // Return package name format for turbo (e.g., "@mosaic/api") + return `@mosaic/${match[2]}`; + }))].filter(Boolean); + + if (packages.length === 0) { + return commands; + } + + // 3. Lint entire affected packages via turbo + // --max-warnings=0 means ANY warning blocks the commit + packages.forEach(pkg => { + commands.push(`pnpm turbo run lint --filter=${pkg} -- --max-warnings=0`); + }); + + // 4. Type-check affected packages + packages.forEach(pkg => { + commands.push(`pnpm turbo run typecheck --filter=${pkg}`); + }); + + return commands; }, // Format all other files -- 2.49.1 From 7443ff4839199f48b97e869eed39e54cd1751bea Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Fri, 30 Jan 2026 13:21:29 -0600 Subject: [PATCH 3/6] fix: Enable strict lint enforcement with correct path matching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: Strict lint enforcement is now ACTIVE Pre-commit hooks now block commits if: - Affected package has ANY lint errors or warnings - Affected package has ANY type errors Impact: If you touch a file in a package with existing violations, you MUST fix ALL violations in that package before committing. This forces incremental cleanup: - Work in @mosaic/shared → Fix all @mosaic/shared violations - Work in @mosaic/api → Fix all @mosaic/api violations - Work in clean packages → No extra work required Fixed regex to handle absolute paths from lint-staged. Co-Authored-By: Claude Sonnet 4.5 --- .lintstagedrc.mjs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.lintstagedrc.mjs b/.lintstagedrc.mjs index 8b3a63e..f05df33 100644 --- a/.lintstagedrc.mjs +++ b/.lintstagedrc.mjs @@ -13,9 +13,11 @@ export default { // 1. Format first (auto-fixes what it can) commands.push(`prettier --write ${filenames.join(' ')}`); - // 2. Extract affected packages + // 2. Extract affected packages from absolute paths + // lint-staged passes absolute paths, so we need to extract the relative part const packages = [...new Set(filenames.map(f => { - const match = f.match(/^(apps|packages)\/([^/]+)\//); + // Match either absolute or relative paths: .../packages/shared/... or packages/shared/... + const match = f.match(/(?:^|\/)(apps|packages)\/([^/]+)\//); if (!match) return null; // Return package name format for turbo (e.g., "@mosaic/api") return `@mosaic/${match[2]}`; @@ -26,7 +28,7 @@ export default { } // 3. Lint entire affected packages via turbo - // --max-warnings=0 means ANY warning blocks the commit + // --max-warnings=0 means ANY warning/error blocks the commit packages.forEach(pkg => { commands.push(`pnpm turbo run lint --filter=${pkg} -- --max-warnings=0`); }); -- 2.49.1 From 0dd8d5f91e8c3661258125f6ec777a2c4f2e01e6 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Fri, 30 Jan 2026 13:22:24 -0600 Subject: [PATCH 4/6] docs: Update Quality Rails status to reflect active enforcement Strict enforcement is now ACTIVE and blocking commits. Updated documentation to reflect: - Pre-commit hooks are actively blocking violations - Package-level enforcement strategy - How developers should handle blocked commits - Next steps for incremental cleanup Co-Authored-By: Claude Sonnet 4.5 --- CLAUDE.md | 25 +++++++++++++++++++------ docs/quality-rails-status.md | 11 ++++++----- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 751872a..25346ca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -341,12 +341,12 @@ TDD Anti-Patterns to Avoid Quality Rails - Mechanical Code Quality Enforcement -**Status:** Installed (2026-01-30) - Currently in formatting-only mode +**Status:** ACTIVE (2026-01-30) - Strict enforcement enabled ✅ Quality Rails provides mechanical enforcement of code quality standards through pre-commit hooks and CI/CD pipelines. See `docs/quality-rails-status.md` for full details. -What's Enforced (Once enabled): +What's Enforced (NOW ACTIVE): - ✅ **Type Safety** - Blocks explicit `any` types (@typescript-eslint/no-explicit-any: error) - ✅ **Return Types** - Requires explicit return types on exported functions @@ -358,14 +358,27 @@ What's Enforced (Once enabled): Current Status: -- 🟡 **Pre-commit hooks**: Enabled (formatting only until violations fixed) -- 🔴 **Strict enforcement**: Disabled (1,226 existing violations to fix first) +- ✅ **Pre-commit hooks**: ACTIVE - Blocks commits with violations +- ✅ **Strict enforcement**: ENABLED - Package-level enforcement - 🟡 **CI/CD pipeline**: Ready (.woodpecker.yml created, not yet configured) +How It Works: + +**Package-Level Enforcement** - If you touch ANY file in a package with violations, +you must fix ALL violations in that package before committing. This forces incremental +cleanup while preventing new violations. + +Example: + +- Edit `apps/api/src/tasks/tasks.service.ts` +- Pre-commit hook runs lint on ENTIRE `@mosaic/api` package +- If `@mosaic/api` has violations → Commit BLOCKED +- Fix all violations in `@mosaic/api` → Commit allowed + Next Steps: -1. Fix existing 1,226 lint violations (priority: explicit `any` types) -2. Enable strict pre-commit enforcement in `.lintstagedrc.mjs` +1. Fix violations package-by-package as you work in them +2. Priority: Fix explicit `any` types and type safety issues first 3. Configure Woodpecker CI to run quality gates on all PRs Why This Matters: diff --git a/docs/quality-rails-status.md b/docs/quality-rails-status.md index eeee862..d4b9537 100644 --- a/docs/quality-rails-status.md +++ b/docs/quality-rails-status.md @@ -4,17 +4,18 @@ 2026-01-30 -## Current Status: **INSTALLED - PARTIAL ENFORCEMENT** +## Current Status: **STRICT ENFORCEMENT ACTIVE** ✅ -Quality Rails has been successfully installed but is currently in **formatting-only mode** due to existing codebase violations. +Quality Rails is now **FULLY ENFORCING** code quality on all commits. Any commit that touches a package with violations will be blocked until that package is cleaned up. ## What's Installed -### ✅ Pre-Commit Hooks (.husky/) +### ✅ Pre-Commit Hooks (.husky/) - ACTIVE - Runs lint-staged on every commit -- Currently only enforces Prettier formatting -- Ready to enable full enforcement once violations are fixed +- **BLOCKS commits** with lint errors or warnings in affected packages +- **BLOCKS commits** with type errors in affected packages +- Auto-formats code with Prettier before linting ### ✅ Enhanced ESLint Rules -- 2.49.1 From 22cd68811d2db1044c4e96e2bf701b20e850d24a Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Fri, 30 Jan 2026 14:28:23 -0600 Subject: [PATCH 5/6] fix: Update pre-commit hook for husky v10 compatibility Remove deprecated shebang that will fail in husky v10. Before (deprecated): #!/bin/sh After (v10-compatible): Direct commands without shebang Ref: https://github.com/typicode/husky/issues/1476 Co-Authored-By: Claude Sonnet 4.5 --- .husky/pre-commit | 1 - 1 file changed, 1 deletion(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index 0f707d9..01e587e 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,3 +1,2 @@ -#!/bin/sh npx lint-staged npx git-secrets --scan || echo "Warning: git-secrets not installed" -- 2.49.1 From 955bed91edd9bda3bace5ea9c6b104867183a0f4 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Fri, 30 Jan 2026 00:25:05 -0600 Subject: [PATCH 6/6] docs: add knowledge module documentation (closes #80) - Created KNOWLEDGE_USER_GUIDE.md with comprehensive user documentation - Getting started, creating entries, wiki-links - Tags and organization, search capabilities - Import/export, version history, graph visualization - Tips, best practices, and permissions - Created KNOWLEDGE_API.md with complete REST API reference - All endpoints with request/response formats - Authentication and permissions - Detailed examples with curl and JavaScript - Error responses and validation - Created KNOWLEDGE_DEV.md with developer documentation - Architecture overview and module structure - Database schema with all models - Service layer implementation details - Caching strategy and performance - Wiki-link parsing and resolution system - Testing guide and contribution guidelines - Updated README.md with Knowledge Module section - Feature overview and quick examples - Links to detailed documentation - Performance metrics - Added knowledge management to overview All documentation includes: - Real examples from codebase - Code snippets and API calls - Best practices and workflows - Cross-references between docs --- KNOWLEDGE_API.md | 1559 +++++++++++++++++++++++++++++++++++++++ KNOWLEDGE_DEV.md | 1240 +++++++++++++++++++++++++++++++ KNOWLEDGE_USER_GUIDE.md | 628 ++++++++++++++++ README.md | 106 +++ 4 files changed, 3533 insertions(+) create mode 100644 KNOWLEDGE_API.md create mode 100644 KNOWLEDGE_DEV.md create mode 100644 KNOWLEDGE_USER_GUIDE.md diff --git a/KNOWLEDGE_API.md b/KNOWLEDGE_API.md new file mode 100644 index 0000000..1ec7159 --- /dev/null +++ b/KNOWLEDGE_API.md @@ -0,0 +1,1559 @@ +# Knowledge Module - API Documentation + +Complete REST API reference for the Knowledge Module endpoints. + +## Table of Contents + +1. [Authentication](#authentication) +2. [Entry Endpoints](#entry-endpoints) +3. [Search Endpoints](#search-endpoints) +4. [Tag Endpoints](#tag-endpoints) +5. [Import/Export Endpoints](#importexport-endpoints) +6. [Stats Endpoints](#stats-endpoints) +7. [Cache Endpoints](#cache-endpoints) +8. [Error Responses](#error-responses) + +--- + +## Authentication + +All Knowledge Module endpoints require authentication via Bearer token and workspace context. + +### Headers + +```http +Authorization: Bearer {session_token} +x-workspace-id: {workspace_uuid} +``` + +### Permission Levels + +- **WORKSPACE_ANY**: Any workspace member (including GUEST) +- **WORKSPACE_MEMBER**: MEMBER role or higher +- **WORKSPACE_ADMIN**: ADMIN or OWNER role + +--- + +## Entry Endpoints + +### List Entries + +Get a paginated list of knowledge entries. + +```http +GET /api/knowledge/entries +``` + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `page` | integer | 1 | Page number | +| `limit` | integer | 20 | Results per page (max: 100) | +| `status` | string | - | Filter by status (DRAFT, PUBLISHED, ARCHIVED) | + +**Permissions:** WORKSPACE_ANY + +**Example Request:** + +```bash +curl -X GET 'http://localhost:3001/api/knowledge/entries?page=1&limit=20&status=PUBLISHED' \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-workspace-id: WORKSPACE_ID" +``` + +**Response:** + +```json +{ + "data": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "slug": "react-hooks-guide", + "title": "React Hooks Guide", + "content": "# React Hooks Guide\n\nComprehensive guide...", + "contentHtml": "

React Hooks Guide

Comprehensive guide...

", + "summary": "Learn about React Hooks", + "status": "PUBLISHED", + "visibility": "WORKSPACE", + "createdAt": "2024-01-29T10:00:00Z", + "updatedAt": "2024-01-30T15:30:00Z", + "createdBy": "user-uuid", + "updatedBy": "user-uuid", + "tags": [ + { + "id": "tag-uuid", + "name": "React", + "slug": "react", + "color": "#61dafb" + } + ] + } + ], + "pagination": { + "page": 1, + "limit": 20, + "total": 45, + "totalPages": 3 + } +} +``` + +--- + +### Get Entry + +Retrieve a single knowledge entry by slug. + +```http +GET /api/knowledge/entries/:slug +``` + +**Path Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `slug` | string | Entry slug (e.g., "react-hooks-guide") | + +**Permissions:** WORKSPACE_ANY + +**Example Request:** + +```bash +curl -X GET 'http://localhost:3001/api/knowledge/entries/react-hooks-guide' \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-workspace-id: WORKSPACE_ID" +``` + +**Response:** + +```json +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "slug": "react-hooks-guide", + "title": "React Hooks Guide", + "content": "# React Hooks Guide\n\nUse [[useState]] and [[useEffect]]...", + "contentHtml": "

React Hooks Guide

Use useState...

", + "summary": "Learn about React Hooks", + "status": "PUBLISHED", + "visibility": "WORKSPACE", + "createdAt": "2024-01-29T10:00:00Z", + "updatedAt": "2024-01-30T15:30:00Z", + "createdBy": "user-uuid", + "updatedBy": "user-uuid", + "tags": [ + { + "id": "tag-uuid", + "name": "React", + "slug": "react", + "color": "#61dafb" + } + ] +} +``` + +--- + +### Create Entry + +Create a new knowledge entry. + +```http +POST /api/knowledge/entries +``` + +**Permissions:** WORKSPACE_MEMBER + +**Request Body:** + +```json +{ + "title": "React Hooks Guide", + "content": "# React Hooks Guide\n\nContent here...", + "summary": "Learn about React Hooks", + "status": "DRAFT", + "visibility": "WORKSPACE", + "tags": ["react", "frontend"], + "changeNote": "Initial draft" +} +``` + +**Body Schema:** + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| `title` | string | Yes | 1-500 characters | +| `content` | string | Yes | Min 1 character | +| `summary` | string | No | Max 1000 characters | +| `status` | enum | No | DRAFT, PUBLISHED, ARCHIVED (default: DRAFT) | +| `visibility` | enum | No | PRIVATE, WORKSPACE, PUBLIC (default: PRIVATE) | +| `tags` | string[] | No | Array of tag slugs | +| `changeNote` | string | No | Max 500 characters | + +**Example Request:** + +```bash +curl -X POST 'http://localhost:3001/api/knowledge/entries' \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-workspace-id: WORKSPACE_ID" \ + -H "Content-Type: application/json" \ + -d '{ + "title": "React Hooks Guide", + "content": "# React Hooks Guide\n\nUse [[useState]] for state...", + "summary": "Learn about React Hooks", + "status": "DRAFT", + "tags": ["react", "frontend"], + "changeNote": "Initial draft" + }' +``` + +**Response:** + +```json +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "slug": "react-hooks-guide", + "title": "React Hooks Guide", + "content": "# React Hooks Guide\n\nUse [[useState]] for state...", + "contentHtml": "

React Hooks Guide

Use useState...

", + "summary": "Learn about React Hooks", + "status": "DRAFT", + "visibility": "WORKSPACE", + "createdAt": "2024-01-30T10:00:00Z", + "updatedAt": "2024-01-30T10:00:00Z", + "createdBy": "user-uuid", + "updatedBy": "user-uuid", + "tags": [ + { + "id": "tag-uuid", + "name": "React", + "slug": "react", + "color": "#61dafb" + } + ] +} +``` + +**Notes:** +- Slug is auto-generated from title +- If tags don't exist, they are created automatically +- Wiki-links in content are automatically parsed and stored +- First version (version 1) is created automatically + +--- + +### Update Entry + +Update an existing knowledge entry. + +```http +PUT /api/knowledge/entries/:slug +``` + +**Path Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `slug` | string | Entry slug | + +**Permissions:** WORKSPACE_MEMBER + +**Request Body:** + +```json +{ + "title": "React Hooks Guide (Updated)", + "content": "# React Hooks Guide\n\nUpdated content...", + "summary": "Updated summary", + "status": "PUBLISHED", + "visibility": "WORKSPACE", + "tags": ["react", "frontend", "tutorial"], + "changeNote": "Added examples and updated title" +} +``` + +All fields are optional. Only provided fields are updated. + +**Example Request:** + +```bash +curl -X PUT 'http://localhost:3001/api/knowledge/entries/react-hooks-guide' \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-workspace-id: WORKSPACE_ID" \ + -H "Content-Type: application/json" \ + -d '{ + "status": "PUBLISHED", + "changeNote": "Ready for publication" + }' +``` + +**Response:** + +```json +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "slug": "react-hooks-guide", + "title": "React Hooks Guide (Updated)", + "content": "# React Hooks Guide\n\nUpdated content...", + "contentHtml": "

React Hooks Guide

Updated...

", + "summary": "Updated summary", + "status": "PUBLISHED", + "visibility": "WORKSPACE", + "createdAt": "2024-01-30T10:00:00Z", + "updatedAt": "2024-01-30T12:00:00Z", + "createdBy": "user-uuid", + "updatedBy": "user-uuid", + "tags": [...] +} +``` + +**Notes:** +- A new version is created on every update +- Wiki-links are re-parsed and synchronized +- Cache is invalidated automatically + +--- + +### Delete Entry + +Soft-delete (archive) a knowledge entry. + +```http +DELETE /api/knowledge/entries/:slug +``` + +**Path Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `slug` | string | Entry slug | + +**Permissions:** WORKSPACE_ADMIN + +**Example Request:** + +```bash +curl -X DELETE 'http://localhost:3001/api/knowledge/entries/react-hooks-guide' \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-workspace-id: WORKSPACE_ID" +``` + +**Response:** + +```json +{ + "message": "Entry archived successfully" +} +``` + +**Notes:** +- This is a **soft delete** (sets status to ARCHIVED) +- Entry remains in database with all versions +- Links to this entry become unresolved +- Can be restored by updating status back to DRAFT or PUBLISHED + +--- + +### Get Backlinks + +Get all entries that link to this entry. + +```http +GET /api/knowledge/entries/:slug/backlinks +``` + +**Path Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `slug` | string | Entry slug | + +**Permissions:** WORKSPACE_ANY + +**Example Request:** + +```bash +curl -X GET 'http://localhost:3001/api/knowledge/entries/react-hooks/backlinks' \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-workspace-id: WORKSPACE_ID" +``` + +**Response:** + +```json +{ + "entry": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "slug": "react-hooks", + "title": "React Hooks" + }, + "backlinks": [ + { + "id": "link-uuid-1", + "sourceId": "entry-uuid-1", + "source": { + "id": "entry-uuid-1", + "slug": "frontend-guide", + "title": "Frontend Development Guide", + "summary": "Complete frontend guide" + }, + "linkText": "React Hooks", + "context": "Learn about [[React Hooks]] for state management.", + "createdAt": "2024-01-29T10:00:00Z" + }, + { + "id": "link-uuid-2", + "sourceId": "entry-uuid-2", + "source": { + "id": "entry-uuid-2", + "slug": "component-patterns", + "title": "React Component Patterns", + "summary": null + }, + "linkText": "hooks", + "context": "Modern [[hooks|React hooks]] make state simple.", + "createdAt": "2024-01-30T08:00:00Z" + } + ], + "count": 2 +} +``` + +**Notes:** +- Only resolved links are included +- Context shows surrounding text (future feature) +- Sorted by creation date (newest first) + +--- + +### List Versions + +Get version history for an entry. + +```http +GET /api/knowledge/entries/:slug/versions +``` + +**Path Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `slug` | string | Entry slug | + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `page` | integer | 1 | Page number | +| `limit` | integer | 20 | Results per page (max: 100) | + +**Permissions:** WORKSPACE_ANY + +**Example Request:** + +```bash +curl -X GET 'http://localhost:3001/api/knowledge/entries/react-hooks-guide/versions?page=1&limit=10' \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-workspace-id: WORKSPACE_ID" +``` + +**Response:** + +```json +{ + "data": [ + { + "id": "version-uuid-3", + "version": 3, + "title": "React Hooks Guide", + "summary": "Learn about React Hooks", + "changeNote": "Added examples", + "createdAt": "2024-01-30T15:00:00Z", + "createdBy": "user-uuid", + "author": { + "id": "user-uuid", + "name": "John Doe", + "email": "john@example.com" + } + }, + { + "id": "version-uuid-2", + "version": 2, + "title": "React Hooks Guide", + "summary": "Learn about React Hooks", + "changeNote": "Fixed typos", + "createdAt": "2024-01-30T12:00:00Z", + "createdBy": "user-uuid", + "author": { ... } + }, + { + "id": "version-uuid-1", + "version": 1, + "title": "React Hooks Guide", + "summary": null, + "changeNote": "Initial draft", + "createdAt": "2024-01-30T10:00:00Z", + "createdBy": "user-uuid", + "author": { ... } + } + ], + "pagination": { + "page": 1, + "limit": 10, + "total": 3, + "totalPages": 1 + } +} +``` + +**Notes:** +- Versions sorted newest first +- Content is NOT included (use Get Version endpoint for full content) +- First version has `changeNote` from creation + +--- + +### Get Version + +Get complete content for a specific version. + +```http +GET /api/knowledge/entries/:slug/versions/:version +``` + +**Path Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `slug` | string | Entry slug | +| `version` | integer | Version number | + +**Permissions:** WORKSPACE_ANY + +**Example Request:** + +```bash +curl -X GET 'http://localhost:3001/api/knowledge/entries/react-hooks-guide/versions/2' \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-workspace-id: WORKSPACE_ID" +``` + +**Response:** + +```json +{ + "id": "version-uuid-2", + "entryId": "entry-uuid", + "version": 2, + "title": "React Hooks Guide", + "content": "# React Hooks Guide\n\nOld content...", + "summary": "Learn about React Hooks", + "changeNote": "Fixed typos", + "createdAt": "2024-01-30T12:00:00Z", + "createdBy": "user-uuid", + "author": { + "id": "user-uuid", + "name": "John Doe", + "email": "john@example.com" + } +} +``` + +**Notes:** +- Includes full content as it existed in that version +- Use this to preview before restoring + +--- + +### Restore Version + +Restore an entry to a previous version. + +```http +POST /api/knowledge/entries/:slug/restore/:version +``` + +**Path Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `slug` | string | Entry slug | +| `version` | integer | Version number to restore | + +**Permissions:** WORKSPACE_MEMBER + +**Request Body:** + +```json +{ + "changeNote": "Restored version 2 - reverted bad changes" +} +``` + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| `changeNote` | string | Yes | Max 500 characters | + +**Example Request:** + +```bash +curl -X POST 'http://localhost:3001/api/knowledge/entries/react-hooks-guide/restore/2' \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-workspace-id: WORKSPACE_ID" \ + -H "Content-Type: application/json" \ + -d '{ + "changeNote": "Restored version 2 - reverted bad changes" + }' +``` + +**Response:** + +```json +{ + "id": "entry-uuid", + "slug": "react-hooks-guide", + "title": "React Hooks Guide", + "content": "# React Hooks Guide\n\nRestored content...", + "summary": "Learn about React Hooks", + "status": "PUBLISHED", + "visibility": "WORKSPACE", + "createdAt": "2024-01-30T10:00:00Z", + "updatedAt": "2024-01-30T16:00:00Z", + "createdBy": "user-uuid", + "updatedBy": "current-user-uuid", + "tags": [...] +} +``` + +**Notes:** +- Creates a **new version** (e.g., version 4) with content from the specified version +- Original versions remain untouched (no history rewriting) +- Change note is required to document why you restored +- Wiki-links are re-parsed from the restored content + +--- + +## Search Endpoints + +### Full-Text Search + +Search across entry titles and content. + +```http +GET /api/knowledge/search +``` + +**Query Parameters:** + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `q` | string | Yes | - | Search query | +| `status` | enum | No | - | DRAFT, PUBLISHED, ARCHIVED | +| `page` | integer | No | 1 | Page number | +| `limit` | integer | No | 20 | Results per page (max: 100) | + +**Permissions:** WORKSPACE_ANY + +**Example Request:** + +```bash +curl -X GET 'http://localhost:3001/api/knowledge/search?q=react+hooks&page=1&limit=10' \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-workspace-id: WORKSPACE_ID" +``` + +**Response:** + +```json +{ + "data": [ + { + "id": "entry-uuid", + "slug": "react-hooks-guide", + "title": "React Hooks Guide", + "content": "# React Hooks Guide\n\nContent...", + "summary": "Learn about React Hooks", + "status": "PUBLISHED", + "visibility": "WORKSPACE", + "createdAt": "2024-01-30T10:00:00Z", + "updatedAt": "2024-01-30T12:00:00Z", + "createdBy": "user-uuid", + "updatedBy": "user-uuid", + "tags": [...] + } + ], + "pagination": { + "page": 1, + "limit": 10, + "total": 5, + "totalPages": 1 + } +} +``` + +**Search behavior:** +- Searches both `title` and `content` fields +- Case-insensitive +- Partial word matching +- Relevance-ranked results + +--- + +### Search by Tags + +Find entries with specific tags. + +```http +GET /api/knowledge/search/by-tags +``` + +**Query Parameters:** + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `tags` | string | Yes | - | Comma-separated tag slugs | +| `status` | enum | No | - | DRAFT, PUBLISHED, ARCHIVED | +| `page` | integer | No | 1 | Page number | +| `limit` | integer | No | 20 | Results per page (max: 100) | + +**Permissions:** WORKSPACE_ANY + +**Example Request:** + +```bash +curl -X GET 'http://localhost:3001/api/knowledge/search/by-tags?tags=react,frontend&status=PUBLISHED' \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-workspace-id: WORKSPACE_ID" +``` + +**Response:** + +Same format as Full-Text Search response. + +**Notes:** +- Finds entries with **ALL** specified tags (AND logic) +- For OR logic, make separate requests + +--- + +### Recent Entries + +Get recently modified entries. + +```http +GET /api/knowledge/search/recent +``` + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `limit` | integer | 10 | Number of entries (max: 50) | +| `status` | enum | - | DRAFT, PUBLISHED, ARCHIVED | + +**Permissions:** WORKSPACE_ANY + +**Example Request:** + +```bash +curl -X GET 'http://localhost:3001/api/knowledge/search/recent?limit=5&status=PUBLISHED' \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-workspace-id: WORKSPACE_ID" +``` + +**Response:** + +```json +{ + "data": [ + { + "id": "entry-uuid", + "slug": "react-hooks-guide", + "title": "React Hooks Guide", + "summary": "Learn about React Hooks", + "status": "PUBLISHED", + "updatedAt": "2024-01-30T16:00:00Z", + "tags": [...] + } + ], + "count": 5 +} +``` + +**Notes:** +- Sorted by `updatedAt` descending (newest first) +- Does not include full content (use Get Entry for details) + +--- + +## Tag Endpoints + +### List Tags + +Get all tags in the workspace. + +```http +GET /api/knowledge/tags +``` + +**Permissions:** WORKSPACE_ANY + +**Example Request:** + +```bash +curl -X GET 'http://localhost:3001/api/knowledge/tags' \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-workspace-id: WORKSPACE_ID" +``` + +**Response:** + +```json +[ + { + "id": "tag-uuid-1", + "name": "React", + "slug": "react", + "color": "#61dafb", + "description": "React library and ecosystem" + }, + { + "id": "tag-uuid-2", + "name": "Frontend", + "slug": "frontend", + "color": "#3b82f6", + "description": null + } +] +``` + +--- + +### Get Tag + +Get a single tag by slug. + +```http +GET /api/knowledge/tags/:slug +``` + +**Path Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `slug` | string | Tag slug | + +**Permissions:** WORKSPACE_ANY + +**Example Request:** + +```bash +curl -X GET 'http://localhost:3001/api/knowledge/tags/react' \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-workspace-id: WORKSPACE_ID" +``` + +**Response:** + +```json +{ + "id": "tag-uuid", + "name": "React", + "slug": "react", + "color": "#61dafb", + "description": "React library and ecosystem" +} +``` + +--- + +### Create Tag + +Create a new tag. + +```http +POST /api/knowledge/tags +``` + +**Permissions:** WORKSPACE_MEMBER + +**Request Body:** + +```json +{ + "name": "TypeScript", + "color": "#3178c6", + "description": "TypeScript language and tooling" +} +``` + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| `name` | string | Yes | Unique per workspace | +| `color` | string | No | Hex color (e.g., "#3178c6") | +| `description` | string | No | - | + +**Example Request:** + +```bash +curl -X POST 'http://localhost:3001/api/knowledge/tags' \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-workspace-id: WORKSPACE_ID" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "TypeScript", + "color": "#3178c6" + }' +``` + +**Response:** + +```json +{ + "id": "new-tag-uuid", + "name": "TypeScript", + "slug": "typescript", + "color": "#3178c6", + "description": null +} +``` + +**Notes:** +- Slug is auto-generated from name +- Tags are workspace-scoped (same slug can exist in different workspaces) + +--- + +### Update Tag + +Update an existing tag. + +```http +PUT /api/knowledge/tags/:slug +``` + +**Path Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `slug` | string | Tag slug | + +**Permissions:** WORKSPACE_MEMBER + +**Request Body:** + +All fields are optional. Only provided fields are updated. + +```json +{ + "name": "TypeScript (Updated)", + "color": "#2f74c0", + "description": "TypeScript programming language" +} +``` + +**Example Request:** + +```bash +curl -X PUT 'http://localhost:3001/api/knowledge/tags/typescript' \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-workspace-id: WORKSPACE_ID" \ + -H "Content-Type: application/json" \ + -d '{ + "color": "#2f74c0" + }' +``` + +**Response:** + +```json +{ + "id": "tag-uuid", + "name": "TypeScript (Updated)", + "slug": "typescript", + "color": "#2f74c0", + "description": "TypeScript programming language" +} +``` + +--- + +### Delete Tag + +Delete a tag (removes from all entries). + +```http +DELETE /api/knowledge/tags/:slug +``` + +**Path Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `slug` | string | Tag slug | + +**Permissions:** WORKSPACE_ADMIN + +**Example Request:** + +```bash +curl -X DELETE 'http://localhost:3001/api/knowledge/tags/typescript' \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-workspace-id: WORKSPACE_ID" +``` + +**Response:** + +``` +204 No Content +``` + +**Notes:** +- Tag is removed from all entries that used it +- Entries themselves are NOT deleted + +--- + +### Get Tag Entries + +Get all entries with a specific tag. + +```http +GET /api/knowledge/tags/:slug/entries +``` + +**Path Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `slug` | string | Tag slug | + +**Permissions:** WORKSPACE_ANY + +**Example Request:** + +```bash +curl -X GET 'http://localhost:3001/api/knowledge/tags/react/entries' \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-workspace-id: WORKSPACE_ID" +``` + +**Response:** + +```json +[ + { + "id": "entry-uuid-1", + "slug": "react-hooks-guide", + "title": "React Hooks Guide", + "summary": "Learn about React Hooks", + "status": "PUBLISHED", + "createdAt": "2024-01-30T10:00:00Z", + "updatedAt": "2024-01-30T12:00:00Z" + }, + { + "id": "entry-uuid-2", + "slug": "component-patterns", + "title": "React Component Patterns", + "summary": null, + "status": "PUBLISHED", + "createdAt": "2024-01-29T08:00:00Z", + "updatedAt": "2024-01-29T09:00:00Z" + } +] +``` + +--- + +## Import/Export Endpoints + +### Export Entries + +Export knowledge entries as a downloadable archive. + +```http +GET /api/knowledge/export +``` + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `format` | enum | markdown | Export format: `markdown` or `json` | +| `entryIds` | string[] | - | Optional array of entry IDs to export | + +**Permissions:** WORKSPACE_ANY + +**Example Requests:** + +Export all entries as Markdown: +```bash +curl -X GET 'http://localhost:3001/api/knowledge/export?format=markdown' \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-workspace-id: WORKSPACE_ID" \ + -o knowledge-export.zip +``` + +Export specific entries as JSON: +```bash +curl -X GET 'http://localhost:3001/api/knowledge/export?format=json&entryIds[]=uuid1&entryIds[]=uuid2' \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-workspace-id: WORKSPACE_ID" \ + -o knowledge-export.zip +``` + +**Response:** + +Binary `.zip` file with headers: +``` +Content-Type: application/zip +Content-Disposition: attachment; filename="knowledge-export-YYYYMMDD-HHMMSS.zip" +``` + +**Markdown format structure:** + +``` +knowledge-export.zip +├── react-hooks-guide.md +├── component-patterns.md +└── state-management.md +``` + +Each `.md` file: +```markdown +--- +slug: react-hooks-guide +title: React Hooks Guide +status: PUBLISHED +visibility: WORKSPACE +tags: react, frontend +createdAt: 2024-01-30T10:00:00Z +updatedAt: 2024-01-30T12:00:00Z +--- + +# React Hooks Guide + +Content with [[wiki-links]]... +``` + +**JSON format structure:** + +``` +knowledge-export.zip +└── entries.json +``` + +`entries.json`: +```json +[ + { + "slug": "react-hooks-guide", + "title": "React Hooks Guide", + "content": "# React Hooks Guide\n\nContent...", + "summary": "Learn about React Hooks", + "status": "PUBLISHED", + "visibility": "WORKSPACE", + "tags": ["react", "frontend"], + "createdAt": "2024-01-30T10:00:00Z", + "updatedAt": "2024-01-30T12:00:00Z" + } +] +``` + +--- + +### Import Entries + +Import knowledge entries from uploaded file. + +```http +POST /api/knowledge/import +``` + +**Permissions:** WORKSPACE_MEMBER + +**Request:** + +Multipart form data with file upload. + +```bash +curl -X POST 'http://localhost:3001/api/knowledge/import' \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-workspace-id: WORKSPACE_ID" \ + -F "file=@knowledge-export.zip" +``` + +**Supported file types:** +- `.md` (single Markdown file) +- `.zip` (archive of Markdown files) + +**File size limit:** 50MB + +**Response:** + +```json +{ + "success": true, + "totalFiles": 10, + "imported": 8, + "failed": 2, + "results": [ + { + "filename": "react-hooks.md", + "success": true, + "entryId": "new-entry-uuid", + "slug": "react-hooks" + }, + { + "filename": "invalid-entry.md", + "success": false, + "error": "Title is required" + } + ] +} +``` + +**Import behavior:** +- New entries created with status DRAFT +- Existing entries (matching slug) are skipped +- Tags created automatically if they don't exist +- Wiki-links preserved (will resolve if targets exist) + +**Front matter parsing:** + +The importer reads YAML front matter: +```markdown +--- +slug: custom-slug +title: Entry Title +summary: Optional summary +status: PUBLISHED +visibility: WORKSPACE +tags: tag1, tag2 +--- + +Content starts here... +``` + +If no front matter, slug is generated from filename. + +--- + +## Stats Endpoints + +### Get Statistics + +Get knowledge base statistics for the workspace. + +```http +GET /api/knowledge/stats +``` + +**Permissions:** WORKSPACE_ANY + +**Example Request:** + +```bash +curl -X GET 'http://localhost:3001/api/knowledge/stats' \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-workspace-id: WORKSPACE_ID" +``` + +**Response:** + +```json +{ + "totalEntries": 42, + "entriesByStatus": { + "DRAFT": 5, + "PUBLISHED": 35, + "ARCHIVED": 2 + }, + "totalTags": 12, + "totalLinks": 87, + "totalVersions": 215, + "averageVersionsPerEntry": 5.1, + "topTags": [ + { + "id": "tag-uuid", + "name": "React", + "slug": "react", + "count": 15 + }, + { + "id": "tag-uuid", + "name": "Frontend", + "slug": "frontend", + "count": 12 + } + ], + "mostLinkedEntries": [ + { + "id": "entry-uuid", + "slug": "react-hooks", + "title": "React Hooks", + "incomingLinkCount": 8 + } + ], + "recentActivity": { + "last24h": 5, + "last7days": 18, + "last30days": 42 + } +} +``` + +--- + +## Cache Endpoints + +### Get Cache Stats + +Get cache performance statistics. + +```http +GET /api/knowledge/cache/stats +``` + +**Permissions:** WORKSPACE_ANY + +**Example Request:** + +```bash +curl -X GET 'http://localhost:3001/api/knowledge/cache/stats' \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-workspace-id: WORKSPACE_ID" +``` + +**Response:** + +```json +{ + "enabled": true, + "stats": { + "hits": 1250, + "misses": 180, + "sets": 195, + "deletes": 15, + "hitRate": 0.874 + } +} +``` + +**Stats explanation:** +- `hits`: Cache hits (data found in cache) +- `misses`: Cache misses (data not in cache, fetched from DB) +- `sets`: Cache writes +- `deletes`: Cache invalidations +- `hitRate`: Percentage of requests served from cache (hits / (hits + misses)) + +--- + +### Clear Cache + +Clear all cached data for the workspace. + +```http +POST /api/knowledge/cache/clear +``` + +**Permissions:** WORKSPACE_ADMIN + +**Example Request:** + +```bash +curl -X POST 'http://localhost:3001/api/knowledge/cache/clear' \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-workspace-id: WORKSPACE_ID" +``` + +**Response:** + +```json +{ + "message": "Cache cleared successfully" +} +``` + +**Notes:** +- Clears ALL knowledge caches for the workspace +- Includes entry caches, search caches, and graph caches +- Does not affect other workspaces + +--- + +### Reset Cache Stats + +Reset cache statistics counters to zero. + +```http +POST /api/knowledge/cache/stats/reset +``` + +**Permissions:** WORKSPACE_ADMIN + +**Example Request:** + +```bash +curl -X POST 'http://localhost:3001/api/knowledge/cache/stats/reset' \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-workspace-id: WORKSPACE_ID" +``` + +**Response:** + +```json +{ + "message": "Cache statistics reset successfully" +} +``` + +--- + +## Error Responses + +All endpoints follow standard HTTP error codes and return JSON error objects. + +### Error Format + +```json +{ + "statusCode": 404, + "message": "Entry not found", + "error": "Not Found" +} +``` + +### Common Status Codes + +| Code | Meaning | Example | +|------|---------|---------| +| `400` | Bad Request | Invalid request body, missing required field | +| `401` | Unauthorized | Missing or invalid auth token | +| `403` | Forbidden | Insufficient permissions for action | +| `404` | Not Found | Entry, tag, or version doesn't exist | +| `409` | Conflict | Slug already exists, duplicate entry | +| `413` | Payload Too Large | Import file exceeds 50MB limit | +| `422` | Unprocessable Entity | Validation failed (e.g., invalid enum value) | +| `500` | Internal Server Error | Unexpected server error | + +### Validation Errors + +When validation fails, you get detailed error information: + +```json +{ + "statusCode": 422, + "message": [ + "title must not be empty", + "title must not exceed 500 characters", + "status must be a valid EntryStatus" + ], + "error": "Unprocessable Entity" +} +``` + +--- + +## JavaScript Examples + +### Using Fetch API + +```javascript +const API_URL = 'http://localhost:3001/api'; +const token = 'YOUR_SESSION_TOKEN'; +const workspaceId = 'YOUR_WORKSPACE_ID'; + +const headers = { + 'Authorization': `Bearer ${token}`, + 'x-workspace-id': workspaceId, + 'Content-Type': 'application/json' +}; + +// Create entry +async function createEntry() { + const response = await fetch(`${API_URL}/knowledge/entries`, { + method: 'POST', + headers, + body: JSON.stringify({ + title: 'My New Entry', + content: '# Hello World\n\nThis is my first [[wiki-link]]!', + tags: ['tutorial'], + status: 'DRAFT' + }) + }); + + if (!response.ok) { + throw new Error(`Failed: ${response.status}`); + } + + return await response.json(); +} + +// Search entries +async function searchEntries(query) { + const params = new URLSearchParams({ q: query, limit: 10 }); + const response = await fetch(`${API_URL}/knowledge/search?${params}`, { + headers + }); + + return await response.json(); +} + +// Export entries +async function exportEntries() { + const response = await fetch(`${API_URL}/knowledge/export?format=markdown`, { + headers + }); + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'knowledge-export.zip'; + a.click(); +} +``` + +### Using Axios + +```javascript +import axios from 'axios'; + +const api = axios.create({ + baseURL: 'http://localhost:3001/api', + headers: { + 'Authorization': `Bearer ${token}`, + 'x-workspace-id': workspaceId + } +}); + +// Get entry +const entry = await api.get('/knowledge/entries/react-hooks'); +console.log(entry.data); + +// Update entry +const updated = await api.put('/knowledge/entries/react-hooks', { + status: 'PUBLISHED', + changeNote: 'Ready for publication' +}); + +// Import file +const formData = new FormData(); +formData.append('file', fileInput.files[0]); +const result = await api.post('/knowledge/import', formData, { + headers: { 'Content-Type': 'multipart/form-data' } +}); +``` + +--- + +## Next Steps + +- **[User Guide](KNOWLEDGE_USER_GUIDE.md)** — Learn how to use the Knowledge Module +- **[Developer Guide](KNOWLEDGE_DEV.md)** — Architecture and implementation details +- **[Main README](README.md)** — Complete Mosaic Stack documentation + +--- + +**Happy building! 🚀** diff --git a/KNOWLEDGE_DEV.md b/KNOWLEDGE_DEV.md new file mode 100644 index 0000000..1c3fe5d --- /dev/null +++ b/KNOWLEDGE_DEV.md @@ -0,0 +1,1240 @@ +# Knowledge Module - Developer Guide + +Comprehensive developer documentation for the Knowledge Module implementation, architecture, and contribution guidelines. + +## Table of Contents + +1. [Architecture Overview](#architecture-overview) +2. [Database Schema](#database-schema) +3. [Service Layer](#service-layer) +4. [Caching Strategy](#caching-strategy) +5. [Wiki-Link System](#wiki-link-system) +6. [Testing Guide](#testing-guide) +7. [Contributing](#contributing) + +--- + +## Architecture Overview + +The Knowledge Module follows a **layered architecture** pattern: + +``` +┌─────────────────────────────────────────┐ +│ Controllers (REST) │ +│ knowledge | search | tags | import │ +└────────────────┬────────────────────────┘ + │ +┌────────────────▼────────────────────────┐ +│ Service Layer │ +│ KnowledgeService | SearchService │ +│ LinkSyncService | GraphService │ +│ TagsService | ImportExportService │ +│ StatsService | CacheService │ +└────────────────┬────────────────────────┘ + │ +┌────────────────▼────────────────────────┐ +│ Data Access (Prisma ORM) │ +│ KnowledgeEntry | KnowledgeLink │ +│ KnowledgeTag | KnowledgeEntryVersion │ +│ KnowledgeEmbedding │ +└────────────────┬────────────────────────┘ + │ +┌────────────────▼────────────────────────┐ +│ PostgreSQL 17 + pgvector │ +└─────────────────────────────────────────┘ +``` + +### Module Structure + +``` +apps/api/src/knowledge/ +├── controllers/ +│ ├── knowledge.controller.ts # Entry CRUD endpoints +│ ├── search.controller.ts # Search endpoints +│ ├── tags.controller.ts # Tag management +│ ├── import-export.controller.ts # Import/export +│ └── stats.controller.ts # Statistics +├── services/ +│ ├── cache.service.ts # Valkey caching +│ ├── graph.service.ts # Graph traversal +│ ├── import-export.service.ts # File import/export +│ ├── link-resolution.service.ts # Link resolution +│ ├── link-sync.service.ts # Link synchronization +│ ├── search.service.ts # Full-text search +│ └── stats.service.ts # Statistics aggregation +├── entities/ +│ ├── knowledge-entry.entity.ts # Entry DTOs +│ ├── knowledge-entry-version.entity.ts +│ ├── graph.entity.ts # Graph DTOs +│ └── stats.entity.ts # Stats DTOs +├── dto/ +│ ├── create-entry.dto.ts +│ ├── update-entry.dto.ts +│ ├── entry-query.dto.ts +│ ├── search-query.dto.ts +│ └── ... +├── utils/ +│ ├── wiki-link-parser.ts # Wiki-link parsing +│ └── markdown.ts # Markdown rendering +├── knowledge.service.ts # Core entry service +├── tags.service.ts # Tag service +└── knowledge.module.ts # NestJS module +``` + +### Key Responsibilities + +**Controllers** +- HTTP request/response handling +- Input validation (DTOs) +- Permission enforcement (guards) +- Error handling + +**Services** +- Business logic +- Data transformation +- Transaction management +- Cache invalidation + +**Repositories (Prisma)** +- Database queries +- Relation loading +- Type-safe data access + +--- + +## Database Schema + +### Core Models + +#### KnowledgeEntry + +Main entity for knowledge base entries. + +```prisma +model KnowledgeEntry { + id String @id @default(uuid()) @db.Uuid + workspaceId String @map("workspace_id") @db.Uuid + workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) + + // Identity + slug String // URL-friendly identifier + title String // Display name + + // Content + content String @db.Text // Raw markdown + contentHtml String? @map("content_html") @db.Text // Rendered HTML + summary String? // Optional brief description + + // Status + status EntryStatus @default(DRAFT) // DRAFT | PUBLISHED | ARCHIVED + visibility Visibility @default(PRIVATE) // PRIVATE | WORKSPACE | PUBLIC + + // Audit + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz + createdBy String @map("created_by") @db.Uuid + updatedBy String @map("updated_by") @db.Uuid + + // Relations + tags KnowledgeEntryTag[] + outgoingLinks KnowledgeLink[] @relation("SourceEntry") + incomingLinks KnowledgeLink[] @relation("TargetEntry") + versions KnowledgeEntryVersion[] + embedding KnowledgeEmbedding? + + @@unique([workspaceId, slug]) + @@index([workspaceId, status]) + @@index([workspaceId, updatedAt]) + @@map("knowledge_entries") +} +``` + +**Indexes:** +- `workspaceId, slug` (unique constraint) +- `workspaceId, status` (filtering) +- `workspaceId, updatedAt` (recent entries) + +#### KnowledgeLink + +Represents wiki-links between entries. + +```prisma +model KnowledgeLink { + id String @id @default(uuid()) @db.Uuid + + sourceId String @map("source_id") @db.Uuid + source KnowledgeEntry @relation("SourceEntry", fields: [sourceId], references: [id], onDelete: Cascade) + + targetId String @map("target_id") @db.Uuid + target KnowledgeEntry @relation("TargetEntry", fields: [targetId], references: [id], onDelete: Cascade) + + // Link metadata + linkText String @map("link_text") // Original link text from markdown + context String? // Surrounding text (future feature) + + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz + + @@unique([sourceId, targetId]) + @@index([sourceId]) + @@index([targetId]) + @@map("knowledge_links") +} +``` + +**Unique constraint:** +- `sourceId, targetId` (prevents duplicate links) + +**Indexes:** +- `sourceId` (outgoing links lookup) +- `targetId` (backlinks lookup) + +#### KnowledgeEntryVersion + +Version history for entries. + +```prisma +model KnowledgeEntryVersion { + id String @id @default(uuid()) @db.Uuid + entryId String @map("entry_id") @db.Uuid + entry KnowledgeEntry @relation(fields: [entryId], references: [id], onDelete: Cascade) + + version Int // Version number (1, 2, 3, ...) + title String + content String @db.Text + summary String? + + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz + createdBy String @map("created_by") @db.Uuid + author User @relation("EntryVersionAuthor", fields: [createdBy], references: [id]) + changeNote String? @map("change_note") // Optional change description + + @@unique([entryId, version]) + @@index([entryId, version]) + @@map("knowledge_entry_versions") +} +``` + +**Versioning strategy:** +- Auto-incrementing version numbers +- Immutable history (no updates or deletes) +- Snapshot of title, content, summary at time of save + +#### KnowledgeTag + +Tags for categorization. + +```prisma +model KnowledgeTag { + id String @id @default(uuid()) @db.Uuid + workspaceId String @map("workspace_id") @db.Uuid + workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) + + name String // Display name + slug String // URL-friendly identifier + color String? // Hex color (e.g., "#3b82f6") + description String? + + entries KnowledgeEntryTag[] + + @@unique([workspaceId, slug]) + @@index([workspaceId]) + @@map("knowledge_tags") +} +``` + +#### KnowledgeEntryTag + +Many-to-many junction table for entries and tags. + +```prisma +model KnowledgeEntryTag { + entryId String @map("entry_id") @db.Uuid + entry KnowledgeEntry @relation(fields: [entryId], references: [id], onDelete: Cascade) + + tagId String @map("tag_id") @db.Uuid + tag KnowledgeTag @relation(fields: [tagId], references: [id], onDelete: Cascade) + + @@id([entryId, tagId]) + @@index([entryId]) + @@index([tagId]) + @@map("knowledge_entry_tags") +} +``` + +#### KnowledgeEmbedding + +Semantic search embeddings (future feature). + +```prisma +model KnowledgeEmbedding { + id String @id @default(uuid()) @db.Uuid + entryId String @unique @map("entry_id") @db.Uuid + entry KnowledgeEntry @relation(fields: [entryId], references: [id], onDelete: Cascade) + + embedding Unsupported("vector(1536)") // pgvector type + model String // Model used (e.g., "text-embedding-ada-002") + + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz + + @@index([entryId]) + @@map("knowledge_embeddings") +} +``` + +--- + +## Service Layer + +### KnowledgeService + +Core service for entry management. + +**Key methods:** + +```typescript +class KnowledgeService { + // CRUD operations + async findAll(workspaceId, query): Promise + async findOne(workspaceId, slug): Promise + async create(workspaceId, userId, dto): Promise + async update(workspaceId, slug, userId, dto): Promise + async remove(workspaceId, slug, userId): Promise + + // Version management + async findVersions(workspaceId, slug, page, limit): Promise + async findVersion(workspaceId, slug, version): Promise + async restoreVersion(workspaceId, slug, version, userId, changeNote): Promise +} +``` + +**Create flow:** + +```typescript +async create(workspaceId, userId, dto) { + // 1. Generate slug from title + const slug = this.generateUniqueSlug(dto.title, workspaceId); + + // 2. Render markdown to HTML + const contentHtml = renderMarkdown(dto.content); + + // 3. Create entry in transaction + const entry = await this.prisma.$transaction(async (tx) => { + // Create entry + const newEntry = await tx.knowledgeEntry.create({ + data: { + workspaceId, + slug, + title: dto.title, + content: dto.content, + contentHtml, + summary: dto.summary, + status: dto.status || 'DRAFT', + visibility: dto.visibility || 'PRIVATE', + createdBy: userId, + updatedBy: userId, + }, + }); + + // Create initial version (v1) + await tx.knowledgeEntryVersion.create({ + data: { + entryId: newEntry.id, + version: 1, + title: newEntry.title, + content: newEntry.content, + summary: newEntry.summary, + createdBy: userId, + changeNote: dto.changeNote || 'Initial version', + }, + }); + + // Handle tags (create or link) + if (dto.tags && dto.tags.length > 0) { + await this.linkTags(tx, newEntry.id, workspaceId, dto.tags); + } + + return newEntry; + }); + + // 4. Parse and sync wiki-links (outside transaction) + await this.linkSync.syncLinks(entry.id, dto.content); + + // 5. Invalidate caches + await this.cache.invalidateSearchCaches(workspaceId); + + // 6. Return with tags + return this.findOne(workspaceId, slug); +} +``` + +**Update flow:** + +```typescript +async update(workspaceId, slug, userId, dto) { + // 1. Find existing entry + const existing = await this.findOne(workspaceId, slug); + + // 2. Get next version number + const latestVersion = await this.getLatestVersion(existing.id); + const nextVersion = latestVersion.version + 1; + + // 3. Render new HTML if content changed + const contentHtml = dto.content + ? renderMarkdown(dto.content) + : existing.contentHtml; + + // 4. Update in transaction + const updated = await this.prisma.$transaction(async (tx) => { + // Update entry + const updatedEntry = await tx.knowledgeEntry.update({ + where: { id: existing.id }, + data: { + title: dto.title ?? existing.title, + content: dto.content ?? existing.content, + contentHtml, + summary: dto.summary ?? existing.summary, + status: dto.status ?? existing.status, + visibility: dto.visibility ?? existing.visibility, + updatedBy: userId, + }, + }); + + // Create version snapshot + await tx.knowledgeEntryVersion.create({ + data: { + entryId: updatedEntry.id, + version: nextVersion, + title: updatedEntry.title, + content: updatedEntry.content, + summary: updatedEntry.summary, + createdBy: userId, + changeNote: dto.changeNote || `Update to version ${nextVersion}`, + }, + }); + + // Update tags if provided + if (dto.tags !== undefined) { + await this.replaceTags(tx, updatedEntry.id, workspaceId, dto.tags); + } + + return updatedEntry; + }); + + // 5. Re-sync links if content changed + if (dto.content) { + await this.linkSync.syncLinks(updated.id, dto.content); + } + + // 6. Invalidate caches + await this.cache.invalidateEntry(workspaceId, slug); + await this.cache.invalidateSearchCaches(workspaceId); + await this.cache.invalidateGraphCachesForEntry(updated.id); + + // 7. Return updated entry + return this.findOne(workspaceId, slug); +} +``` + +### LinkSyncService + +Manages wiki-link parsing and synchronization. + +**Key methods:** + +```typescript +class LinkSyncService { + async syncLinks(entryId: string, content: string): Promise + async getBacklinks(entryId: string): Promise +} +``` + +**Link sync flow:** + +```typescript +async syncLinks(entryId, content) { + // 1. Parse wiki-links from content + const parsedLinks = parseWikiLinks(content); + + // 2. Get source entry details + const entry = await this.prisma.knowledgeEntry.findUnique({ + where: { id: entryId }, + select: { workspaceId: true }, + }); + + // 3. Delete existing links from this entry + await this.prisma.knowledgeLink.deleteMany({ + where: { sourceId: entryId }, + }); + + // 4. For each parsed link: + for (const link of parsedLinks) { + // Try to resolve target entry + const target = await this.linkResolver.resolve( + link.target, + entry.workspaceId + ); + + if (target) { + // Create resolved link + await this.prisma.knowledgeLink.create({ + data: { + sourceId: entryId, + targetId: target.id, + linkText: link.displayText, + }, + }); + } + // Note: Unresolved links are simply not created + // They may resolve later when target entry is created + } + + // 5. Invalidate graph caches + await this.cache.invalidateGraphCachesForEntry(entryId); +} +``` + +### LinkResolutionService + +Resolves wiki-link targets to actual entries. + +**Resolution strategy:** + +```typescript +async resolve(target: string, workspaceId: string) { + // Strategy 1: Match by exact slug + let entry = await this.prisma.knowledgeEntry.findUnique({ + where: { + workspaceId_slug: { workspaceId, slug: target }, + }, + }); + + if (entry) return entry; + + // Strategy 2: Generate slug from target and try again + const slugified = slugify(target, { lower: true, strict: true }); + entry = await this.prisma.knowledgeEntry.findUnique({ + where: { + workspaceId_slug: { workspaceId, slug: slugified }, + }, + }); + + if (entry) return entry; + + // Strategy 3: Match by title (case-insensitive) + entry = await this.prisma.knowledgeEntry.findFirst({ + where: { + workspaceId, + title: { + equals: target, + mode: 'insensitive', + }, + }, + }); + + return entry || null; +} +``` + +**Resolution examples:** + +| Link Target | Resolution | +|-------------|------------| +| `react-hooks` | Exact slug match | +| `React Hooks` | Slugify to `react-hooks`, then match | +| `REACT HOOKS` | Case-insensitive title match → `React Hooks` | + +### SearchService + +Full-text search and filtering. + +**Key methods:** + +```typescript +class SearchService { + async search(query, workspaceId, options): Promise + async searchByTags(tags, workspaceId, options): Promise + async recentEntries(workspaceId, limit, status?): Promise +} +``` + +**Search implementation:** + +```typescript +async search(query, workspaceId, options) { + // Check cache first + const cached = await this.cache.getSearch(workspaceId, query, options); + if (cached) return cached; + + // Build where clause + const where: Prisma.KnowledgeEntryWhereInput = { + workspaceId, + OR: [ + { title: { contains: query, mode: 'insensitive' } }, + { content: { contains: query, mode: 'insensitive' } }, + ], + }; + + if (options.status) { + where.status = options.status; + } + + // Execute search with pagination + const [entries, total] = await Promise.all([ + this.prisma.knowledgeEntry.findMany({ + where, + include: { tags: { include: { tag: true } } }, + orderBy: { updatedAt: 'desc' }, + skip: (options.page - 1) * options.limit, + take: options.limit, + }), + this.prisma.knowledgeEntry.count({ where }), + ]); + + const result = { + data: entries, + pagination: { + page: options.page, + limit: options.limit, + total, + totalPages: Math.ceil(total / options.limit), + }, + }; + + // Cache the result + await this.cache.setSearch(workspaceId, query, options, result); + + return result; +} +``` + +### GraphService + +Knowledge graph traversal. + +**Key methods:** + +```typescript +class GraphService { + async getEntryGraph( + workspaceId: string, + entryId: string, + maxDepth: number = 1 + ): Promise +} +``` + +**BFS graph traversal:** + +```typescript +async getEntryGraph(workspaceId, entryId, maxDepth) { + // Check cache + const cached = await this.cache.getGraph(workspaceId, entryId, maxDepth); + if (cached) return cached; + + const nodes: GraphNode[] = []; + const edges: GraphEdge[] = []; + const visited = new Set(); + const queue: Array<[string, number]> = [[entryId, 0]]; + + visited.add(entryId); + + while (queue.length > 0) { + const [currentId, depth] = queue.shift()!; + + // Fetch entry with links + const entry = await this.prisma.knowledgeEntry.findUnique({ + where: { id: currentId }, + include: { + tags: { include: { tag: true } }, + outgoingLinks: { include: { target: true } }, + incomingLinks: { include: { source: true } }, + }, + }); + + if (!entry) continue; + + // Add node + nodes.push({ + id: entry.id, + slug: entry.slug, + title: entry.title, + summary: entry.summary, + tags: entry.tags.map(et => ({ + id: et.tag.id, + name: et.tag.name, + slug: et.tag.slug, + color: et.tag.color, + })), + depth, + }); + + // Continue BFS if not at max depth + if (depth < maxDepth) { + // Process outgoing links + for (const link of entry.outgoingLinks) { + edges.push({ + id: link.id, + sourceId: link.sourceId, + targetId: link.targetId, + linkText: link.linkText, + }); + + if (!visited.has(link.targetId)) { + visited.add(link.targetId); + queue.push([link.targetId, depth + 1]); + } + } + + // Process incoming links + for (const link of entry.incomingLinks) { + const edgeExists = edges.some( + e => e.sourceId === link.sourceId && e.targetId === link.targetId + ); + if (!edgeExists) { + edges.push({ + id: link.id, + sourceId: link.sourceId, + targetId: link.targetId, + linkText: link.linkText, + }); + } + + if (!visited.has(link.sourceId)) { + visited.add(link.sourceId); + queue.push([link.sourceId, depth + 1]); + } + } + } + } + + const result = { + centerNode: nodes.find(n => n.id === entryId)!, + nodes, + edges, + stats: { + totalNodes: nodes.length, + totalEdges: edges.length, + maxDepth, + }, + }; + + // Cache result + await this.cache.setGraph(workspaceId, entryId, maxDepth, result); + + return result; +} +``` + +--- + +## Caching Strategy + +The Knowledge Module uses **Valkey** (Redis-compatible) for high-performance caching. + +### Cache Keys + +``` +knowledge:entry:{workspaceId}:{slug} +knowledge:search:{workspaceId}:{query}:{options_hash} +knowledge:search-tags:{workspaceId}:{tags}:{options_hash} +knowledge:graph:{workspaceId}:{entryId}:{depth} +``` + +### Cache Configuration + +```typescript +class KnowledgeCacheService { + private readonly ENTRY_PREFIX = 'knowledge:entry:'; + private readonly SEARCH_PREFIX = 'knowledge:search:'; + private readonly SEARCH_TAGS_PREFIX = 'knowledge:search-tags:'; + private readonly GRAPH_PREFIX = 'knowledge:graph:'; + + private readonly DEFAULT_TTL = 300; // 5 minutes + private readonly isEnabled: boolean; + + constructor() { + this.isEnabled = process.env.KNOWLEDGE_CACHE_ENABLED !== 'false'; + this.DEFAULT_TTL = parseInt(process.env.KNOWLEDGE_CACHE_TTL || '300'); + } +} +``` + +### Invalidation Strategy + +**Entry changes:** +- **Create**: Invalidate search caches +- **Update**: Invalidate entry cache, search caches, graph caches +- **Delete**: Invalidate entry cache, search caches, graph caches + +**Link changes:** +- Invalidate graph caches for source and target entries + +**Tag changes:** +- Invalidate tag-based search caches + +```typescript +async invalidateEntry(workspaceId: string, slug: string) { + const key = `${this.ENTRY_PREFIX}${workspaceId}:${slug}`; + await this.valkey.del(key); + this.stats.deletes++; +} + +async invalidateSearchCaches(workspaceId: string) { + const pattern = `${this.SEARCH_PREFIX}${workspaceId}:*`; + const keys = await this.valkey.keys(pattern); + if (keys.length > 0) { + await this.valkey.del(...keys); + this.stats.deletes += keys.length; + } +} + +async invalidateGraphCachesForEntry(entryId: string) { + // Graph caches include entryId in the key + const pattern = `${this.GRAPH_PREFIX}*:${entryId}:*`; + const keys = await this.valkey.keys(pattern); + if (keys.length > 0) { + await this.valkey.del(...keys); + this.stats.deletes += keys.length; + } +} +``` + +### Performance Metrics + +Track cache effectiveness: + +```typescript +interface CacheStats { + hits: number; + misses: number; + sets: number; + deletes: number; + hitRate: number; +} + +getStats(): CacheStats { + const total = this.stats.hits + this.stats.misses; + return { + ...this.stats, + hitRate: total > 0 ? this.stats.hits / total : 0, + }; +} +``` + +--- + +## Wiki-Link System + +### Parsing Algorithm + +The wiki-link parser handles complex edge cases: + +**Supported syntax:** +```markdown +[[Page Name]] → Link to "Page Name" +[[page-slug]] → Link by slug +[[Page Name|Display Text]] → Custom display text +``` + +**Edge cases handled:** +- Nested brackets: `[[Link with [brackets] inside]]` +- Code blocks: `` `[[not a link]]` `` +- Fenced code: ` ```[[not a link]]``` ` +- Escaped brackets: `\[[not a link]]` +- Triple brackets: `[[[not a link]]]` + +**Parsing flow:** + +1. **Find excluded regions** (code blocks, inline code) +2. **Scan for `[[` patterns** +3. **Find matching `]]`** +4. **Validate link target** +5. **Parse pipe separator for display text** +6. **Return array of WikiLink objects** + +```typescript +interface WikiLink { + raw: string; // "[[Page Name]]" + target: string; // "Page Name" or "page-slug" + displayText: string; // "Page Name" or custom text + start: number; // Position in content + end: number; // Position in content +} +``` + +### Link Resolution + +**Three-step resolution:** + +``` +1. Exact slug match: "react-hooks" → entry with slug "react-hooks" +2. Slugified match: "React Hooks" → slugify → "react-hooks" → match +3. Title match: Case-insensitive title search +``` + +**Unresolved links:** +- Not stored in database +- Will auto-resolve when target entry is created +- Future: UI indication for broken links + +### Link Synchronization + +**On entry create/update:** + +``` +1. Parse wiki-links from content +2. Delete existing links from this entry +3. For each parsed link: + a. Try to resolve target + b. If resolved, create KnowledgeLink record + c. If unresolved, skip (may resolve later) +4. Invalidate graph caches +``` + +**Why delete-and-recreate?** +- Simpler than diffing changes +- Ensures consistency with current content +- Links are cheap to recreate + +--- + +## Testing Guide + +### Test Structure + +``` +apps/api/src/knowledge/ +├── knowledge.service.spec.ts +├── tags.service.spec.ts +├── search.controller.spec.ts +├── tags.controller.spec.ts +├── services/ +│ ├── cache.service.spec.ts +│ ├── graph.service.spec.ts +│ ├── link-resolution.service.spec.ts +│ ├── link-sync.service.spec.ts +│ └── search.service.spec.ts +└── utils/ + ├── markdown.spec.ts + └── wiki-link-parser.spec.ts +``` + +### Running Tests + +```bash +# All knowledge module tests +pnpm test knowledge + +# Specific test file +pnpm test knowledge.service.spec.ts + +# Watch mode +pnpm test:watch knowledge + +# Coverage +pnpm test:coverage +``` + +### Test Coverage Requirements + +- **Minimum:** 85% overall coverage +- **Critical paths:** 100% coverage required + - Entry CRUD operations + - Version management + - Link resolution + - Wiki-link parsing + +### Writing Tests + +**Service tests** (unit): + +```typescript +describe('KnowledgeService', () => { + let service: KnowledgeService; + let prisma: PrismaService; + let linkSync: LinkSyncService; + let cache: KnowledgeCacheService; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + providers: [ + KnowledgeService, + { + provide: PrismaService, + useValue: mockDeep(), + }, + { + provide: LinkSyncService, + useValue: mockDeep(), + }, + { + provide: KnowledgeCacheService, + useValue: mockDeep(), + }, + ], + }).compile(); + + service = module.get(KnowledgeService); + prisma = module.get(PrismaService); + linkSync = module.get(LinkSyncService); + cache = module.get(KnowledgeCacheService); + }); + + describe('create', () => { + it('should create entry with unique slug', async () => { + const dto = { + title: 'Test Entry', + content: '# Test', + }; + + prisma.knowledgeEntry.create.mockResolvedValue({ + id: 'entry-id', + slug: 'test-entry', + ...dto, + }); + + const result = await service.create('workspace-id', 'user-id', dto); + + expect(result.slug).toBe('test-entry'); + expect(linkSync.syncLinks).toHaveBeenCalledWith('entry-id', dto.content); + expect(cache.invalidateSearchCaches).toHaveBeenCalled(); + }); + }); +}); +``` + +**Controller tests** (integration): + +```typescript +describe('KnowledgeController (e2e)', () => { + let app: INestApplication; + let prisma: PrismaService; + + beforeAll(async () => { + const module = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = module.createNestApplication(); + await app.init(); + + prisma = module.get(PrismaService); + }); + + afterAll(async () => { + await prisma.$disconnect(); + await app.close(); + }); + + describe('POST /knowledge/entries', () => { + it('should create entry and return 201', () => { + return request(app.getHttpServer()) + .post('/knowledge/entries') + .set('Authorization', `Bearer ${authToken}`) + .set('x-workspace-id', workspaceId) + .send({ + title: 'Test Entry', + content: '# Test Content', + status: 'DRAFT', + }) + .expect(201) + .expect((res) => { + expect(res.body.slug).toBe('test-entry'); + expect(res.body.status).toBe('DRAFT'); + }); + }); + }); +}); +``` + +**Utility tests:** + +```typescript +describe('parseWikiLinks', () => { + it('should parse simple wiki link', () => { + const content = 'See [[My Page]] for details.'; + const links = parseWikiLinks(content); + + expect(links).toHaveLength(1); + expect(links[0]).toMatchObject({ + target: 'My Page', + displayText: 'My Page', + raw: '[[My Page]]', + }); + }); + + it('should parse link with custom display text', () => { + const content = 'See [[page-slug|custom text]] here.'; + const links = parseWikiLinks(content); + + expect(links[0]).toMatchObject({ + target: 'page-slug', + displayText: 'custom text', + }); + }); + + it('should ignore links in code blocks', () => { + const content = '```\n[[Not A Link]]\n```'; + const links = parseWikiLinks(content); + + expect(links).toHaveLength(0); + }); +}); +``` + +### Test Data Setup + +Create reusable fixtures: + +```typescript +// test/fixtures/knowledge.fixtures.ts +export const createMockEntry = (overrides = {}) => ({ + id: 'entry-uuid', + workspaceId: 'workspace-uuid', + slug: 'test-entry', + title: 'Test Entry', + content: '# Test', + contentHtml: '

Test

', + summary: null, + status: 'DRAFT', + visibility: 'PRIVATE', + createdAt: new Date(), + updatedAt: new Date(), + createdBy: 'user-uuid', + updatedBy: 'user-uuid', + ...overrides, +}); + +export const createMockVersion = (entryId: string, version: number) => ({ + id: `version-${version}-uuid`, + entryId, + version, + title: 'Test Entry', + content: `# Version ${version}`, + summary: null, + changeNote: `Update ${version}`, + createdAt: new Date(), + createdBy: 'user-uuid', +}); +``` + +--- + +## Contributing + +### Development Workflow + +1. **Create feature branch** + ```bash + git checkout -b feature/your-feature develop + ``` + +2. **Write tests first** (TDD approach) + ```bash + pnpm test:watch knowledge + ``` + +3. **Implement feature** + - Follow TypeScript strict mode + - Use existing patterns + - Add JSDoc comments + +4. **Run tests and linting** + ```bash + pnpm test knowledge + pnpm lint + pnpm format + ``` + +5. **Commit with conventional format** + ```bash + git commit -m "feat(knowledge): add semantic search endpoint" + ``` + +6. **Create pull request to `develop`** + +### Code Style + +**TypeScript:** +- Strict mode enabled +- No `any` types +- Explicit return types +- Interface over type when possible + +**NestJS conventions:** +- Services are `@Injectable()` +- Controllers use `@Controller()`, `@Get()`, etc. +- DTOs with class-validator decorators +- Dependency injection via constructor + +**Naming:** +- `camelCase` for variables and functions +- `PascalCase` for classes and interfaces +- `UPPER_SNAKE_CASE` for constants +- `kebab-case` for file names + +### Adding New Features + +**New endpoint:** + +1. Create DTO in `dto/` +2. Add controller method with proper guards +3. Implement service method +4. Write tests (unit + integration) +5. Update API documentation + +**New service:** + +1. Create service class in `services/` +2. Add `@Injectable()` decorator +3. Register in `knowledge.module.ts` +4. Write comprehensive tests +5. Document public API with JSDoc + +**Database changes:** + +1. Update `schema.prisma` +2. Create migration: `pnpm prisma migrate dev --name your_migration_name` +3. Update entity interfaces in `entities/` +4. Update services to use new schema +5. Write migration tests + +### Performance Considerations + +**Always consider:** +- Database query efficiency (use indexes) +- N+1 query problems (use `include` wisely) +- Cache invalidation strategy +- Transaction boundaries +- Large content handling + +**Optimization checklist:** +- [ ] Proper indexes on database columns +- [ ] Caching for expensive operations +- [ ] Pagination for list endpoints +- [ ] Lazy loading for relations +- [ ] Bulk operations where possible + +### Security Checklist + +- [ ] Input validation (DTOs) +- [ ] Permission guards on endpoints +- [ ] Workspace isolation (never cross workspaces) +- [ ] SQL injection prevention (Prisma handles this) +- [ ] No sensitive data in logs +- [ ] Rate limiting (future) + +--- + +## Additional Resources + +- **[User Guide](KNOWLEDGE_USER_GUIDE.md)** — End-user documentation +- **[API Documentation](KNOWLEDGE_API.md)** — Complete API reference +- **[Main README](README.md)** — Project overview +- **[NestJS Docs](https://docs.nestjs.com/)** — Framework documentation +- **[Prisma Docs](https://www.prisma.io/docs)** — ORM documentation + +--- + +**Happy coding! 🚀** diff --git a/KNOWLEDGE_USER_GUIDE.md b/KNOWLEDGE_USER_GUIDE.md new file mode 100644 index 0000000..46f2db1 --- /dev/null +++ b/KNOWLEDGE_USER_GUIDE.md @@ -0,0 +1,628 @@ +# Knowledge Module - User Guide + +The Knowledge Module is a powerful, personal wiki and knowledge management system built into Mosaic Stack. Create interconnected notes, organize with tags, track changes over time, and visualize relationships between your knowledge entries. + +## Table of Contents + +1. [Getting Started](#getting-started) +2. [Creating Entries](#creating-entries) +3. [Wiki-links and Backlinks](#wiki-links-and-backlinks) +4. [Tags and Organization](#tags-and-organization) +5. [Search](#search) +6. [Import/Export](#importexport) +7. [Version History](#version-history) +8. [Graph Visualization](#graph-visualization) + +--- + +## Getting Started + +The Knowledge Module provides a flexible way to capture and organize information: + +- **Markdown-based**: Write entries using Markdown for rich formatting +- **Wiki-style linking**: Connect entries using `[[wiki-links]]` +- **Tag-based organization**: Categorize entries with tags +- **Full version history**: Every edit is tracked and recoverable +- **Powerful search**: Find entries with full-text search +- **Visual knowledge graph**: See relationships between entries +- **Import/Export**: Bulk import/export for portability + +### Entry Lifecycle + +Each knowledge entry has three possible statuses: + +- **DRAFT** — Entry is being worked on, visible only to you +- **PUBLISHED** — Entry is complete and visible to workspace members +- **ARCHIVED** — Entry is hidden from normal views but preserved + +And three visibility levels: + +- **PRIVATE** — Only visible to you +- **WORKSPACE** — Visible to all workspace members +- **PUBLIC** — Visible to anyone (future feature) + +--- + +## Creating Entries + +### Basic Entry Creation + +Every entry has: + +- **Title** (required) — The entry name (up to 500 characters) +- **Content** (required) — Markdown-formatted text +- **Summary** (optional) — Brief description (up to 1000 characters) +- **Tags** (optional) — Categories for organization +- **Status** — DRAFT, PUBLISHED, or ARCHIVED +- **Visibility** — PRIVATE, WORKSPACE, or PUBLIC + +### Slugs + +When you create an entry, the system automatically generates a unique **slug** from the title: + +- `"My First Entry"` → `my-first-entry` +- `"API Design Patterns"` → `api-design-patterns` +- `"React Hooks Guide"` → `react-hooks-guide` + +Slugs are used in URLs and wiki-links. They're unique per workspace. + +### Example Entry + +```markdown +Title: React Component Patterns + +Content: +## Component Composition + +React components can be composed using several patterns: + +### Container/Presentational Pattern +Separate data logic (containers) from UI (presentational). + +See also: [[React Hooks]], [[State Management]] + +Tags: react, frontend, patterns +``` + +### Change Notes + +When creating or updating entries, you can add an optional **change note** to describe what you changed: + +``` +"Added section on custom hooks" +"Fixed typo in code example" +"Initial draft" +``` + +Change notes appear in version history, making it easy to track why changes were made. + +--- + +## Wiki-links and Backlinks + +### Creating Links + +Link to other entries using **wiki-link syntax**: + +```markdown +[[Entry Title]] +[[entry-slug]] +[[Entry Title|Custom Link Text]] +``` + +Examples: + +```markdown +For more details, see [[API Documentation]]. +Learn about [[react-hooks|React Hooks]]. +Related: [[Frontend Best Practices]], [[TypeScript Guide]] +``` + +### How Wiki-links Work + +1. **Automatic resolution**: The system finds the target entry by slug or title +2. **Smart matching**: Links work with slugs (`react-hooks`) or titles (`React Hooks`) +3. **Custom text**: Use `[[slug|display text]]` for custom link text +4. **Auto-linking**: Links are parsed and resolved when you save the entry + +### Unresolved Links + +If you link to an entry that doesn't exist yet, it's marked as **unresolved**: + +```markdown +[[Future Entry That Doesn't Exist Yet]] +``` + +Unresolved links: +- Are still stored and tracked +- Will automatically resolve when the target entry is created +- Show as unlinked in the UI (implementation-specific) + +This lets you create links before creating the entries they point to! + +### Backlinks + +Every entry automatically tracks its **backlinks** — entries that link *to* it. + +**Example**: If entry "React Hooks" is linked from: +- "Frontend Guide" +- "Component Patterns" +- "State Management" + +Then "React Hooks" will show 3 backlinks. + +**Use backlinks to:** +- Discover related content +- Understand entry relationships +- Navigate bidirectionally through knowledge + +Access backlinks via: `GET /api/knowledge/entries/:slug/backlinks` + +--- + +## Tags and Organization + +### Creating Tags + +Tags help categorize and organize entries. Create tags with: + +- **Name** (required) — Display name (e.g., "Frontend Development") +- **Slug** (auto-generated) — URL-friendly identifier (e.g., "frontend-development") +- **Color** (optional) — Hex color for visual organization (e.g., "#3b82f6") +- **Description** (optional) — Tag purpose or usage notes + +### Tagging Entries + +Add tags when creating or updating entries: + +```json +{ + "title": "React Component Guide", + "content": "...", + "tags": ["react", "frontend", "tutorial"] +} +``` + +### Finding Tagged Entries + +Search for entries with specific tags: + +``` +GET /api/knowledge/search/by-tags?tags=react,frontend +``` + +This finds entries that have **ALL** specified tags (AND logic). + +### Tag Management + +- **List all tags**: `GET /api/knowledge/tags` +- **Get tag details**: `GET /api/knowledge/tags/:slug` +- **Get tagged entries**: `GET /api/knowledge/tags/:slug/entries` +- **Update tag**: `PUT /api/knowledge/tags/:slug` +- **Delete tag**: `DELETE /api/knowledge/tags/:slug` (admin only) + +Deleting a tag removes it from all entries but doesn't delete the entries themselves. + +--- + +## Search + +The Knowledge Module provides powerful search capabilities: + +### Full-Text Search + +Search across entry titles and content with relevance ranking: + +``` +GET /api/knowledge/search?q=react hooks&page=1&limit=20 +``` + +**Features:** +- Searches **title** and **content** fields +- Relevance ranking (best matches first) +- Case-insensitive +- Partial word matching +- Pagination support + +**Query parameters:** +- `q` (required) — Search query string +- `status` (optional) — Filter by status (DRAFT, PUBLISHED, ARCHIVED) +- `page` (default: 1) — Page number +- `limit` (default: 20, max: 100) — Results per page + +### Tag-Based Search + +Find entries with specific tags: + +``` +GET /api/knowledge/search/by-tags?tags=react,typescript +``` + +Returns entries that have **ALL** specified tags. + +### Recent Entries + +Get recently modified entries: + +``` +GET /api/knowledge/search/recent?limit=10&status=PUBLISHED +``` + +**Parameters:** +- `limit` (default: 10, max: 50) — Number of entries +- `status` (optional) — Filter by status + +Perfect for "what's new" or "recently updated" views. + +### Combining Filters + +All search endpoints support status filtering: + +- `status=DRAFT` — Only draft entries +- `status=PUBLISHED` — Only published entries +- `status=ARCHIVED` — Only archived entries +- (no status) — All statuses + +--- + +## Import/Export + +### Exporting Entries + +Export your knowledge base for backup or migration: + +``` +GET /api/knowledge/export?format=markdown +``` + +**Export formats:** + +1. **Markdown** (default) + - Each entry saved as `.md` file + - Filename: `{slug}.md` + - Front matter with metadata (title, tags, status, etc.) + - Returns `.zip` archive + +2. **JSON** + - Structured JSON format + - Complete entry data including metadata + - Returns `.zip` archive + +**Export specific entries:** + +``` +GET /api/knowledge/export?format=markdown&entryIds[]=uuid1&entryIds[]=uuid2 +``` + +If `entryIds` is omitted, exports **all entries** in the workspace. + +**Example Markdown export:** + +```markdown +--- +slug: react-hooks-guide +title: React Hooks Guide +status: PUBLISHED +visibility: WORKSPACE +tags: react, frontend +createdAt: 2024-01-29T10:00:00Z +updatedAt: 2024-01-30T15:30:00Z +--- + +# React Hooks Guide + +Content goes here... + +[[Related Entry]] +``` + +### Importing Entries + +Import entries from `.md` or `.zip` files: + +```bash +curl -X POST http://localhost:3001/api/knowledge/import \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-workspace-id: WORKSPACE_ID" \ + -F "file=@knowledge-export.zip" +``` + +**Supported formats:** + +1. **Single Markdown file** (`.md`) + - Creates one entry + - Reads front matter for metadata + - Generates slug from filename if not in front matter + +2. **Zip archive** (`.zip`) + - Multiple `.md` files + - Each file becomes one entry + - Front matter optional + +**Import behavior:** + +- **New entries**: Creates with status DRAFT +- **Existing entries** (matching slug): Skipped (does not overwrite) +- **Wiki-links**: Preserved and will resolve if targets exist +- **Tags**: Created if they don't exist +- **Validation**: Invalid entries are skipped with error details + +**Response:** + +```json +{ + "success": true, + "totalFiles": 10, + "imported": 8, + "failed": 2, + "results": [ + { + "filename": "react-hooks.md", + "success": true, + "entryId": "uuid", + "slug": "react-hooks" + }, + { + "filename": "invalid-entry.md", + "success": false, + "error": "Title is required" + } + ] +} +``` + +### File Size Limits + +- Maximum file size: **50MB** +- Accepted file types: `.md`, `.zip` + +--- + +## Version History + +Every edit to a knowledge entry is automatically saved as a **version**. You can view history, compare changes, and restore previous versions. + +### How Versioning Works + +- **Automatic versioning**: Every update creates a new version +- **Version numbers**: Auto-incremented (1, 2, 3, ...) +- **What's tracked**: Title, content, summary +- **Change notes**: Optional message describing the change +- **Author tracking**: Who made each change +- **Timestamps**: When each version was created + +### Viewing Version History + +**List all versions for an entry:** + +``` +GET /api/knowledge/entries/:slug/versions?page=1&limit=20 +``` + +Returns paginated list of versions with: +- Version number +- Title +- Summary +- Change note +- Author info +- Timestamp + +**Get a specific version:** + +``` +GET /api/knowledge/entries/:slug/versions/:version +``` + +Returns the complete entry as it existed at that version: +- Title +- Content +- Summary +- Change note +- Author +- Created timestamp + +### Restoring a Previous Version + +Restore an entry to a previous version: + +``` +POST /api/knowledge/entries/:slug/restore/:version +Body: { "changeNote": "Restored version 5" } +``` + +**What happens:** +1. Creates a **new version** with content from the specified version +2. The change note is required to document why you restored +3. Original versions remain intact (no data loss) +4. Version numbers continue incrementing (no rewriting history) + +**Example:** +- Current version: 10 +- Restore version 5 +- New version created: 11 (with content from version 5) + +### Best Practices + +- **Write meaningful change notes**: "Added examples" is better than "Updated" +- **Review before publishing**: Keep entries in DRAFT while iterating +- **Restore carefully**: Preview the old version before restoring +- **Use versions for comparison**: See how entries evolved over time + +--- + +## Graph Visualization + +The Knowledge Module includes a powerful **graph visualization** feature (currently available via service layer, REST endpoint coming soon). + +### How the Graph Works + +The knowledge graph represents: + +- **Nodes**: Knowledge entries +- **Edges**: Wiki-links between entries +- **Relationships**: Bidirectional (incoming and outgoing links) +- **Depth traversal**: Explore connections up to N levels deep + +### Entry-Centered Graph + +Get a graph view centered on a specific entry: + +```typescript +// Service layer (REST endpoint coming soon) +const graph = await graphService.getEntryGraph( + workspaceId, + entryId, + maxDepth // default: 1 +); +``` + +**Response structure:** + +```typescript +{ + centerNode: { + id: "uuid", + slug: "react-hooks", + title: "React Hooks Guide", + summary: "Comprehensive guide to React Hooks", + tags: [ + { id: "uuid", name: "React", slug: "react", color: "#61dafb" } + ], + depth: 0 + }, + nodes: [ + // All connected entries up to maxDepth + { id: "uuid", slug: "...", title: "...", depth: 1 }, + { id: "uuid", slug: "...", title: "...", depth: 2 } + ], + edges: [ + { + id: "uuid", + sourceId: "entry1-uuid", + targetId: "entry2-uuid", + linkText: "React Hooks" + } + ], + stats: { + totalNodes: 15, + totalEdges: 22, + maxDepth: 2 + } +} +``` + +### Graph Properties + +- **Depth 0**: Just the center node (no connections) +- **Depth 1**: Center node + directly connected entries +- **Depth 2**: Depth 1 + entries connected to depth 1 nodes +- **Depth N**: Continue expanding N levels + +**Node information:** +- Entry metadata (slug, title, summary) +- Tags with colors +- Depth level from center + +**Edge information:** +- Source and target entry IDs +- Original link text from the markdown +- Unique link identifier + +### Use Cases + +- **Discover connections**: Find related entries +- **Visualize knowledge structure**: See how concepts relate +- **Navigate bidirectionally**: Follow links in both directions +- **Cluster analysis**: Identify knowledge hubs (highly connected entries) +- **Content gap analysis**: Find isolated entries needing more connections + +### Performance & Caching + +Graph queries are **cached** for performance: + +- **Cache key**: `workspace:entry:depth` +- **TTL**: 5 minutes (configurable) +- **Invalidation**: Automatic on entry or link updates + +Large graphs (depth > 2) can be expensive. The cache ensures fast repeat access. + +--- + +## Tips & Best Practices + +### Content Organization + +1. **Start with outlines**: Create stub entries, fill in later +2. **Link early and often**: Wiki-links are cheap, use them liberally +3. **Tag consistently**: Establish a tag taxonomy early +4. **Write summaries**: Help future-you find content faster +5. **Use DRAFT status**: Iterate privately before publishing + +### Naming Conventions + +- **Titles**: Clear, descriptive, unique +- **Slugs**: Auto-generated, don't worry about them +- **Tags**: Short, lowercase, consistent naming (e.g., `react` not `React` or `ReactJS`) + +### Knowledge Graph Health + +- **Avoid orphans**: Link new entries to existing content +- **Create hubs**: Some entries naturally become central (index pages) +- **Bidirectional linking**: Link both ways when relationships are mutual +- **Tag hubs**: Use tags for broad categories, links for specific relationships + +### Workflow Patterns + +**Personal Wiki:** +``` +Draft → Link → Tag → Publish → Iterate +``` + +**Team Knowledge Base:** +``` +Draft → Review → Link → Tag → Publish → Maintain +``` + +**Research Notes:** +``` +Capture → Organize → Synthesize → Link → Archive +``` + +--- + +## Permissions + +Knowledge Module endpoints require specific permissions: + +- **Read** (ANY workspace member) + - List entries + - View entries + - View backlinks + - View versions + - Search + - Export + +- **Write** (MEMBER role or higher) + - Create entries + - Update entries + - Import entries + - Restore versions + +- **Delete/Admin** (ADMIN role or higher) + - Archive entries + - Delete entries + - Clear cache + +See [API Documentation](KNOWLEDGE_API.md) for complete endpoint permissions. + +--- + +## Next Steps + +- **[API Documentation](KNOWLEDGE_API.md)** — Complete REST API reference +- **[Developer Guide](KNOWLEDGE_DEV.md)** — Architecture and implementation details +- **[Main README](README.md)** — Full Mosaic Stack documentation + +--- + +**Happy knowledge building! 🧠✨** diff --git a/README.md b/README.md index 49a8ecb..26d70c5 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Multi-tenant personal assistant platform with PostgreSQL backend, Authentik SSO, Mosaic Stack is a modern, PDA-friendly platform designed to help users manage their personal and professional lives with: - **Multi-user workspaces** with team collaboration +- **Knowledge management** with wiki-style linking and version history - **Task management** with flexible organization - **Event & calendar** integration - **Project tracking** with Gantt charts and Kanban boards @@ -185,6 +186,111 @@ mosaic-stack/ See the [issue tracker](https://git.mosaicstack.dev/mosaic/stack/issues) for complete roadmap. +## Knowledge Module + +The **Knowledge Module** is a powerful personal wiki and knowledge management system built into Mosaic Stack. Create interconnected notes, organize with tags, track changes over time, and visualize relationships. + +### Features + +- **📝 Markdown-based entries** — Write using familiar Markdown syntax +- **🔗 Wiki-style linking** — Connect entries using `[[wiki-links]]` +- **🏷️ Tag organization** — Categorize and filter with flexible tagging +- **📜 Full version history** — Every edit is tracked and recoverable +- **🔍 Powerful search** — Full-text search across titles and content +- **📊 Knowledge graph** — Visualize relationships between entries +- **📤 Import/Export** — Bulk import/export for portability +- **⚡ Valkey caching** — High-performance caching for fast access + +### Quick Examples + +**Create an entry:** +```bash +curl -X POST http://localhost:3001/api/knowledge/entries \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-workspace-id: WORKSPACE_ID" \ + -d '{ + "title": "React Hooks Guide", + "content": "# React Hooks\n\nSee [[Component Patterns]] for more.", + "tags": ["react", "frontend"], + "status": "PUBLISHED" + }' +``` + +**Search entries:** +```bash +curl -X GET 'http://localhost:3001/api/knowledge/search?q=react+hooks' \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-workspace-id: WORKSPACE_ID" +``` + +**Export knowledge base:** +```bash +curl -X GET 'http://localhost:3001/api/knowledge/export?format=markdown' \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-workspace-id: WORKSPACE_ID" \ + -o knowledge-export.zip +``` + +### Documentation + +- **[User Guide](KNOWLEDGE_USER_GUIDE.md)** — Getting started, features, and workflows +- **[API Documentation](KNOWLEDGE_API.md)** — Complete REST API reference with examples +- **[Developer Guide](KNOWLEDGE_DEV.md)** — Architecture, implementation, and contributing + +### Key Concepts + +**Wiki-links** +Connect entries using double-bracket syntax: +```markdown +See [[Entry Title]] or [[entry-slug]] for details. +Use [[Page|custom text]] for custom display text. +``` + +**Version History** +Every edit creates a new version. View history, compare changes, and restore previous versions: +```bash +# List versions +GET /api/knowledge/entries/:slug/versions + +# Get specific version +GET /api/knowledge/entries/:slug/versions/:version + +# Restore version +POST /api/knowledge/entries/:slug/restore/:version +``` + +**Backlinks** +Automatically discover entries that link to a given entry: +```bash +GET /api/knowledge/entries/:slug/backlinks +``` + +**Tags** +Organize entries with tags: +```bash +# Create tag +POST /api/knowledge/tags +{ "name": "React", "color": "#61dafb" } + +# Find entries with tags +GET /api/knowledge/search/by-tags?tags=react,frontend +``` + +### Performance + +With Valkey caching enabled: +- **Entry retrieval:** ~2-5ms (vs ~50ms uncached) +- **Search queries:** ~2-5ms (vs ~200ms uncached) +- **Graph traversals:** ~2-5ms (vs ~400ms uncached) +- **Cache hit rates:** 70-90% for active workspaces + +Configure caching via environment variables: +```bash +VALKEY_URL=redis://localhost:6379 +KNOWLEDGE_CACHE_ENABLED=true +KNOWLEDGE_CACHE_TTL=300 # 5 minutes +``` + ## Development Workflow ### Branch Strategy -- 2.49.1