├── .DS_Store ├── .cline_custom_instructions ├── .clinerules ├── .gitignore ├── LICENSE ├── README.md ├── app └── page.tsx ├── components └── facts │ ├── columns.ts │ └── data-table.tsx ├── docs ├── side-by-side.png └── validation │ └── 2025-02-24-validation-report.md ├── facts-server ├── .DS_Store ├── build │ ├── index.d.ts │ ├── index.js │ ├── sdk │ │ ├── server.d.ts │ │ ├── server.js │ │ ├── stdio.d.ts │ │ ├── stdio.js │ │ ├── types.d.ts │ │ └── types.js │ ├── storage.d.ts │ ├── storage.js │ ├── types.d.ts │ ├── types.js │ ├── validation.d.ts │ └── validation.js ├── package-lock.json ├── package.json ├── prisma │ ├── facts.db │ ├── migrations │ │ ├── 20250223115817_init │ │ │ └── migration.sql │ │ ├── 20250223171046_add_fact_category │ │ │ └── migration.sql │ │ └── migration_lock.toml │ ├── schema.prisma │ └── setup_vector.sql ├── src │ ├── index.ts │ ├── storage.ts │ ├── types.ts │ └── validation.ts └── tsconfig.json ├── facts-ui ├── .gitignore ├── README.md ├── app │ ├── api │ │ └── mcp │ │ │ └── route.ts │ ├── facts │ │ └── new │ │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── components.json ├── components │ ├── facts │ │ ├── columns.tsx │ │ └── data-table.tsx │ └── ui │ │ └── button.tsx ├── eslint.config.mjs ├── jsconfig.json ├── lib │ └── api │ │ └── facts.ts ├── next-env.d.ts ├── next.config.ts ├── package-lock.json ├── package.json ├── path │ └── to │ │ └── file ├── postcss.config.mjs ├── prisma │ └── schema.prisma ├── public │ ├── file.svg │ ├── globe.svg │ ├── next.svg │ ├── vercel.svg │ └── window.svg ├── src │ ├── components │ │ ├── facts │ │ │ ├── columns.tsx │ │ │ ├── data-table.tsx │ │ │ └── new-fact-form.tsx │ │ └── ui │ │ │ ├── badge.tsx │ │ │ ├── button.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── input.tsx │ │ │ ├── select.tsx │ │ │ ├── sonner.tsx │ │ │ ├── table.tsx │ │ │ └── textarea.tsx │ └── lib │ │ ├── api │ │ └── facts.ts │ │ ├── prisma.ts │ │ ├── types │ │ └── facts.ts │ │ └── utils.ts ├── tailwind.config.ts └── tsconfig.json ├── hivemind-ui └── next-env.d.ts ├── hivemind └── package.json ├── index.ts ├── lib └── api │ └── facts.ts └── src └── components └── facts └── data-table.tsx /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewhopper/hivemind/b82fb50a74b5eadef26a19cc1f69a178772b29d3/.DS_Store -------------------------------------------------------------------------------- /.cline_custom_instructions: -------------------------------------------------------------------------------- 1 | You have access to a knowledgebase MCP server that stores your guidance for architecting, implementing and validating software. Use this in your workflow to fetch and store architecture decisions. Make sure to use the get all facts service to check for libraries that could be useful. For example if building a frontend you'll search with get all facts, see that NextJS with Tailwind and ShadCN is preferred. Or when selecting a DB you'll see that Postgres is required. If you add a new major library like ORM or AUTH that's not in the facts ask the user for approval. 2 | 3 | # Fact Validation Rules 4 | 5 | ## Pre-Planning Phase 6 | 1. Before creating any plan, check the facts server for: 7 | - Required architecture patterns for the project type 8 | - Mandatory tools and frameworks 9 | - Project structure requirements 10 | - Database requirements if applicable 11 | - Security requirements if applicable 12 | 13 | 2. Use these facts to: 14 | - Determine which tools and frameworks must be used 15 | - Identify required project structure 16 | - Set constraints for the planning phase 17 | - Generate initial requirements list 18 | 19 | ## Planning Phase 20 | 1. While creating the plan: 21 | - Reference fact requirements for each architectural decision 22 | - Validate tool choices against fact dependencies 23 | - Ensure compliance with required patterns 24 | - Check version compatibility between components 25 | 26 | 2. Before finalizing the plan: 27 | - Validate against all REQUIRED facts 28 | - Check compatibility between chosen components 29 | - Verify adherence to architectural patterns 30 | - Generate a planning validation report 31 | 32 | ## Implementation Phase 33 | 1. During implementation: 34 | - Follow fact-based patterns and practices 35 | - Use required tools and frameworks 36 | - Implement required security measures 37 | - Follow documentation requirements 38 | 39 | 2. After implementation: 40 | - Run automated validation against facts 41 | - Generate compliance report 42 | - Check all acceptance criteria 43 | - Verify all required patterns are followed -------------------------------------------------------------------------------- /.clinerules: -------------------------------------------------------------------------------- 1 | version: "1.0" 2 | 3 | # Communication settings 4 | communication: 5 | emojis: 6 | design: 7 | start: "🎨 Starting design phase..." 8 | thinking: "🤔 Analyzing design requirements..." 9 | diagram: "📊 Creating system diagram..." 10 | complete: "✨ Design spec created in docs/design/" 11 | 12 | implement: 13 | start: "🚀 Starting implementation..." 14 | progress: "⚙️ Implementing features..." 15 | testing: "🧪 Testing implementation..." 16 | complete: "✅ Implementation finished" 17 | 18 | validate: 19 | start: "🔍 Starting validation..." 20 | checking: "📋 Checking against facts..." 21 | issue: "⚠️ Found compliance issue..." 22 | pass: "✅ Validation passed" 23 | fail: "❌ Validation failed" 24 | complete: "📝 Validation report created in docs/validation/" 25 | 26 | revise: 27 | start: "🔄 Starting revision..." 28 | progress: "📝 Making changes..." 29 | complete: "✨ Revision complete" 30 | 31 | refactor: 32 | start: "🔧 Starting refactoring..." 33 | progress: "🔨 Refactoring in progress..." 34 | metrics: "📊 Checking quality metrics..." 35 | complete: "✨ Refactoring complete" 36 | 37 | # Documentation settings 38 | documentation: 39 | output_dir: "./docs" 40 | templates: 41 | design: 42 | path: "docs/design" 43 | files: 44 | - name: "{timestamp}-design-spec.md" 45 | sections: 46 | - "Overview" 47 | - "Architecture" 48 | - "Components" 49 | - "Data Flow" 50 | - "Technical Decisions" 51 | 52 | validate: 53 | path: "docs/validation" 54 | files: 55 | - name: "{timestamp}-validation-report.md" 56 | sections: 57 | - "Executive Summary" 58 | - "Validation Context" 59 | - "Required Facts" 60 | subsections: 61 | - "Compliant" 62 | - "Non-compliant" 63 | - "Not Applicable" 64 | - "Recommended Facts" 65 | subsections: 66 | - "Implemented" 67 | - "Missing" 68 | - "Performance Metrics" 69 | - "Security Analysis" 70 | - "Code Quality Metrics" 71 | - "Issues" 72 | subsections: 73 | - "Critical" 74 | - "Major" 75 | - "Minor" 76 | - "Recommendations" 77 | - "Next Steps" 78 | 79 | # Operation modes configuration 80 | modes: 81 | design: 82 | description: "Architecture and system design phase" 83 | allowed_operations: 84 | - read_file 85 | - list_files 86 | - search_files 87 | - list_code_definition_names 88 | - write_to_file: 89 | extensions: [".md", ".mdx", ".mmd"] 90 | - access_mcp_resource 91 | - use_mcp_tool 92 | - ask_followup_question 93 | restrictions: 94 | - "No code modifications allowed" 95 | - "Can only create/modify markdown and mermaid files" 96 | - "Focus on architecture documentation and diagrams" 97 | 98 | implement: 99 | description: "Code implementation phase" 100 | allowed_operations: 101 | - all 102 | restrictions: 103 | - "Must follow design documents" 104 | - "Must validate against facts" 105 | 106 | validate: 107 | description: "Verification and compliance checking" 108 | allowed_operations: 109 | - read_file 110 | - list_files 111 | - search_files 112 | - list_code_definition_names 113 | - write_to_file: 114 | extensions: [".md"] 115 | - browser_action 116 | - execute_command: 117 | type: "read-only" 118 | - access_mcp_resource 119 | - use_mcp_tool 120 | - ask_followup_question 121 | restrictions: 122 | - "No code modifications allowed" 123 | - "Can only create validation reports in markdown" 124 | - "Can run tests and analysis tools" 125 | 126 | revise: 127 | description: "Code modification and improvement" 128 | allowed_operations: 129 | - all 130 | restrictions: 131 | - "Must maintain existing architecture" 132 | - "Changes must be validated against facts" 133 | 134 | refactor: 135 | description: "Code restructuring and optimization" 136 | allowed_operations: 137 | - all 138 | restrictions: 139 | - "Must maintain existing functionality" 140 | - "Must improve code quality metrics" 141 | - "Changes must be validated against facts" 142 | 143 | # Facts integration settings 144 | facts: 145 | # Automatically save decisions made during development 146 | auto_save: true 147 | 148 | # Default settings for new facts 149 | defaults: 150 | strictness: "RECOMMENDED" # Default strictness level 151 | min_version: "1.0.0" # Minimum version compatibility 152 | max_version: "*" # Maximum version compatibility ("*" means latest) 153 | 154 | # Validation settings 155 | validation: 156 | # Validate all proposals against existing facts 157 | validate_proposals: true 158 | # Stop if validation fails for REQUIRED facts 159 | strict_validation: true 160 | # Categories to validate against 161 | categories: [ 162 | "ARCHITECTURE_PATTERN", 163 | "DATABASE", 164 | "FRONTEND", 165 | "BACKEND", 166 | "DOCUMENTATION" 167 | ] 168 | 169 | # Validation stages 170 | stages: 171 | pre_planning: 172 | enabled: true 173 | categories: 174 | - ARCHITECTURE_PATTERN 175 | - PROJECT_STRUCTURE 176 | - FRAMEWORK 177 | - DATABASE 178 | - SECURITY 179 | actions: 180 | on_validation: 181 | - check_required_facts 182 | - determine_tools 183 | - set_constraints 184 | on_failure: 185 | - block_planning 186 | - generate_requirements 187 | 188 | post_planning: 189 | enabled: true 190 | categories: 191 | - ARCHITECTURE_PATTERN 192 | - DESIGN_PATTERN 193 | - PROJECT_STRUCTURE 194 | - FRAMEWORK 195 | actions: 196 | on_validation: 197 | - validate_architecture 198 | - check_dependencies 199 | - verify_patterns 200 | on_failure: 201 | - reject_plan 202 | - generate_violations_report 203 | 204 | post_implementation: 205 | enabled: true 206 | categories: 207 | - all 208 | checks: 209 | - type: automated 210 | patterns: 211 | - file_structure 212 | - dependency_usage 213 | - security_compliance 214 | - type: manual 215 | patterns: 216 | - code_quality 217 | - pattern_adherence 218 | actions: 219 | on_success: 220 | - generate_compliance_report 221 | - update_metrics 222 | on_failure: 223 | - generate_issues_report 224 | - suggest_fixes 225 | # LLM-based validation hooks 226 | hooks: 227 | pre_commit: true # Validate before git commits 228 | pre_deploy: true # Validate before deployments 229 | pull_request: true # Validate on PR creation/update 230 | 231 | # LLM-based preflight validation 232 | preflight: 233 | enabled: true 234 | post_implementation: true 235 | report_path: "docs/validation" 236 | checks: 237 | - type: "architecture" 238 | categories: ["ARCHITECTURE_PATTERN", "PROJECT_STRUCTURE", "DESIGN_PATTERN"] 239 | strictness: ["REQUIRED", "RECOMMENDED"] 240 | - type: "database" 241 | categories: ["DATABASE"] 242 | strictness: ["REQUIRED"] 243 | - type: "api" 244 | categories: ["BACKEND", "FRONTEND"] 245 | strictness: ["REQUIRED"] 246 | actions: 247 | on_failure: 248 | - generate_report 249 | - fail_implementation 250 | on_success: 251 | - generate_report 252 | - mark_validated 253 | 254 | # LLM-based postflight validation 255 | postflight: 256 | enabled: true 257 | report_path: "docs/validation/postflight" 258 | checks: 259 | - type: "performance" 260 | thresholds: 261 | response_time: 200 262 | memory_usage: 512 263 | - type: "security" 264 | categories: ["SECURITY"] 265 | strictness: ["REQUIRED"] 266 | - type: "quality" 267 | metrics: 268 | code_coverage: 80 269 | complexity: 20 270 | actions: 271 | on_failure: 272 | - generate_report 273 | - notify_team 274 | on_success: 275 | - generate_report 276 | - update_metrics 277 | 278 | # Automatic fact creation triggers 279 | triggers: 280 | # Create facts when new dependencies are added 281 | dependencies: 282 | - package.json 283 | - requirements.txt 284 | - pyproject.toml 285 | - Cargo.toml 286 | 287 | # Create facts for new architectural components 288 | architecture: 289 | - "**/*.config.{js,ts}" 290 | - "**/schema.prisma" 291 | - "**/docker-compose.yml" 292 | - "**/Dockerfile" 293 | 294 | # Create facts for API contracts 295 | api: 296 | - "**/api/**/*.{ts,js}" 297 | - "**/routes/**/*.{ts,js}" 298 | - "**/*.proto" 299 | - "**/openapi.{yaml,yml,json}" 300 | 301 | # Create facts for security configurations 302 | security: 303 | - "**/auth/**/*.{ts,js}" 304 | - "**/*.env.example" 305 | - "**/security.config.{js,ts}" 306 | 307 | # Fact type mappings 308 | types: 309 | architecture_decision: 310 | category: "ARCHITECTURE_PATTERN" 311 | strictness: "REQUIRED" 312 | 313 | code_style: 314 | category: "CODE_STYLE" 315 | strictness: "RECOMMENDED" 316 | 317 | naming_convention: 318 | category: "NAMING_CONVENTION" 319 | strictness: "REQUIRED" 320 | 321 | project_structure: 322 | category: "PROJECT_STRUCTURE" 323 | strictness: "REQUIRED" 324 | 325 | design_pattern: 326 | category: "DESIGN_PATTERN" 327 | strictness: "RECOMMENDED" 328 | 329 | testing_pattern: 330 | category: "TESTING_PATTERN" 331 | strictness: "REQUIRED" 332 | 333 | security_rule: 334 | category: "SECURITY" 335 | strictness: "REQUIRED" 336 | 337 | deployment_rule: 338 | category: "DEPLOYMENT" 339 | strictness: "REQUIRED" 340 | 341 | performance_guideline: 342 | category: "OPTIMIZATION" 343 | strictness: "RECOMMENDED" 344 | 345 | error_handling: 346 | category: "ERROR_HANDLING" 347 | strictness: "REQUIRED" 348 | 349 | documentation: 350 | category: "DOCUMENTATION" 351 | strictness: "RECOMMENDED" 352 | 353 | git_workflow: 354 | category: "GIT_WORKFLOW" 355 | strictness: "REQUIRED" 356 | 357 | code_review: 358 | category: "CODE_REVIEW" 359 | strictness: "RECOMMENDED" 360 | 361 | package_management: 362 | category: "PACKAGE_MANAGEMENT" 363 | strictness: "REQUIRED" 364 | 365 | accessibility: 366 | category: "ACCESSIBILITY" 367 | strictness: "REQUIRED" 368 | 369 | internationalization: 370 | category: "INTERNATIONALIZATION" 371 | strictness: "RECOMMENDED" 372 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | facts-server/node_modules/* 2 | facts-ui/.next/* 3 | facts-ui/node_modules/* 4 | .obsidian/app.json 5 | .obsidian/appearance.json 6 | .obsidian/core-plugins.json 7 | .obsidian/workspace.json 8 | hivemind-ui/.next/* 9 | hivemind-ui/node_modules/* 10 | hivemind/node_modules/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Andrew Hopper | andrewhopper.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HiveMind 2 | 3 | A fact management system that provides automatic coding tools with consistent guidance about libraries, patterns, and development standards. 4 | 5 | ## Why HiveMind? 6 | 7 | AI coding tools make the same mistakes again and again. If you've tried building with NextJS 15 they frequently mess up AppRouter; using ShadCN chances are they've used the wrong command- npx install shadcn-ui instead of npx install shadcn. They also don't make good or consistent architecture and design decisions. 8 | 9 | ![Side by side comparison showing HiveMind's impact](docs/side-by-side.png) 10 | 11 | When working with automatic coding tools like Cline in Cursor, developers often find themselves repeatedly providing the same guidance about coding standards, preferred libraries, and architectural patterns. HiveMind solves this by creating a centralized system for managing and enforcing these development guidelines through the Model Context Protocol (MCP). 12 | 13 | This repository is a prototype that demonstrates an MCP server that allows facts to be set, retrieved, searched, and validated against. The goal is to help autonomous coding tools maintain consistency with your project's standards and preferences. 14 | 15 | See an [example validation report](docs/validation/2025-02-24-validation-report.md) that demonstrates how HiveMind validates projects against established facts. 16 | 17 | ## Hivemind in Action 18 | 19 |
20 | 21 |

Collaborating with Claude on Dev Standards

22 |
23 | 24 | 25 | 26 |
27 | 28 |
29 | 30 |

Using dev standards for a new project

31 |
32 | 33 | 34 | 35 |
36 | 37 | 38 | ## Features 39 | - **Facts Management**: Store and retrieve development decisions, guidelines, and standards 40 | - **Automated Validation**: Validate code changes against established criteria 41 | - **Flexible Categories**: Support for multiple development aspects (frontend, backend, security, etc.) 42 | - **Configurable Rules**: Adjustable strictness levels for different types of guidelines 43 | 44 | ## Getting Started 45 | 46 | ### Prerequisites 47 | - Node.js 48 | - SQLite 49 | - MCP SDK 50 | 51 | ### Installation 52 | 1. Clone the repository 53 | 2. Install dependencies: 54 | ```bash 55 | cd facts-server 56 | npm install 57 | ``` 58 | 3. Build the project: 59 | ```bash 60 | npm run build 61 | ``` 62 | 4. Configure the MCP service: 63 | 64 | ``` 65 | "hivemind": { 66 | "command": "node", 67 | "args": [ 68 | "/hivemind/facts-server/build/index.js" 69 | ], 70 | "env": {}, 71 | "disabled": false, 72 | "autoApprove": [] 73 | } 74 | ``` 75 | 76 | ### Configuration 77 | 78 | The `.clinerules` file configures how facts are managed: 79 | 80 | - Automatic saving of development decisions 81 | - Validation of proposals against existing facts 82 | - Strictness levels for different fact types 83 | - Category mappings for various development aspects 84 | 85 | ## Usage 86 | 87 | ### Running the Facts Server 88 | ```bash 89 | cd facts-server 90 | npm start 91 | ``` 92 | 93 | ### Interacting with Facts 94 | 95 | The facts server provides the following tools: 96 | - Create/update facts 97 | - Search facts by type, strictness, and version 98 | - Validate content against fact criteria 99 | - Retrieve existing facts 100 | 101 | ## Technical Details 102 | 103 | ### System Architecture 104 | 105 | ```mermaid 106 | graph TB 107 | subgraph "System Architecture" 108 | MCP[Model Context Protocol] 109 | 110 | subgraph "Facts Management" 111 | FS[Facts Server] 112 | DB[(SQLite DB)] 113 | VS[Vector Search] 114 | end 115 | 116 | subgraph "Validation Layer" 117 | VP[Validation Processor] 118 | RC[Rules Configuration] 119 | end 120 | 121 | IDE[Development Environment] 122 | end 123 | 124 | classDef default fill:#f9f9f9,stroke:#333,stroke-width:2px; 125 | classDef highlight fill:#e1f5fe,stroke:#0288d1,stroke-width:2px; 126 | class MCP,FS highlight 127 | ``` 128 | 129 | ### Request Flow 130 | 131 | ```mermaid 132 | %%{init: {'sequence': {'width': 400, 'actorMargin': 30, 'messageMargin': 15, 'boxMargin': 10, 'fontSize': 11, 'actorFontSize': 11, 'noteFontSize': 11, 'messageFontSize': 11}}}%% 133 | sequenceDiagram 134 | participant IDE as Dev Env 135 | participant MCP as MCP 136 | participant FS as Facts 137 | participant DB as DB 138 | participant VP as Validator 139 | 140 | IDE->>MCP: Dev Request 141 | MCP->>FS: Query Facts 142 | FS->>DB: Fetch Rules 143 | DB-->>FS: Rules Data 144 | FS->>VP: Validate 145 | VP-->>FS: Result 146 | FS-->>MCP: Facts & Valid. 147 | MCP-->>IDE: Standards 148 | ``` 149 | 150 | The architecture diagram shows the core components of HiveMind, centered around the Model Context Protocol (MCP) which integrates with your development environment. The Facts Server manages development guidelines and standards using a SQLite database with vector search capabilities. 151 | 152 | ### Project Structure 153 | 154 | ``` 155 | HiveMind/ 156 | ├── facts-server/ # MCP server for facts management 157 | │ ├── src/ # TypeScript source files 158 | │ ├── prisma/ # Database schema and migrations 159 | │ └── build/ # Compiled JavaScript output 160 | └── .clinerules # Configuration for facts integration 161 | ``` 162 | 163 | ### Technology Stack 164 | - TypeScript 165 | - Prisma (SQLite) 166 | - MCP SDK 167 | - Vector extensions for similarity search 168 | 169 | ### Development Scripts 170 | - `npm run build` - Build the TypeScript code 171 | - `npm run dev` - Run with debugging enabled 172 | - `npm start` - Start the facts server 173 | 174 | ## Contributing 175 | 176 | Contributions are welcome! Feel free to submit issues, feature requests, or pull requests. 177 | 178 | ## Contact 179 | 180 | You can reach me on: 181 | - https://www.linkedin.com/in/andrewhopper 182 | - https://x.com/andrewhopper 183 | - https://andrewhopper.com 184 | 185 | ## License 186 | 187 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 188 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { DataTable } from "@/components/facts/data-table" 2 | import { columns } from "@/components/facts/columns" 3 | import { getAllFacts } from "@/lib/api/facts" // This should now work 4 | import { Button } from "@/components/ui/button" 5 | import Link from "next/link" 6 | 7 | // ... rest of your page code -------------------------------------------------------------------------------- /components/facts/columns.ts: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ColumnDef } from "@/components/ui/table" 4 | 5 | export const columns: ColumnDef[] = [ 6 | { 7 | accessorKey: "title", 8 | header: "Title", 9 | }, 10 | { 11 | accessorKey: "description", 12 | header: "Description", 13 | }, 14 | { 15 | accessorKey: "createdAt", 16 | header: "Created At", 17 | } 18 | ] -------------------------------------------------------------------------------- /components/facts/data-table.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React from "react" 4 | import { 5 | ColumnDef, 6 | flexRender, 7 | getCoreRowModel, 8 | useReactTable, 9 | } from "@/components/ui/table" 10 | import { Button } from "@/components/ui/button" 11 | import { useState } from "react" 12 | 13 | interface DataTableProps { 14 | columns: ColumnDef[] 15 | data: TData[] 16 | } 17 | 18 | export function DataTable({ 19 | columns, 20 | data 21 | }: DataTableProps) { 22 | const table = useReactTable({ 23 | data, 24 | columns, 25 | getCoreRowModel: getCoreRowModel(), 26 | }) 27 | 28 | return ( 29 |
30 | 31 | 32 | {table.getHeaderGroups().map((headerGroup) => ( 33 | 34 | {headerGroup.headers.map((header) => ( 35 | 43 | ))} 44 | 45 | ))} 46 | 47 | 48 | {table.getRowModel().rows.map((row) => ( 49 | 50 | {row.getVisibleCells().map((cell) => ( 51 | 57 | ))} 58 | 59 | ))} 60 | 61 |
36 | {header.isPlaceholder 37 | ? null 38 | : flexRender( 39 | header.column.columnDef.header, 40 | header.getContext() 41 | )} 42 |
52 | {flexRender( 53 | cell.column.columnDef.cell, 54 | cell.getContext() 55 | )} 56 |
62 |
63 | ) 64 | } -------------------------------------------------------------------------------- /docs/side-by-side.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewhopper/hivemind/b82fb50a74b5eadef26a19cc1f69a178772b29d3/docs/side-by-side.png -------------------------------------------------------------------------------- /docs/validation/2025-02-24-validation-report.md: -------------------------------------------------------------------------------- 1 | # Validation Report 2 | 3 | ## Summary 4 | Validation of current architecture against established facts reveals some conflicts that need to be addressed. 5 | 6 | ## Required Facts 7 | - ✅ Next.js Frontend (arch_nextjs_frontend) 8 | - Properly implemented with Next.js 15 and App Router 9 | - Follows required architectural patterns 10 | 11 | - ✅ Prisma ORM (arch_prisma_orm) 12 | - Schema and migrations properly configured 13 | - Type-safe database operations implemented 14 | 15 | - ❌ Database Choice (use-postgres) 16 | - **CONFLICT**: Current implementation uses SQLite, but fact requires PostgreSQL 17 | - This is a REQUIRED fact and needs immediate attention 18 | 19 | ## Recommended Facts 20 | - ✅ Documentation (maintain-readme) 21 | - README.md maintained with required sections 22 | - Documentation follows markdown format 23 | 24 | ## Issues 25 | 1. Database Implementation Conflict 26 | - Current: SQLite with Prisma 27 | - Required: PostgreSQL (per use-postgres fact) 28 | - Impact: High (REQUIRED fact violation) 29 | 30 | ## Recommendations 31 | 1. Database Migration 32 | - Plan migration from SQLite to PostgreSQL 33 | - Update Prisma schema to use PostgreSQL provider 34 | - Ensure all database operations are compatible 35 | - Create migration scripts for existing data 36 | 37 | 2. Alternative Approach 38 | - If SQLite is necessary for specific use cases: 39 | - Update use-postgres fact to allow SQLite for development/embedded cases 40 | - Add conditions specifying when each database type should be used 41 | - Document rationale for database choice flexibility 42 | -------------------------------------------------------------------------------- /facts-server/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewhopper/hivemind/b82fb50a74b5eadef26a19cc1f69a178772b29d3/facts-server/.DS_Store -------------------------------------------------------------------------------- /facts-server/build/index.d.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | export {}; 3 | -------------------------------------------------------------------------------- /facts-server/build/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { Server } from '@modelcontextprotocol/sdk/server/index.js'; 3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 4 | import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from '@modelcontextprotocol/sdk/types.js'; 5 | import { PrismaStorageProvider } from './storage.js'; 6 | import { validateCriteria } from './validation.js'; 7 | import { StrictnessLevel, FactCategory } from './types.js'; 8 | class FactsServer { 9 | constructor() { 10 | this.storage = new PrismaStorageProvider(); 11 | this.server = new Server({ 12 | name: 'facts-server', 13 | version: '0.1.0', 14 | }, { 15 | capabilities: { 16 | tools: { 17 | get_all_facts: { 18 | description: 'Get all facts in the system', 19 | inputSchema: { 20 | type: 'object', 21 | properties: {} 22 | } 23 | }, 24 | get_fact: { 25 | description: 'Get a fact by ID', 26 | inputSchema: { 27 | type: 'object', 28 | properties: { 29 | id: { 30 | type: 'string', 31 | description: 'The ID of the fact to retrieve' 32 | } 33 | }, 34 | required: ['id'] 35 | } 36 | }, 37 | search_facts: { 38 | description: 'Search facts by type, strictness, and version', 39 | inputSchema: { 40 | type: 'object', 41 | properties: { 42 | type: { 43 | type: 'string', 44 | description: 'Filter by fact type' 45 | }, 46 | strictness: { 47 | type: 'string', 48 | enum: Object.values(StrictnessLevel), 49 | description: 'Filter by strictness level' 50 | }, 51 | version: { 52 | type: 'string', 53 | description: 'Filter by version compatibility' 54 | } 55 | } 56 | } 57 | }, 58 | set_fact: { 59 | description: 'Create or update a fact', 60 | inputSchema: { 61 | type: 'object', 62 | properties: { 63 | id: { type: 'string' }, 64 | content: { type: 'string' }, 65 | strictness: { 66 | type: 'string', 67 | enum: Object.values(StrictnessLevel) 68 | }, 69 | type: { type: 'string' }, 70 | category: { 71 | type: 'string', 72 | enum: Object.values(FactCategory), 73 | description: 'The category this fact belongs to' 74 | }, 75 | minVersion: { type: 'string' }, 76 | maxVersion: { type: 'string' }, 77 | conditions: { 78 | type: 'array', 79 | items: { 80 | type: 'object', 81 | properties: { 82 | factId: { type: 'string' }, 83 | type: { 84 | type: 'string', 85 | enum: ['REQUIRES', 'CONFLICTS_WITH'] 86 | } 87 | }, 88 | required: ['factId', 'type'] 89 | } 90 | }, 91 | acceptanceCriteria: { 92 | type: 'array', 93 | items: { 94 | type: 'object', 95 | properties: { 96 | id: { type: 'string' }, 97 | description: { type: 'string' }, 98 | validationType: { 99 | type: 'string', 100 | enum: ['MANUAL', 'AUTOMATED'] 101 | }, 102 | validationScript: { type: 'string' } 103 | }, 104 | required: ['id', 'description', 'validationType'] 105 | } 106 | } 107 | }, 108 | required: ['id', 'content', 'strictness', 'type', 'category', 'minVersion', 'maxVersion'] 109 | } 110 | }, 111 | validate_criteria: { 112 | description: 'Validate content against fact acceptance criteria', 113 | inputSchema: { 114 | type: 'object', 115 | properties: { 116 | factId: { 117 | type: 'string', 118 | description: 'ID of the fact to validate against' 119 | }, 120 | content: { 121 | type: 'string', 122 | description: 'Content to validate' 123 | } 124 | }, 125 | required: ['factId', 'content'] 126 | } 127 | } 128 | } 129 | } 130 | }); 131 | this.setupHandlers(); 132 | } 133 | setupHandlers() { 134 | // Handler for listing available tools 135 | this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ 136 | tools: [ 137 | { 138 | name: 'get_all_facts', 139 | description: 'Get all facts in the system', 140 | inputSchema: { 141 | type: 'object', 142 | properties: {} 143 | } 144 | }, 145 | { 146 | name: 'get_fact', 147 | description: 'Get a fact by ID', 148 | inputSchema: { 149 | type: 'object', 150 | properties: { 151 | id: { 152 | type: 'string', 153 | description: 'The ID of the fact to retrieve' 154 | } 155 | }, 156 | required: ['id'] 157 | } 158 | }, 159 | { 160 | name: 'search_facts', 161 | description: 'Search facts by type, strictness, and version', 162 | inputSchema: { 163 | type: 'object', 164 | properties: { 165 | type: { 166 | type: 'string', 167 | description: 'Filter by fact type' 168 | }, 169 | strictness: { 170 | type: 'string', 171 | enum: Object.values(StrictnessLevel), 172 | description: 'Filter by strictness level' 173 | }, 174 | version: { 175 | type: 'string', 176 | description: 'Filter by version compatibility' 177 | } 178 | } 179 | } 180 | }, 181 | { 182 | name: 'set_fact', 183 | description: 'Create or update a fact', 184 | inputSchema: { 185 | type: 'object', 186 | properties: { 187 | id: { type: 'string' }, 188 | content: { type: 'string' }, 189 | strictness: { 190 | type: 'string', 191 | enum: Object.values(StrictnessLevel) 192 | }, 193 | type: { type: 'string' }, 194 | category: { 195 | type: 'string', 196 | enum: Object.values(FactCategory), 197 | description: 'The category this fact belongs to' 198 | }, 199 | minVersion: { type: 'string' }, 200 | maxVersion: { type: 'string' }, 201 | conditions: { 202 | type: 'array', 203 | items: { 204 | type: 'object', 205 | properties: { 206 | factId: { type: 'string' }, 207 | type: { 208 | type: 'string', 209 | enum: ['REQUIRES', 'CONFLICTS_WITH'] 210 | } 211 | }, 212 | required: ['factId', 'type'] 213 | } 214 | }, 215 | acceptanceCriteria: { 216 | type: 'array', 217 | items: { 218 | type: 'object', 219 | properties: { 220 | id: { type: 'string' }, 221 | description: { type: 'string' }, 222 | validationType: { 223 | type: 'string', 224 | enum: ['MANUAL', 'AUTOMATED'] 225 | }, 226 | validationScript: { type: 'string' } 227 | }, 228 | required: ['id', 'description', 'validationType'] 229 | } 230 | } 231 | }, 232 | required: ['id', 'content', 'strictness', 'type', 'category', 'minVersion', 'maxVersion'] 233 | } 234 | }, 235 | { 236 | name: 'validate_criteria', 237 | description: 'Validate content against fact acceptance criteria', 238 | inputSchema: { 239 | type: 'object', 240 | properties: { 241 | factId: { 242 | type: 'string', 243 | description: 'ID of the fact to validate against' 244 | }, 245 | content: { 246 | type: 'string', 247 | description: 'Content to validate' 248 | } 249 | }, 250 | required: ['factId', 'content'] 251 | } 252 | } 253 | ] 254 | })); 255 | // Handler for executing tools 256 | this.server.setRequestHandler(CallToolRequestSchema, async (request) => { 257 | switch (request.params.name) { 258 | case 'get_all_facts': { 259 | const facts = await this.storage.searchFacts({}); 260 | return { 261 | content: [{ type: 'text', text: JSON.stringify(facts, null, 2) }] 262 | }; 263 | } 264 | case 'get_fact': { 265 | const args = request.params.arguments; 266 | const fact = await this.storage.getFact(args.id); 267 | if (!fact) { 268 | return { 269 | content: [{ type: 'text', text: `Fact not found: ${args.id}` }], 270 | isError: true 271 | }; 272 | } 273 | return { 274 | content: [{ type: 'text', text: JSON.stringify(fact, null, 2) }] 275 | }; 276 | } 277 | case 'search_facts': { 278 | const args = request.params.arguments; 279 | const facts = await this.storage.searchFacts(args); 280 | return { 281 | content: [{ type: 'text', text: JSON.stringify(facts, null, 2) }] 282 | }; 283 | } 284 | case 'set_fact': { 285 | const args = request.params.arguments; 286 | try { 287 | await this.storage.setFact(args.id, args.content, args.strictness, args.type, args.category, args.minVersion, args.maxVersion, args.conditions || [], args.acceptanceCriteria || []); 288 | return { 289 | content: [{ type: 'text', text: `Fact ${args.id} saved successfully` }] 290 | }; 291 | } 292 | catch (error) { 293 | return { 294 | content: [{ type: 'text', text: `Error saving fact: ${error instanceof Error ? error.message : 'Unknown error'}` }], 295 | isError: true 296 | }; 297 | } 298 | } 299 | case 'validate_criteria': { 300 | const args = request.params.arguments; 301 | const fact = await this.storage.getFact(args.factId); 302 | if (!fact) { 303 | return { 304 | content: [{ type: 'text', text: `Fact not found: ${args.factId}` }], 305 | isError: true 306 | }; 307 | } 308 | try { 309 | const results = await validateCriteria(args.content, fact.acceptanceCriteria); 310 | return { 311 | content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] 312 | }; 313 | } 314 | catch (error) { 315 | return { 316 | content: [{ type: 'text', text: `Validation error: ${error instanceof Error ? error.message : 'Unknown error'}` }], 317 | isError: true 318 | }; 319 | } 320 | } 321 | } 322 | throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); 323 | }); 324 | } 325 | async run() { 326 | const transport = new StdioServerTransport(); 327 | await this.server.connect(transport); 328 | console.error('Facts MCP server running on stdio'); 329 | // Handle cleanup on shutdown 330 | process.on('SIGINT', async () => { 331 | await this.storage.close(); 332 | process.exit(0); 333 | }); 334 | process.on('SIGTERM', async () => { 335 | await this.storage.close(); 336 | process.exit(0); 337 | }); 338 | } 339 | } 340 | const server = new FactsServer(); 341 | server.run().catch(console.error); 342 | -------------------------------------------------------------------------------- /facts-server/build/sdk/server.d.ts: -------------------------------------------------------------------------------- 1 | import { ServerRequest, ServerResponse, ServerInfo, ServerCapabilities, ServerTransport } from './types.js'; 2 | export declare class Server { 3 | private info; 4 | private capabilities; 5 | private transport; 6 | private handlers; 7 | constructor(info: ServerInfo, capabilities: { 8 | capabilities: ServerCapabilities; 9 | }); 10 | connect(transport: ServerTransport): Promise; 11 | close(): Promise; 12 | setHandler(method: string, handler: (request: ServerRequest) => Promise): void; 13 | } 14 | -------------------------------------------------------------------------------- /facts-server/build/sdk/server.js: -------------------------------------------------------------------------------- 1 | export class Server { 2 | constructor(info, capabilities) { 3 | this.info = info; 4 | this.capabilities = capabilities; 5 | this.transport = null; 6 | this.handlers = new Map(); 7 | } 8 | async connect(transport) { 9 | this.transport = transport; 10 | transport.onRequest(async (request) => { 11 | const handler = this.handlers.get(request.method); 12 | if (!handler) { 13 | return { 14 | content: [{ type: 'text', text: `Unknown method: ${request.method}` }], 15 | isError: true 16 | }; 17 | } 18 | return handler(request); 19 | }); 20 | await transport.connect(); 21 | } 22 | async close() { 23 | if (this.transport) { 24 | await this.transport.disconnect(); 25 | this.transport = null; 26 | } 27 | } 28 | setHandler(method, handler) { 29 | this.handlers.set(method, handler); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /facts-server/build/sdk/stdio.d.ts: -------------------------------------------------------------------------------- 1 | import { ServerRequest, ServerResponse, ServerTransport } from './types.js'; 2 | export declare class StdioServerTransport implements ServerTransport { 3 | private requestHandler; 4 | private readline; 5 | connect(): Promise; 6 | disconnect(): Promise; 7 | onRequest(handler: (request: ServerRequest) => Promise): void; 8 | } 9 | -------------------------------------------------------------------------------- /facts-server/build/sdk/stdio.js: -------------------------------------------------------------------------------- 1 | import { createInterface } from 'readline'; 2 | export class StdioServerTransport { 3 | constructor() { 4 | this.requestHandler = null; 5 | this.readline = createInterface({ 6 | input: process.stdin, 7 | output: process.stdout 8 | }); 9 | } 10 | async connect() { 11 | this.readline.on('line', async (line) => { 12 | try { 13 | const request = JSON.parse(line); 14 | if (this.requestHandler) { 15 | const response = await this.requestHandler(request); 16 | console.log(JSON.stringify(response)); 17 | } 18 | } 19 | catch (error) { 20 | console.error('Error processing request:', error); 21 | console.log(JSON.stringify({ 22 | content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}` }], 23 | isError: true 24 | })); 25 | } 26 | }); 27 | } 28 | async disconnect() { 29 | this.readline.close(); 30 | } 31 | onRequest(handler) { 32 | this.requestHandler = handler; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /facts-server/build/sdk/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface ServerRequest { 2 | method: string; 3 | params?: Record; 4 | } 5 | export interface ServerResponse { 6 | content: Array<{ 7 | type: string; 8 | text: string; 9 | }>; 10 | isError?: boolean; 11 | } 12 | export interface ServerCapabilities { 13 | tools?: Record; 18 | required?: string[]; 19 | }; 20 | }>; 21 | } 22 | export interface ServerInfo { 23 | name: string; 24 | version: string; 25 | } 26 | export interface ServerTransport { 27 | connect(): Promise; 28 | disconnect(): Promise; 29 | onRequest(handler: (request: ServerRequest) => Promise): void; 30 | } 31 | -------------------------------------------------------------------------------- /facts-server/build/sdk/types.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /facts-server/build/storage.d.ts: -------------------------------------------------------------------------------- 1 | import { StorageProvider, StorageSearchResult, StrictnessLevel, FactCategory, Condition, AcceptanceCriterion } from './types.js'; 2 | export declare class PrismaStorageProvider implements StorageProvider { 3 | private prisma; 4 | constructor(); 5 | setFact(id: string, content: string, strictness: StrictnessLevel, type: string, category: FactCategory, minVersion: string, maxVersion: string, conditions: Condition[], acceptanceCriteria: AcceptanceCriterion[], contentEmbedding?: string): Promise; 6 | getFact(id: string): Promise; 7 | searchFacts(options: { 8 | type?: string; 9 | category?: FactCategory; 10 | strictness?: StrictnessLevel; 11 | version?: string; 12 | embedding?: string; 13 | similarityThreshold?: number; 14 | }): Promise; 15 | deleteFact(id: string): Promise; 16 | close(): Promise; 17 | } 18 | -------------------------------------------------------------------------------- /facts-server/build/storage.js: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | export class PrismaStorageProvider { 3 | constructor() { 4 | this.prisma = new PrismaClient(); 5 | } 6 | async setFact(id, content, strictness, type, category, minVersion, maxVersion, conditions, acceptanceCriteria, contentEmbedding) { 7 | const data = { 8 | id, 9 | content, 10 | strictness, 11 | type, 12 | category, 13 | minVersion, 14 | maxVersion, 15 | content_embedding: contentEmbedding, 16 | conditions: { 17 | create: conditions.map(condition => ({ 18 | type: condition.type, 19 | targetId: condition.factId 20 | })) 21 | }, 22 | acceptanceCriteria: { 23 | create: acceptanceCriteria.map(criterion => ({ 24 | id: criterion.id, 25 | description: criterion.description, 26 | validationType: criterion.validationType, 27 | validationScript: criterion.validationScript 28 | })) 29 | } 30 | }; 31 | await this.prisma.fact.upsert({ 32 | where: { id }, 33 | create: data, 34 | update: { 35 | ...data, 36 | conditions: { 37 | deleteMany: {}, 38 | create: conditions.map(condition => ({ 39 | type: condition.type, 40 | targetId: condition.factId 41 | })) 42 | }, 43 | acceptanceCriteria: { 44 | deleteMany: {}, 45 | create: acceptanceCriteria.map(criterion => ({ 46 | id: criterion.id, 47 | description: criterion.description, 48 | validationType: criterion.validationType, 49 | validationScript: criterion.validationScript 50 | })) 51 | } 52 | } 53 | }); 54 | } 55 | async getFact(id) { 56 | const fact = await this.prisma.fact.findUnique({ 57 | where: { id }, 58 | include: { 59 | conditions: true, 60 | acceptanceCriteria: true 61 | } 62 | }); 63 | if (!fact) { 64 | return null; 65 | } 66 | return { 67 | id: fact.id, 68 | content: fact.content, 69 | strictness: fact.strictness, 70 | type: fact.type, 71 | category: fact.category, 72 | minVersion: fact.minVersion, 73 | maxVersion: fact.maxVersion, 74 | conditions: fact.conditions.map(c => ({ 75 | factId: c.targetId, 76 | type: c.type 77 | })), 78 | acceptanceCriteria: fact.acceptanceCriteria.map(ac => ({ 79 | id: ac.id, 80 | description: ac.description, 81 | validationType: ac.validationType, 82 | validationScript: ac.validationScript || undefined 83 | })), 84 | createdAt: fact.createdAt.toISOString(), 85 | updatedAt: fact.updatedAt.toISOString(), 86 | applicable: fact.applicable 87 | }; 88 | } 89 | async searchFacts(options) { 90 | let facts; 91 | if (options.embedding) { 92 | // Use raw query for vector similarity search 93 | const threshold = options.similarityThreshold || 0.8; 94 | const whereConditions = []; 95 | const params = [options.embedding, threshold]; 96 | if (options.type) { 97 | whereConditions.push('f.type = ?'); 98 | params.push(options.type); 99 | } 100 | if (options.category) { 101 | whereConditions.push('f.category = ?'); 102 | params.push(options.category); 103 | } 104 | if (options.strictness) { 105 | whereConditions.push('f.strictness = ?'); 106 | params.push(options.strictness); 107 | } 108 | if (options.version) { 109 | whereConditions.push('f.minVersion <= ? AND f.maxVersion >= ?'); 110 | params.push(options.version, options.version); 111 | } 112 | const whereClause = whereConditions.length 113 | ? 'AND ' + whereConditions.join(' AND ') 114 | : ''; 115 | const rawResults = await this.prisma.$queryRawUnsafe(`SELECT f.*, 116 | vector_similarity(f.content_embedding, ?) as similarity 117 | FROM "Fact" f 118 | WHERE vector_similarity(f.content_embedding, ?) > ? 119 | ${whereClause} 120 | ORDER BY similarity DESC`, options.embedding, options.embedding, threshold, ...params); 121 | // Fetch full fact data including relations 122 | const factPromises = rawResults.map(result => this.prisma.fact.findUnique({ 123 | where: { id: result.id }, 124 | include: { 125 | conditions: true, 126 | acceptanceCriteria: true 127 | } 128 | })); 129 | facts = (await Promise.all(factPromises)).filter((fact) => fact !== null); 130 | } 131 | else { 132 | const where = {}; 133 | if (options.type) { 134 | where.type = options.type; 135 | } 136 | if (options.category) { 137 | where.category = options.category; 138 | } 139 | if (options.strictness) { 140 | where.strictness = options.strictness; 141 | } 142 | if (options.version) { 143 | where.AND = [ 144 | { minVersion: { lte: options.version } }, 145 | { maxVersion: { gte: options.version } } 146 | ]; 147 | } 148 | facts = await this.prisma.fact.findMany({ 149 | where, 150 | include: { 151 | conditions: true, 152 | acceptanceCriteria: true 153 | } 154 | }); 155 | } 156 | return facts.map(fact => ({ 157 | id: fact.id, 158 | content: fact.content, 159 | strictness: fact.strictness, 160 | type: fact.type, 161 | category: fact.category, 162 | minVersion: fact.minVersion, 163 | maxVersion: fact.maxVersion, 164 | conditions: fact.conditions.map(c => ({ 165 | factId: c.targetId, 166 | type: c.type 167 | })), 168 | acceptanceCriteria: fact.acceptanceCriteria.map(ac => ({ 169 | id: ac.id, 170 | description: ac.description, 171 | validationType: ac.validationType, 172 | validationScript: ac.validationScript || undefined 173 | })), 174 | createdAt: fact.createdAt.toISOString(), 175 | updatedAt: fact.updatedAt.toISOString(), 176 | applicable: fact.applicable 177 | })); 178 | } 179 | async deleteFact(id) { 180 | try { 181 | await this.prisma.fact.delete({ 182 | where: { id } 183 | }); 184 | return true; 185 | } 186 | catch (error) { 187 | return false; 188 | } 189 | } 190 | async close() { 191 | await this.prisma.$disconnect(); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /facts-server/build/types.d.ts: -------------------------------------------------------------------------------- 1 | export declare enum FactCategory { 2 | FRONTEND = "FRONTEND", 3 | BACKEND = "BACKEND", 4 | DATABASE = "DATABASE", 5 | FULL_STACK = "FULL_STACK", 6 | DESIGN_PATTERN = "DESIGN_PATTERN", 7 | ARCHITECTURE_PATTERN = "ARCHITECTURE_PATTERN", 8 | TESTING_PATTERN = "TESTING_PATTERN", 9 | NAMING_CONVENTION = "NAMING_CONVENTION", 10 | PROJECT_STRUCTURE = "PROJECT_STRUCTURE", 11 | CODE_STYLE = "CODE_STYLE", 12 | DEPLOYMENT = "DEPLOYMENT", 13 | CI_CD = "CI_CD", 14 | MONITORING = "MONITORING", 15 | SECURITY = "SECURITY", 16 | GIT_WORKFLOW = "GIT_WORKFLOW", 17 | CODE_REVIEW = "CODE_REVIEW", 18 | DOCUMENTATION = "DOCUMENTATION", 19 | PACKAGE_MANAGEMENT = "PACKAGE_MANAGEMENT", 20 | VERSIONING = "VERSIONING", 21 | OPTIMIZATION = "OPTIMIZATION", 22 | CACHING = "CACHING", 23 | ACCESSIBILITY = "ACCESSIBILITY", 24 | INTERNATIONALIZATION = "INTERNATIONALIZATION", 25 | ERROR_HANDLING = "ERROR_HANDLING" 26 | } 27 | export declare enum StrictnessLevel { 28 | REQUIRED = "REQUIRED", 29 | RECOMMENDED = "RECOMMENDED", 30 | OPTIONAL = "OPTIONAL" 31 | } 32 | export interface Condition { 33 | factId: string; 34 | type: 'REQUIRES' | 'CONFLICTS_WITH'; 35 | } 36 | export type ValidationType = 'MANUAL' | 'AUTOMATED' | 'URL_CHECK'; 37 | export interface AcceptanceCriterion { 38 | id: string; 39 | description: string; 40 | validationType: ValidationType; 41 | validationScript?: string; 42 | } 43 | export interface FactData { 44 | content: string; 45 | strictness: StrictnessLevel; 46 | type: string; 47 | category: FactCategory; 48 | minVersion: string; 49 | maxVersion: string; 50 | conditions: Condition[]; 51 | acceptanceCriteria: AcceptanceCriterion[]; 52 | createdAt: string; 53 | updatedAt: string; 54 | } 55 | export interface StorageSearchResult extends FactData { 56 | id: string; 57 | applicable: boolean; 58 | } 59 | export interface ValidationResult { 60 | criterionId: string; 61 | passed: boolean; 62 | message?: string; 63 | } 64 | export interface StorageProvider { 65 | setFact(id: string, content: string, strictness: StrictnessLevel, type: string, category: FactCategory, minVersion: string, maxVersion: string, conditions: Condition[], acceptanceCriteria: AcceptanceCriterion[], contentEmbedding?: string): Promise; 66 | getFact(id: string): Promise; 67 | searchFacts(options: { 68 | type?: string; 69 | strictness?: StrictnessLevel; 70 | version?: string; 71 | embedding?: string; 72 | similarityThreshold?: number; 73 | }): Promise; 74 | deleteFact(id: string): Promise; 75 | } 76 | -------------------------------------------------------------------------------- /facts-server/build/types.js: -------------------------------------------------------------------------------- 1 | export var FactCategory; 2 | (function (FactCategory) { 3 | // Architecture Layers 4 | FactCategory["FRONTEND"] = "FRONTEND"; 5 | FactCategory["BACKEND"] = "BACKEND"; 6 | FactCategory["DATABASE"] = "DATABASE"; 7 | FactCategory["FULL_STACK"] = "FULL_STACK"; 8 | // Development Patterns 9 | FactCategory["DESIGN_PATTERN"] = "DESIGN_PATTERN"; 10 | FactCategory["ARCHITECTURE_PATTERN"] = "ARCHITECTURE_PATTERN"; 11 | FactCategory["TESTING_PATTERN"] = "TESTING_PATTERN"; 12 | // Code Organization 13 | FactCategory["NAMING_CONVENTION"] = "NAMING_CONVENTION"; 14 | FactCategory["PROJECT_STRUCTURE"] = "PROJECT_STRUCTURE"; 15 | FactCategory["CODE_STYLE"] = "CODE_STYLE"; 16 | // Operations 17 | FactCategory["DEPLOYMENT"] = "DEPLOYMENT"; 18 | FactCategory["CI_CD"] = "CI_CD"; 19 | FactCategory["MONITORING"] = "MONITORING"; 20 | FactCategory["SECURITY"] = "SECURITY"; 21 | // Development Process 22 | FactCategory["GIT_WORKFLOW"] = "GIT_WORKFLOW"; 23 | FactCategory["CODE_REVIEW"] = "CODE_REVIEW"; 24 | FactCategory["DOCUMENTATION"] = "DOCUMENTATION"; 25 | // Dependencies 26 | FactCategory["PACKAGE_MANAGEMENT"] = "PACKAGE_MANAGEMENT"; 27 | FactCategory["VERSIONING"] = "VERSIONING"; 28 | // Performance 29 | FactCategory["OPTIMIZATION"] = "OPTIMIZATION"; 30 | FactCategory["CACHING"] = "CACHING"; 31 | // Cross-cutting 32 | FactCategory["ACCESSIBILITY"] = "ACCESSIBILITY"; 33 | FactCategory["INTERNATIONALIZATION"] = "INTERNATIONALIZATION"; 34 | FactCategory["ERROR_HANDLING"] = "ERROR_HANDLING"; 35 | })(FactCategory || (FactCategory = {})); 36 | export var StrictnessLevel; 37 | (function (StrictnessLevel) { 38 | StrictnessLevel["REQUIRED"] = "REQUIRED"; 39 | StrictnessLevel["RECOMMENDED"] = "RECOMMENDED"; 40 | StrictnessLevel["OPTIONAL"] = "OPTIONAL"; 41 | })(StrictnessLevel || (StrictnessLevel = {})); 42 | -------------------------------------------------------------------------------- /facts-server/build/validation.d.ts: -------------------------------------------------------------------------------- 1 | import { AcceptanceCriterion, ValidationResult } from './types.js'; 2 | export declare class ValidationError extends Error { 3 | constructor(message: string); 4 | } 5 | export declare function validateCriteria(content: string, criteria: AcceptanceCriterion[]): Promise; 6 | export declare function validateFactConditions(factConditions: Map, conditions: { 7 | factId: string; 8 | type: 'REQUIRES' | 'CONFLICTS_WITH'; 9 | }[]): boolean; 10 | -------------------------------------------------------------------------------- /facts-server/build/validation.js: -------------------------------------------------------------------------------- 1 | export class ValidationError extends Error { 2 | constructor(message) { 3 | super(message); 4 | this.name = 'ValidationError'; 5 | } 6 | } 7 | export async function validateCriteria(content, criteria) { 8 | const results = []; 9 | for (const criterion of criteria) { 10 | if (criterion.validationType === 'MANUAL') { 11 | results.push({ 12 | criterionId: criterion.id, 13 | passed: false, 14 | message: 'Manual validation required' 15 | }); 16 | continue; 17 | } 18 | if (!criterion.validationScript) { 19 | throw new ValidationError(`Automated criterion ${criterion.id} has no validation script`); 20 | } 21 | try { 22 | // Create a function from the validation script 23 | const validationFn = new Function('content', criterion.validationScript + '\nreturn ' + criterion.validationScript.match(/function\s+(\w+)/)?.[1] + '(content);'); 24 | const passed = await Promise.resolve(validationFn(content)); 25 | results.push({ 26 | criterionId: criterion.id, 27 | passed, 28 | message: passed ? 'Validation passed' : 'Validation failed' 29 | }); 30 | } 31 | catch (error) { 32 | results.push({ 33 | criterionId: criterion.id, 34 | passed: false, 35 | message: `Validation error: ${error instanceof Error ? error.message : 'Unknown error'}` 36 | }); 37 | } 38 | } 39 | return results; 40 | } 41 | export function validateFactConditions(factConditions, conditions) { 42 | for (const condition of conditions) { 43 | const factExists = factConditions.get(condition.factId) ?? false; 44 | if (condition.type === 'REQUIRES' && !factExists) { 45 | return false; 46 | } 47 | if (condition.type === 'CONFLICTS_WITH' && factExists) { 48 | return false; 49 | } 50 | } 51 | return true; 52 | } 53 | -------------------------------------------------------------------------------- /facts-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "facts-server", 3 | "version": "1.0.0", 4 | "description": "MCP server for managing documentation facts and acceptance criteria", 5 | "type": "module", 6 | "main": "build/index.js", 7 | "scripts": { 8 | "build": "tsc && chmod +x build/index.js", 9 | "dev": "node --inspect -r ts-node/register --loader ts-node/esm src/index.ts", 10 | "start": "node build/index.js" 11 | }, 12 | "keywords": [ 13 | "mcp", 14 | "documentation", 15 | "validation" 16 | ], 17 | "author": "Andrew Hopper (andrewhopper.com)", 18 | "license": "MIT", 19 | "dependencies": { 20 | "@modelcontextprotocol/sdk": "^1.5.0", 21 | "@prisma/client": "^6.4.1", 22 | "sqlite-vec": "^0.1.7-alpha.2", 23 | "sqlite3": "^5.1.7" 24 | }, 25 | "devDependencies": { 26 | "prisma": "^6.4.1", 27 | "ts-node": "^10.9.2", 28 | "typescript": "^5.3.3" 29 | } 30 | } -------------------------------------------------------------------------------- /facts-server/prisma/facts.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewhopper/hivemind/b82fb50a74b5eadef26a19cc1f69a178772b29d3/facts-server/prisma/facts.db -------------------------------------------------------------------------------- /facts-server/prisma/migrations/20250223115817_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Fact" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "content" TEXT NOT NULL, 5 | "strictness" TEXT NOT NULL, 6 | "type" TEXT NOT NULL, 7 | "minVersion" TEXT NOT NULL, 8 | "maxVersion" TEXT NOT NULL, 9 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 10 | "updatedAt" DATETIME NOT NULL, 11 | "applicable" BOOLEAN NOT NULL DEFAULT true, 12 | "content_embedding" TEXT 13 | ); 14 | 15 | -- CreateTable 16 | CREATE TABLE "Condition" ( 17 | "id" TEXT NOT NULL PRIMARY KEY, 18 | "factId" TEXT NOT NULL, 19 | "type" TEXT NOT NULL, 20 | "targetId" TEXT NOT NULL, 21 | CONSTRAINT "Condition_factId_fkey" FOREIGN KEY ("factId") REFERENCES "Fact" ("id") ON DELETE RESTRICT ON UPDATE CASCADE 22 | ); 23 | 24 | -- CreateTable 25 | CREATE TABLE "AcceptanceCriterion" ( 26 | "id" TEXT NOT NULL PRIMARY KEY, 27 | "factId" TEXT NOT NULL, 28 | "description" TEXT NOT NULL, 29 | "validationType" TEXT NOT NULL, 30 | "validationScript" TEXT, 31 | CONSTRAINT "AcceptanceCriterion_factId_fkey" FOREIGN KEY ("factId") REFERENCES "Fact" ("id") ON DELETE RESTRICT ON UPDATE CASCADE 32 | ); 33 | -------------------------------------------------------------------------------- /facts-server/prisma/migrations/20250223171046_add_fact_category/migration.sql: -------------------------------------------------------------------------------- 1 | -- Add category column as nullable first 2 | PRAGMA foreign_keys=OFF; 3 | 4 | -- Create new table with nullable category 5 | CREATE TABLE "new_Fact" ( 6 | "id" TEXT NOT NULL PRIMARY KEY, 7 | "content" TEXT NOT NULL, 8 | "strictness" TEXT NOT NULL, 9 | "type" TEXT NOT NULL, 10 | "category" TEXT, 11 | "minVersion" TEXT NOT NULL, 12 | "maxVersion" TEXT NOT NULL, 13 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 14 | "updatedAt" DATETIME NOT NULL, 15 | "applicable" BOOLEAN NOT NULL DEFAULT true, 16 | "content_embedding" TEXT 17 | ); 18 | 19 | -- Copy existing data 20 | INSERT INTO "new_Fact" ("applicable", "content", "content_embedding", "createdAt", "id", "maxVersion", "minVersion", "strictness", "type", "updatedAt") 21 | SELECT "applicable", "content", "content_embedding", "createdAt", "id", "maxVersion", "minVersion", "strictness", "type", "updatedAt" 22 | FROM "Fact"; 23 | 24 | -- Drop old table 25 | DROP TABLE "Fact"; 26 | ALTER TABLE "new_Fact" RENAME TO "Fact"; 27 | 28 | -- Update existing records with appropriate categories 29 | UPDATE "Fact" SET "category" = 'FRONTEND' WHERE "type" = 'styling'; 30 | UPDATE "Fact" SET "category" = 'FRAMEWORK' WHERE "type" = 'framework'; 31 | 32 | -- Make category required by creating new table 33 | CREATE TABLE "new_Fact2" ( 34 | "id" TEXT NOT NULL PRIMARY KEY, 35 | "content" TEXT NOT NULL, 36 | "strictness" TEXT NOT NULL, 37 | "type" TEXT NOT NULL, 38 | "category" TEXT NOT NULL, 39 | "minVersion" TEXT NOT NULL, 40 | "maxVersion" TEXT NOT NULL, 41 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 42 | "updatedAt" DATETIME NOT NULL, 43 | "applicable" BOOLEAN NOT NULL DEFAULT true, 44 | "content_embedding" TEXT 45 | ); 46 | 47 | -- Copy data with non-null categories 48 | INSERT INTO "new_Fact2" SELECT * FROM "Fact"; 49 | 50 | -- Replace the table 51 | DROP TABLE "Fact"; 52 | ALTER TABLE "new_Fact2" RENAME TO "Fact"; 53 | 54 | PRAGMA foreign_keys=ON; 55 | -------------------------------------------------------------------------------- /facts-server/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (e.g., Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /facts-server/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "sqlite" 3 | url = "file:./facts.db" 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | model Fact { 11 | id String @id 12 | content String 13 | strictness String // Will map to StrictnessLevel enum 14 | type String 15 | category String // Will map to FactCategory enum 16 | minVersion String 17 | maxVersion String 18 | createdAt DateTime @default(now()) 19 | updatedAt DateTime @updatedAt 20 | applicable Boolean @default(true) 21 | content_embedding String? 22 | 23 | // Relations 24 | conditions Condition[] 25 | acceptanceCriteria AcceptanceCriterion[] 26 | } 27 | 28 | model Condition { 29 | id String @id @default(cuid()) 30 | factId String 31 | type String // REQUIRES or CONFLICTS_WITH 32 | targetId String // The ID of the fact this condition references 33 | fact Fact @relation(fields: [factId], references: [id]) 34 | } 35 | 36 | model AcceptanceCriterion { 37 | id String @id @default(cuid()) 38 | factId String 39 | description String 40 | validationType String // MANUAL or AUTOMATED 41 | validationScript String? 42 | fact Fact @relation(fields: [factId], references: [id]) 43 | } 44 | -------------------------------------------------------------------------------- /facts-server/prisma/setup_vector.sql: -------------------------------------------------------------------------------- 1 | -- Load sqlite-vec extension 2 | SELECT load_extension('sqlite-vec'); 3 | 4 | -- Create a function to calculate vector similarity 5 | CREATE FUNCTION vector_similarity(vec1 TEXT, vec2 TEXT) 6 | RETURNS REAL AS 7 | BEGIN 8 | RETURN cosine_similarity(json(vec1), json(vec2)); 9 | END; 10 | -------------------------------------------------------------------------------- /facts-server/src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { Server } from '@modelcontextprotocol/sdk/server/index.js'; 3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 4 | import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from '@modelcontextprotocol/sdk/types.js'; 5 | import { PrismaStorageProvider } from './storage.js'; 6 | import { validateCriteria, validateFactConditions } from './validation.js'; 7 | import { StrictnessLevel, FactCategory, AcceptanceCriterion, Condition } from './types.js'; 8 | 9 | class FactsServer { 10 | private server: Server; 11 | private storage: PrismaStorageProvider; 12 | 13 | constructor() { 14 | this.storage = new PrismaStorageProvider(); 15 | this.server = new Server( 16 | { 17 | name: 'facts-server', 18 | version: '0.1.0', 19 | }, 20 | { 21 | capabilities: { 22 | tools: { 23 | get_all_facts: { 24 | description: 'Get all facts in the system', 25 | inputSchema: { 26 | type: 'object', 27 | properties: {} 28 | } 29 | }, 30 | get_fact: { 31 | description: 'Get a fact by ID', 32 | inputSchema: { 33 | type: 'object', 34 | properties: { 35 | id: { 36 | type: 'string', 37 | description: 'The ID of the fact to retrieve' 38 | } 39 | }, 40 | required: ['id'] 41 | } 42 | }, 43 | search_facts: { 44 | description: 'Search facts by type, strictness, and version', 45 | inputSchema: { 46 | type: 'object', 47 | properties: { 48 | type: { 49 | type: 'string', 50 | description: 'Filter by fact type' 51 | }, 52 | strictness: { 53 | type: 'string', 54 | enum: Object.values(StrictnessLevel), 55 | description: 'Filter by strictness level' 56 | }, 57 | version: { 58 | type: 'string', 59 | description: 'Filter by version compatibility' 60 | } 61 | } 62 | } 63 | }, 64 | set_fact: { 65 | description: 'Create or update a fact', 66 | inputSchema: { 67 | type: 'object', 68 | properties: { 69 | id: { type: 'string' }, 70 | content: { type: 'string' }, 71 | strictness: { 72 | type: 'string', 73 | enum: Object.values(StrictnessLevel) 74 | }, 75 | type: { type: 'string' }, 76 | category: { 77 | type: 'string', 78 | enum: Object.values(FactCategory), 79 | description: 'The category this fact belongs to' 80 | }, 81 | minVersion: { type: 'string' }, 82 | maxVersion: { type: 'string' }, 83 | conditions: { 84 | type: 'array', 85 | items: { 86 | type: 'object', 87 | properties: { 88 | factId: { type: 'string' }, 89 | type: { 90 | type: 'string', 91 | enum: ['REQUIRES', 'CONFLICTS_WITH'] 92 | } 93 | }, 94 | required: ['factId', 'type'] 95 | } 96 | }, 97 | acceptanceCriteria: { 98 | type: 'array', 99 | items: { 100 | type: 'object', 101 | properties: { 102 | id: { type: 'string' }, 103 | description: { type: 'string' }, 104 | validationType: { 105 | type: 'string', 106 | enum: ['MANUAL', 'AUTOMATED'] 107 | }, 108 | validationScript: { type: 'string' } 109 | }, 110 | required: ['id', 'description', 'validationType'] 111 | } 112 | } 113 | }, 114 | required: ['id', 'content', 'strictness', 'type', 'category', 'minVersion', 'maxVersion'] 115 | } 116 | }, 117 | validate_criteria: { 118 | description: 'Validate content against fact acceptance criteria', 119 | inputSchema: { 120 | type: 'object', 121 | properties: { 122 | factId: { 123 | type: 'string', 124 | description: 'ID of the fact to validate against' 125 | }, 126 | content: { 127 | type: 'string', 128 | description: 'Content to validate' 129 | } 130 | }, 131 | required: ['factId', 'content'] 132 | } 133 | } 134 | } 135 | } 136 | } 137 | ); 138 | 139 | this.setupHandlers(); 140 | } 141 | 142 | private setupHandlers(): void { 143 | // Handler for listing available tools 144 | this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ 145 | tools: [ 146 | { 147 | name: 'get_all_facts', 148 | description: 'Get all facts in the system', 149 | inputSchema: { 150 | type: 'object', 151 | properties: {} 152 | } 153 | }, 154 | { 155 | name: 'get_fact', 156 | description: 'Get a fact by ID', 157 | inputSchema: { 158 | type: 'object', 159 | properties: { 160 | id: { 161 | type: 'string', 162 | description: 'The ID of the fact to retrieve' 163 | } 164 | }, 165 | required: ['id'] 166 | } 167 | }, 168 | { 169 | name: 'search_facts', 170 | description: 'Search facts by type, strictness, and version', 171 | inputSchema: { 172 | type: 'object', 173 | properties: { 174 | type: { 175 | type: 'string', 176 | description: 'Filter by fact type' 177 | }, 178 | strictness: { 179 | type: 'string', 180 | enum: Object.values(StrictnessLevel), 181 | description: 'Filter by strictness level' 182 | }, 183 | version: { 184 | type: 'string', 185 | description: 'Filter by version compatibility' 186 | } 187 | } 188 | } 189 | }, 190 | { 191 | name: 'set_fact', 192 | description: 'Create or update a fact', 193 | inputSchema: { 194 | type: 'object', 195 | properties: { 196 | id: { type: 'string' }, 197 | content: { type: 'string' }, 198 | strictness: { 199 | type: 'string', 200 | enum: Object.values(StrictnessLevel) 201 | }, 202 | type: { type: 'string' }, 203 | category: { 204 | type: 'string', 205 | enum: Object.values(FactCategory), 206 | description: 'The category this fact belongs to' 207 | }, 208 | minVersion: { type: 'string' }, 209 | maxVersion: { type: 'string' }, 210 | conditions: { 211 | type: 'array', 212 | items: { 213 | type: 'object', 214 | properties: { 215 | factId: { type: 'string' }, 216 | type: { 217 | type: 'string', 218 | enum: ['REQUIRES', 'CONFLICTS_WITH'] 219 | } 220 | }, 221 | required: ['factId', 'type'] 222 | } 223 | }, 224 | acceptanceCriteria: { 225 | type: 'array', 226 | items: { 227 | type: 'object', 228 | properties: { 229 | id: { type: 'string' }, 230 | description: { type: 'string' }, 231 | validationType: { 232 | type: 'string', 233 | enum: ['MANUAL', 'AUTOMATED'] 234 | }, 235 | validationScript: { type: 'string' } 236 | }, 237 | required: ['id', 'description', 'validationType'] 238 | } 239 | } 240 | }, 241 | required: ['id', 'content', 'strictness', 'type', 'category', 'minVersion', 'maxVersion'] 242 | } 243 | }, 244 | { 245 | name: 'validate_criteria', 246 | description: 'Validate content against fact acceptance criteria', 247 | inputSchema: { 248 | type: 'object', 249 | properties: { 250 | factId: { 251 | type: 'string', 252 | description: 'ID of the fact to validate against' 253 | }, 254 | content: { 255 | type: 'string', 256 | description: 'Content to validate' 257 | } 258 | }, 259 | required: ['factId', 'content'] 260 | } 261 | } 262 | ] 263 | })); 264 | 265 | // Handler for executing tools 266 | this.server.setRequestHandler(CallToolRequestSchema, async (request) => { 267 | switch (request.params.name) { 268 | case 'get_all_facts': { 269 | const facts = await this.storage.searchFacts({}); 270 | return { 271 | content: [{ type: 'text', text: JSON.stringify(facts, null, 2) }] 272 | }; 273 | } 274 | 275 | case 'get_fact': { 276 | const args = request.params.arguments as { id: string }; 277 | const fact = await this.storage.getFact(args.id); 278 | if (!fact) { 279 | return { 280 | content: [{ type: 'text', text: `Fact not found: ${args.id}` }], 281 | isError: true 282 | }; 283 | } 284 | return { 285 | content: [{ type: 'text', text: JSON.stringify(fact, null, 2) }] 286 | }; 287 | } 288 | 289 | case 'search_facts': { 290 | const args = request.params.arguments as { 291 | type?: string; 292 | category?: FactCategory; 293 | strictness?: StrictnessLevel; 294 | version?: string; 295 | }; 296 | const facts = await this.storage.searchFacts(args); 297 | return { 298 | content: [{ type: 'text', text: JSON.stringify(facts, null, 2) }] 299 | }; 300 | } 301 | 302 | case 'set_fact': { 303 | const args = request.params.arguments as { 304 | id: string; 305 | content: string; 306 | strictness: StrictnessLevel; 307 | type: string; 308 | category: FactCategory; 309 | minVersion: string; 310 | maxVersion: string; 311 | conditions?: Condition[]; 312 | acceptanceCriteria?: AcceptanceCriterion[]; 313 | }; 314 | try { 315 | await this.storage.setFact( 316 | args.id, 317 | args.content, 318 | args.strictness as StrictnessLevel, 319 | args.type, 320 | args.category, 321 | args.minVersion, 322 | args.maxVersion, 323 | args.conditions || [], 324 | args.acceptanceCriteria || [] 325 | ); 326 | return { 327 | content: [{ type: 'text', text: `Fact ${args.id} saved successfully` }] 328 | }; 329 | } catch (error) { 330 | return { 331 | content: [{ type: 'text', text: `Error saving fact: ${error instanceof Error ? error.message : 'Unknown error'}` }], 332 | isError: true 333 | }; 334 | } 335 | } 336 | 337 | case 'validate_criteria': { 338 | const args = request.params.arguments as { 339 | factId: string; 340 | content: string; 341 | }; 342 | const fact = await this.storage.getFact(args.factId); 343 | if (!fact) { 344 | return { 345 | content: [{ type: 'text', text: `Fact not found: ${args.factId}` }], 346 | isError: true 347 | }; 348 | } 349 | 350 | try { 351 | const results = await validateCriteria(args.content, fact.acceptanceCriteria); 352 | return { 353 | content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] 354 | }; 355 | } catch (error) { 356 | return { 357 | content: [{ type: 'text', text: `Validation error: ${error instanceof Error ? error.message : 'Unknown error'}` }], 358 | isError: true 359 | }; 360 | } 361 | } 362 | } 363 | 364 | throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); 365 | }); 366 | } 367 | 368 | async run(): Promise { 369 | const transport = new StdioServerTransport(); 370 | await this.server.connect(transport); 371 | console.error('Facts MCP server running on stdio'); 372 | 373 | // Handle cleanup on shutdown 374 | process.on('SIGINT', async () => { 375 | await this.storage.close(); 376 | process.exit(0); 377 | }); 378 | 379 | process.on('SIGTERM', async () => { 380 | await this.storage.close(); 381 | process.exit(0); 382 | }); 383 | } 384 | } 385 | 386 | const server = new FactsServer(); 387 | server.run().catch(console.error); 388 | -------------------------------------------------------------------------------- /facts-server/src/storage.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient, Prisma } from '@prisma/client'; 2 | import { StorageProvider, StorageSearchResult, StrictnessLevel, FactCategory, Condition, AcceptanceCriterion } from './types.js'; 3 | 4 | export class PrismaStorageProvider implements StorageProvider { 5 | private prisma: PrismaClient; 6 | 7 | constructor() { 8 | this.prisma = new PrismaClient(); 9 | } 10 | 11 | async setFact( 12 | id: string, 13 | content: string, 14 | strictness: StrictnessLevel, 15 | type: string, 16 | category: FactCategory, 17 | minVersion: string, 18 | maxVersion: string, 19 | conditions: Condition[], 20 | acceptanceCriteria: AcceptanceCriterion[], 21 | contentEmbedding?: string 22 | ): Promise { 23 | const data: Prisma.FactCreateInput | Prisma.FactUpdateInput = { 24 | id, 25 | content, 26 | strictness, 27 | type, 28 | category, 29 | minVersion, 30 | maxVersion, 31 | content_embedding: contentEmbedding, 32 | conditions: { 33 | create: conditions.map(condition => ({ 34 | type: condition.type, 35 | targetId: condition.factId 36 | })) 37 | }, 38 | acceptanceCriteria: { 39 | create: acceptanceCriteria.map(criterion => ({ 40 | id: criterion.id, 41 | description: criterion.description, 42 | validationType: criterion.validationType, 43 | validationScript: criterion.validationScript 44 | })) 45 | } 46 | }; 47 | 48 | await this.prisma.fact.upsert({ 49 | where: { id }, 50 | create: data as Prisma.FactCreateInput, 51 | update: { 52 | ...data, 53 | conditions: { 54 | deleteMany: {}, 55 | create: conditions.map(condition => ({ 56 | type: condition.type, 57 | targetId: condition.factId 58 | })) 59 | }, 60 | acceptanceCriteria: { 61 | deleteMany: {}, 62 | create: acceptanceCriteria.map(criterion => ({ 63 | id: criterion.id, 64 | description: criterion.description, 65 | validationType: criterion.validationType, 66 | validationScript: criterion.validationScript 67 | })) 68 | } 69 | } 70 | }); 71 | } 72 | 73 | async getFact(id: string): Promise { 74 | const fact = await this.prisma.fact.findUnique({ 75 | where: { id }, 76 | include: { 77 | conditions: true, 78 | acceptanceCriteria: true 79 | } 80 | }); 81 | 82 | if (!fact) { 83 | return null; 84 | } 85 | 86 | return { 87 | id: fact.id, 88 | content: fact.content, 89 | strictness: fact.strictness as StrictnessLevel, 90 | type: fact.type, 91 | category: fact.category as FactCategory, 92 | minVersion: fact.minVersion, 93 | maxVersion: fact.maxVersion, 94 | conditions: fact.conditions.map(c => ({ 95 | factId: c.targetId, 96 | type: c.type as 'REQUIRES' | 'CONFLICTS_WITH' 97 | })), 98 | acceptanceCriteria: fact.acceptanceCriteria.map(ac => ({ 99 | id: ac.id, 100 | description: ac.description, 101 | validationType: ac.validationType as 'MANUAL' | 'AUTOMATED', 102 | validationScript: ac.validationScript || undefined 103 | })), 104 | createdAt: fact.createdAt.toISOString(), 105 | updatedAt: fact.updatedAt.toISOString(), 106 | applicable: fact.applicable 107 | }; 108 | } 109 | 110 | async searchFacts(options: { 111 | type?: string; 112 | category?: FactCategory; 113 | strictness?: StrictnessLevel; 114 | version?: string; 115 | embedding?: string; 116 | similarityThreshold?: number; 117 | }): Promise { 118 | let facts; 119 | 120 | if (options.embedding) { 121 | // Use raw query for vector similarity search 122 | const threshold = options.similarityThreshold || 0.8; 123 | const whereConditions = []; 124 | const params: any[] = [options.embedding, threshold]; 125 | 126 | if (options.type) { 127 | whereConditions.push('f.type = ?'); 128 | params.push(options.type); 129 | } 130 | 131 | if (options.category) { 132 | whereConditions.push('f.category = ?'); 133 | params.push(options.category); 134 | } 135 | 136 | if (options.strictness) { 137 | whereConditions.push('f.strictness = ?'); 138 | params.push(options.strictness); 139 | } 140 | 141 | if (options.version) { 142 | whereConditions.push('f.minVersion <= ? AND f.maxVersion >= ?'); 143 | params.push(options.version, options.version); 144 | } 145 | 146 | const whereClause = whereConditions.length 147 | ? 'AND ' + whereConditions.join(' AND ') 148 | : ''; 149 | 150 | const rawResults = await this.prisma.$queryRawUnsafe>( 151 | `SELECT f.*, 152 | vector_similarity(f.content_embedding, ?) as similarity 153 | FROM "Fact" f 154 | WHERE vector_similarity(f.content_embedding, ?) > ? 155 | ${whereClause} 156 | ORDER BY similarity DESC`, 157 | options.embedding, 158 | options.embedding, 159 | threshold, 160 | ...params 161 | ); 162 | 163 | // Fetch full fact data including relations 164 | const factPromises = rawResults.map(result => 165 | this.prisma.fact.findUnique({ 166 | where: { id: result.id }, 167 | include: { 168 | conditions: true, 169 | acceptanceCriteria: true 170 | } 171 | }) 172 | ); 173 | 174 | facts = (await Promise.all(factPromises)).filter((fact): fact is NonNullable => fact !== null); 175 | } else { 176 | const where: Prisma.FactWhereInput = {}; 177 | 178 | if (options.type) { 179 | where.type = options.type; 180 | } 181 | 182 | if (options.category) { 183 | where.category = options.category; 184 | } 185 | 186 | if (options.strictness) { 187 | where.strictness = options.strictness; 188 | } 189 | 190 | if (options.version) { 191 | where.AND = [ 192 | { minVersion: { lte: options.version } }, 193 | { maxVersion: { gte: options.version } } 194 | ]; 195 | } 196 | 197 | facts = await this.prisma.fact.findMany({ 198 | where, 199 | include: { 200 | conditions: true, 201 | acceptanceCriteria: true 202 | } 203 | }); 204 | } 205 | 206 | return facts.map(fact => ({ 207 | id: fact.id, 208 | content: fact.content, 209 | strictness: fact.strictness as StrictnessLevel, 210 | type: fact.type, 211 | category: fact.category as FactCategory, 212 | minVersion: fact.minVersion, 213 | maxVersion: fact.maxVersion, 214 | conditions: fact.conditions.map(c => ({ 215 | factId: c.targetId, 216 | type: c.type as 'REQUIRES' | 'CONFLICTS_WITH' 217 | })), 218 | acceptanceCriteria: fact.acceptanceCriteria.map(ac => ({ 219 | id: ac.id, 220 | description: ac.description, 221 | validationType: ac.validationType as 'MANUAL' | 'AUTOMATED', 222 | validationScript: ac.validationScript || undefined 223 | })), 224 | createdAt: fact.createdAt.toISOString(), 225 | updatedAt: fact.updatedAt.toISOString(), 226 | applicable: fact.applicable 227 | })); 228 | } 229 | 230 | async deleteFact(id: string): Promise { 231 | try { 232 | await this.prisma.fact.delete({ 233 | where: { id } 234 | }); 235 | return true; 236 | } catch (error) { 237 | return false; 238 | } 239 | } 240 | 241 | async close(): Promise { 242 | await this.prisma.$disconnect(); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /facts-server/src/types.ts: -------------------------------------------------------------------------------- 1 | export enum FactCategory { 2 | // Architecture Layers 3 | FRONTEND = 'FRONTEND', 4 | BACKEND = 'BACKEND', 5 | DATABASE = 'DATABASE', 6 | FULL_STACK = 'FULL_STACK', 7 | 8 | // Development Patterns 9 | DESIGN_PATTERN = 'DESIGN_PATTERN', 10 | ARCHITECTURE_PATTERN = 'ARCHITECTURE_PATTERN', 11 | TESTING_PATTERN = 'TESTING_PATTERN', 12 | 13 | // Code Organization 14 | NAMING_CONVENTION = 'NAMING_CONVENTION', 15 | PROJECT_STRUCTURE = 'PROJECT_STRUCTURE', 16 | CODE_STYLE = 'CODE_STYLE', 17 | 18 | // Operations 19 | DEPLOYMENT = 'DEPLOYMENT', 20 | CI_CD = 'CI_CD', 21 | MONITORING = 'MONITORING', 22 | SECURITY = 'SECURITY', 23 | 24 | // Development Process 25 | GIT_WORKFLOW = 'GIT_WORKFLOW', 26 | CODE_REVIEW = 'CODE_REVIEW', 27 | DOCUMENTATION = 'DOCUMENTATION', 28 | 29 | // Dependencies 30 | PACKAGE_MANAGEMENT = 'PACKAGE_MANAGEMENT', 31 | VERSIONING = 'VERSIONING', 32 | 33 | // Performance 34 | OPTIMIZATION = 'OPTIMIZATION', 35 | CACHING = 'CACHING', 36 | 37 | // Cross-cutting 38 | ACCESSIBILITY = 'ACCESSIBILITY', 39 | INTERNATIONALIZATION = 'INTERNATIONALIZATION', 40 | ERROR_HANDLING = 'ERROR_HANDLING' 41 | } 42 | 43 | export enum StrictnessLevel { 44 | REQUIRED = 'REQUIRED', 45 | RECOMMENDED = 'RECOMMENDED', 46 | OPTIONAL = 'OPTIONAL' 47 | } 48 | 49 | export interface Condition { 50 | factId: string; 51 | type: 'REQUIRES' | 'CONFLICTS_WITH'; 52 | } 53 | 54 | export type ValidationType = 'MANUAL' | 'AUTOMATED' | 'URL_CHECK'; 55 | 56 | export interface AcceptanceCriterion { 57 | id: string; 58 | description: string; 59 | validationType: ValidationType; 60 | validationScript?: string; 61 | } 62 | 63 | export interface FactData { 64 | content: string; 65 | strictness: StrictnessLevel; 66 | type: string; 67 | category: FactCategory; 68 | minVersion: string; 69 | maxVersion: string; 70 | conditions: Condition[]; 71 | acceptanceCriteria: AcceptanceCriterion[]; 72 | createdAt: string; 73 | updatedAt: string; 74 | } 75 | 76 | export interface StorageSearchResult extends FactData { 77 | id: string; 78 | applicable: boolean; 79 | } 80 | 81 | export interface ValidationResult { 82 | criterionId: string; 83 | passed: boolean; 84 | message?: string; 85 | } 86 | 87 | export interface StorageProvider { 88 | setFact( 89 | id: string, 90 | content: string, 91 | strictness: StrictnessLevel, 92 | type: string, 93 | category: FactCategory, 94 | minVersion: string, 95 | maxVersion: string, 96 | conditions: Condition[], 97 | acceptanceCriteria: AcceptanceCriterion[], 98 | contentEmbedding?: string 99 | ): Promise; 100 | 101 | getFact(id: string): Promise; 102 | 103 | searchFacts(options: { 104 | type?: string; 105 | strictness?: StrictnessLevel; 106 | version?: string; 107 | embedding?: string; 108 | similarityThreshold?: number; 109 | }): Promise; 110 | 111 | deleteFact(id: string): Promise; 112 | } 113 | -------------------------------------------------------------------------------- /facts-server/src/validation.ts: -------------------------------------------------------------------------------- 1 | import { AcceptanceCriterion, ValidationResult } from './types.js'; 2 | 3 | export class ValidationError extends Error { 4 | constructor(message: string) { 5 | super(message); 6 | this.name = 'ValidationError'; 7 | } 8 | } 9 | 10 | export async function validateCriteria( 11 | content: string, 12 | criteria: AcceptanceCriterion[] 13 | ): Promise { 14 | const results: ValidationResult[] = []; 15 | 16 | for (const criterion of criteria) { 17 | if (criterion.validationType === 'MANUAL') { 18 | results.push({ 19 | criterionId: criterion.id, 20 | passed: false, 21 | message: 'Manual validation required' 22 | }); 23 | continue; 24 | } 25 | 26 | if (!criterion.validationScript) { 27 | throw new ValidationError(`Automated criterion ${criterion.id} has no validation script`); 28 | } 29 | 30 | try { 31 | // Create a function from the validation script 32 | const validationFn = new Function('content', criterion.validationScript + '\nreturn ' + criterion.validationScript.match(/function\s+(\w+)/)?.[1] + '(content);'); 33 | 34 | const passed = await Promise.resolve(validationFn(content)); 35 | 36 | results.push({ 37 | criterionId: criterion.id, 38 | passed, 39 | message: passed ? 'Validation passed' : 'Validation failed' 40 | }); 41 | } catch (error) { 42 | results.push({ 43 | criterionId: criterion.id, 44 | passed: false, 45 | message: `Validation error: ${error instanceof Error ? error.message : 'Unknown error'}` 46 | }); 47 | } 48 | } 49 | 50 | return results; 51 | } 52 | 53 | export function validateFactConditions( 54 | factConditions: Map, 55 | conditions: { factId: string; type: 'REQUIRES' | 'CONFLICTS_WITH' }[] 56 | ): boolean { 57 | for (const condition of conditions) { 58 | const factExists = factConditions.get(condition.factId) ?? false; 59 | 60 | if (condition.type === 'REQUIRES' && !factExists) { 61 | return false; 62 | } 63 | 64 | if (condition.type === 'CONFLICTS_WITH' && factExists) { 65 | return false; 66 | } 67 | } 68 | 69 | return true; 70 | } 71 | -------------------------------------------------------------------------------- /facts-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ES2020", 5 | "moduleResolution": "node", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "declaration": true 13 | }, 14 | "include": [ 15 | "src/**/*" 16 | ], 17 | "exclude": [ 18 | "node_modules", 19 | "build" 20 | ] 21 | } -------------------------------------------------------------------------------- /facts-ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /facts-ui/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /facts-ui/app/api/mcp/route.ts: -------------------------------------------------------------------------------- 1 | import { Server } from "@modelcontextprotocol/sdk/server" 2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio" 3 | 4 | export async function POST(request: Request) { 5 | try { 6 | const body = await request.json() 7 | const { server, tool, args } = body 8 | 9 | if (server !== "facts") { 10 | return Response.json({ error: "Invalid MCP server" }, { status: 400 }) 11 | } 12 | 13 | const mcpServer = new Server( 14 | { 15 | name: "facts-server", 16 | version: "0.1.0", 17 | }, 18 | { 19 | capabilities: { 20 | tools: {}, 21 | }, 22 | } 23 | ) 24 | 25 | const transport = new StdioServerTransport() 26 | await mcpServer.connect(transport) 27 | 28 | const result = await mcpServer.callTool(tool, args) 29 | return Response.json(result) 30 | } catch (error) { 31 | console.error("MCP error:", error) 32 | return Response.json( 33 | { error: "Failed to process MCP request" }, 34 | { status: 500 } 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /facts-ui/app/facts/new/page.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button" 2 | import Link from "next/link" 3 | import { NewFactForm } from "@/components/facts/new-fact-form" 4 | 5 | export default function NewFactPage() { 6 | return ( 7 |
8 |
9 |

Add New Fact

10 | 13 |
14 | 15 |
16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /facts-ui/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewhopper/hivemind/b82fb50a74b5eadef26a19cc1f69a178772b29d3/facts-ui/app/favicon.ico -------------------------------------------------------------------------------- /facts-ui/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 0 0% 3.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 0 0% 3.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 0 0% 3.9%; 15 | 16 | --primary: 0 0% 9%; 17 | --primary-foreground: 0 0% 98%; 18 | 19 | --secondary: 0 0% 96.1%; 20 | --secondary-foreground: 0 0% 9%; 21 | 22 | --muted: 0 0% 96.1%; 23 | --muted-foreground: 0 0% 45.1%; 24 | 25 | --accent: 0 0% 96.1%; 26 | --accent-foreground: 0 0% 9%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 0 0% 98%; 30 | 31 | --border: 0 0% 89.8%; 32 | --input: 0 0% 89.8%; 33 | --ring: 0 0% 3.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 0 0% 3.9%; 40 | --foreground: 0 0% 98%; 41 | 42 | --card: 0 0% 3.9%; 43 | --card-foreground: 0 0% 98%; 44 | 45 | --popover: 0 0% 3.9%; 46 | --popover-foreground: 0 0% 98%; 47 | 48 | --primary: 0 0% 98%; 49 | --primary-foreground: 0 0% 9%; 50 | 51 | --secondary: 0 0% 14.9%; 52 | --secondary-foreground: 0 0% 98%; 53 | 54 | --muted: 0 0% 14.9%; 55 | --muted-foreground: 0 0% 63.9%; 56 | 57 | --accent: 0 0% 14.9%; 58 | --accent-foreground: 0 0% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 0 0% 98%; 62 | 63 | --border: 0 0% 14.9%; 64 | --input: 0 0% 14.9%; 65 | --ring: 0 0% 83.1%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } 77 | 78 | 79 | 80 | @layer base { 81 | * { 82 | @apply border-border outline-ring/50; 83 | } 84 | body { 85 | @apply bg-background text-foreground; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /facts-ui/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | import { Toaster } from "@/components/ui/sonner"; 5 | 6 | const inter = Inter({ subsets: ["latin"] }); 7 | 8 | export const metadata: Metadata = { 9 | title: "Facts Library", 10 | description: "A library of development facts and guidelines", 11 | }; 12 | 13 | interface RootLayoutProps { 14 | children: React.ReactNode; 15 | } 16 | 17 | export default function RootLayout({ children }: RootLayoutProps) { 18 | return ( 19 | 20 | 21 | {children} 22 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /facts-ui/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { DataTable } from "@/components/facts/data-table" 2 | import { columns } from "@/components/facts/columns" 3 | import { getAllFacts } from "@/lib/api/facts" 4 | import { Button } from "@/components/ui/button" 5 | import Link from "next/link" 6 | 7 | export default async function Home() { 8 | const facts = await getAllFacts() 9 | 10 | return ( 11 |
12 |
13 |
14 |

Facts Library

15 | 18 |
19 | 20 |
21 |
22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /facts-ui/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /facts-ui/components/facts/columns.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ColumnDef } from "@tanstack/react-table" 4 | 5 | export type Fact = { 6 | id: string 7 | title: string 8 | description: string 9 | source: string 10 | createdAt: Date 11 | } 12 | 13 | export const columns: ColumnDef[] = [ 14 | { 15 | accessorKey: "title", 16 | header: "Title", 17 | }, 18 | { 19 | accessorKey: "description", 20 | header: "Description", 21 | }, 22 | { 23 | accessorKey: "source", 24 | header: "Source", 25 | }, 26 | ] -------------------------------------------------------------------------------- /facts-ui/components/facts/data-table.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | ColumnDef, 5 | flexRender, 6 | getCoreRowModel, 7 | useReactTable, 8 | } from "@tanstack/react-table" 9 | 10 | import { 11 | Table, 12 | TableBody, 13 | TableCell, 14 | TableHead, 15 | TableHeader, 16 | TableRow, 17 | } from "@/components/ui/table" 18 | 19 | interface DataTableProps { 20 | columns: ColumnDef[] 21 | data: TData[] 22 | } 23 | 24 | export function DataTable({ 25 | columns, 26 | data, 27 | }: DataTableProps) { 28 | const table = useReactTable({ 29 | data, 30 | columns, 31 | getCoreRowModel: getCoreRowModel(), 32 | }) 33 | 34 | return ( 35 |
36 | 37 | 38 | {table.getHeaderGroups().map((headerGroup) => ( 39 | 40 | {headerGroup.headers.map((header) => ( 41 | 42 | {header.isPlaceholder 43 | ? null 44 | : flexRender( 45 | header.column.columnDef.header, 46 | header.getContext() 47 | )} 48 | 49 | ))} 50 | 51 | ))} 52 | 53 | 54 | {table.getRowModel().rows?.length ? ( 55 | table.getRowModel().rows.map((row) => ( 56 | 57 | {row.getVisibleCells().map((cell) => ( 58 | 59 | {flexRender(cell.column.columnDef.cell, cell.getContext())} 60 | 61 | ))} 62 | 63 | )) 64 | ) : ( 65 | 66 | 67 | No results. 68 | 69 | 70 | )} 71 | 72 |
73 |
74 | ) 75 | } -------------------------------------------------------------------------------- /facts-ui/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /facts-ui/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /facts-ui/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": [ 6 | "./*" 7 | ] 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /facts-ui/lib/api/facts.ts: -------------------------------------------------------------------------------- 1 | import prisma from '@/lib/prisma' 2 | 3 | export async function getAllFacts() { 4 | const facts = await prisma.fact.findMany({ 5 | orderBy: { 6 | createdAt: 'desc', 7 | }, 8 | }) 9 | return facts 10 | } -------------------------------------------------------------------------------- /facts-ui/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /facts-ui/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /facts-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "facts-ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@hookform/resolvers": "^4.1.1", 13 | "@modelcontextprotocol/sdk": "^1.5.0", 14 | "@prisma/client": "^6.4.1", 15 | "@radix-ui/react-dialog": "^1.1.6", 16 | "@radix-ui/react-dropdown-menu": "^2.1.6", 17 | "@radix-ui/react-label": "^2.1.2", 18 | "@radix-ui/react-popover": "^1.1.6", 19 | "@radix-ui/react-scroll-area": "^1.2.3", 20 | "@radix-ui/react-select": "^2.1.6", 21 | "@radix-ui/react-separator": "^1.1.2", 22 | "@radix-ui/react-slot": "^1.1.2", 23 | "@radix-ui/react-tabs": "^1.1.3", 24 | "@tanstack/react-table": "^8.21.2", 25 | "class-variance-authority": "^0.7.1", 26 | "clsx": "^2.1.1", 27 | "lucide-react": "^0.475.0", 28 | "next": "15.1.7", 29 | "next-themes": "^0.4.4", 30 | "prisma": "^6.4.1", 31 | "react": "^19.0.0", 32 | "react-dom": "^19.0.0", 33 | "react-hook-form": "^7.54.2", 34 | "sonner": "^2.0.1", 35 | "tailwind-merge": "^3.0.2", 36 | "tailwindcss-animate": "^1.0.7", 37 | "zod": "^3.24.2" 38 | }, 39 | "devDependencies": { 40 | "@eslint/eslintrc": "^3", 41 | "@types/node": "^20", 42 | "@types/react": "^19", 43 | "@types/react-dom": "^19", 44 | "eslint": "^9", 45 | "eslint-config-next": "15.1.7", 46 | "postcss": "^8", 47 | "tailwindcss": "^3.4.1", 48 | "typescript": "^5" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /facts-ui/path/to/file: -------------------------------------------------------------------------------- 1 | import { columns } from '../../components/facts/columns' -------------------------------------------------------------------------------- /facts-ui/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /facts-ui/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "sqlite" 3 | url = "../../facts-server/prisma/facts.db" 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | model Fact { 11 | id String @id 12 | content String 13 | strictness String // Will map to StrictnessLevel enum 14 | type String 15 | category String // Will map to FactCategory enum 16 | minVersion String 17 | maxVersion String 18 | createdAt DateTime @default(now()) 19 | updatedAt DateTime @updatedAt 20 | applicable Boolean @default(true) 21 | content_embedding String? 22 | 23 | // Relations 24 | conditions Condition[] 25 | acceptanceCriteria AcceptanceCriterion[] 26 | } 27 | 28 | model Condition { 29 | id String @id @default(cuid()) 30 | factId String 31 | type String // REQUIRES or CONFLICTS_WITH 32 | targetId String // The ID of the fact this condition references 33 | fact Fact @relation(fields: [factId], references: [id]) 34 | } 35 | 36 | model AcceptanceCriterion { 37 | id String @id @default(cuid()) 38 | factId String 39 | description String 40 | validationType String // MANUAL or AUTOMATED 41 | validationScript String? 42 | fact Fact @relation(fields: [factId], references: [id]) 43 | } 44 | -------------------------------------------------------------------------------- /facts-ui/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /facts-ui/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /facts-ui/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /facts-ui/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /facts-ui/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /facts-ui/src/components/facts/columns.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ColumnDef } from "@tanstack/react-table" 4 | import { Fact, StrictnessLevel } from "@/lib/types/facts" 5 | import { Badge } from "@/components/ui/badge" 6 | import { Button } from "@/components/ui/button" 7 | import { MoreHorizontal } from "lucide-react" 8 | import { 9 | DropdownMenu, 10 | DropdownMenuContent, 11 | DropdownMenuItem, 12 | DropdownMenuLabel, 13 | DropdownMenuSeparator, 14 | DropdownMenuTrigger, 15 | } from "@/components/ui/dropdown-menu" 16 | import Link from "next/link" 17 | 18 | export const columns: ColumnDef[] = [ 19 | { 20 | accessorKey: "id", 21 | header: "ID", 22 | }, 23 | { 24 | accessorKey: "content", 25 | header: "Content", 26 | }, 27 | { 28 | accessorKey: "type", 29 | header: "Type", 30 | }, 31 | { 32 | accessorKey: "category", 33 | header: "Category", 34 | }, 35 | { 36 | accessorKey: "strictness", 37 | header: "Strictness", 38 | cell: ({ row }) => { 39 | const strictness = row.getValue("strictness") as StrictnessLevel 40 | return ( 41 | 46 | {strictness} 47 | 48 | ) 49 | }, 50 | }, 51 | { 52 | id: "actions", 53 | cell: ({ row }) => { 54 | const fact = row.original 55 | 56 | return ( 57 | 58 | 59 | 63 | 64 | 65 | Actions 66 | 67 | View Details 68 | 69 | 70 | 71 | Edit 72 | 73 | 74 | 75 | ) 76 | }, 77 | }, 78 | ] 79 | -------------------------------------------------------------------------------- /facts-ui/src/components/facts/data-table.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | ColumnDef, 5 | flexRender, 6 | getCoreRowModel, 7 | useReactTable, 8 | getPaginationRowModel, 9 | getSortedRowModel, 10 | SortingState, 11 | } from "@tanstack/react-table" 12 | import { 13 | Table, 14 | TableBody, 15 | TableCell, 16 | TableHead, 17 | TableHeader, 18 | TableRow, 19 | } from "@/components/ui/table" 20 | import { Button } from "@/components/ui/button" 21 | import { useState } from "react" 22 | 23 | interface DataTableProps { 24 | columns: ColumnDef[] 25 | data: TData[] 26 | } 27 | 28 | export function DataTable({ 29 | columns, 30 | data, 31 | }: DataTableProps) { 32 | const [sorting, setSorting] = useState([]) 33 | 34 | const table = useReactTable({ 35 | data, 36 | columns, 37 | getCoreRowModel: getCoreRowModel(), 38 | getPaginationRowModel: getPaginationRowModel(), 39 | getSortedRowModel: getSortedRowModel(), 40 | onSortingChange: setSorting, 41 | state: { 42 | sorting, 43 | }, 44 | }) 45 | 46 | return ( 47 |
48 |
49 | 50 | 51 | {table.getHeaderGroups().map((headerGroup) => ( 52 | 53 | {headerGroup.headers.map((header) => { 54 | return ( 55 | 56 | {header.isPlaceholder 57 | ? null 58 | : flexRender( 59 | header.column.columnDef.header, 60 | header.getContext() 61 | )} 62 | 63 | ) 64 | })} 65 | 66 | ))} 67 | 68 | 69 | {table.getRowModel().rows?.length ? ( 70 | table.getRowModel().rows.map((row) => ( 71 | 75 | {row.getVisibleCells().map((cell) => ( 76 | 77 | {flexRender( 78 | cell.column.columnDef.cell, 79 | cell.getContext() 80 | )} 81 | 82 | ))} 83 | 84 | )) 85 | ) : ( 86 | 87 | 91 | No results. 92 | 93 | 94 | )} 95 | 96 |
97 |
98 |
99 | 107 | 115 |
116 |
117 | ) 118 | } 119 | -------------------------------------------------------------------------------- /facts-ui/src/components/facts/new-fact-form.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | import { useRouter } from "next/navigation" 3 | import { zodResolver } from "@hookform/resolvers/zod" 4 | import { useForm } from "react-hook-form" 5 | import { toast } from "sonner" 6 | import { Button } from "@/components/ui/button" 7 | import { Input } from "@/components/ui/input" 8 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" 9 | import { Textarea } from "@/components/ui/textarea" 10 | import { FactCategory, StrictnessLevel, factSchema, type FactFormData } from "@/lib/types/facts" 11 | import { createFact } from "@/lib/api/facts" 12 | 13 | export function NewFactForm() { 14 | const router = useRouter() 15 | const [isSubmitting, setIsSubmitting] = useState(false) 16 | 17 | const form = useForm({ 18 | resolver: zodResolver(factSchema), 19 | defaultValues: { 20 | conditions: [], 21 | acceptanceCriteria: [] 22 | } 23 | }) 24 | 25 | async function onSubmit(data: FactFormData) { 26 | try { 27 | setIsSubmitting(true) 28 | await createFact(data) 29 | toast.success("Fact created successfully") 30 | router.push("/") 31 | } catch (error) { 32 | toast.error("Failed to create fact") 33 | console.error(error) 34 | } finally { 35 | setIsSubmitting(false) 36 | } 37 | } 38 | 39 | return ( 40 |
41 |
42 |
43 | 44 | 49 | {form.formState.errors.id && ( 50 |

{form.formState.errors.id.message}

51 | )} 52 |
53 | 54 |
55 | 56 |